defmodule LivebookWeb.Endpoint do use Phoenix.Endpoint, otp_app: :livebook # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. # Set :encryption_salt if you would also like to encrypt it. @session_options [ store: :cookie, key: "lb_session", signing_salt: "deadbook" ] # Don't check the origin as we don't know how the web app is gonna be accessed. # It runs locally, but may be exposed via IP or domain name. The WebSocket # connection is already protected from CSWSH by using CSRF token. @websocket_options [ check_origin: false, connect_info: [:user_agent, :uri, session: @session_options] ] socket "/live", Phoenix.LiveView.Socket, websocket: @websocket_options socket "/socket", LivebookWeb.Socket, websocket: @websocket_options # Serve static files at "/". # # In usual Phoenix applications, we serve static files from priv/static, # however Livebook can also be run as escript, in which case it is # packaged into a single file and priv/ is not accessible directly. # In that case, we include priv/ in the escript archive by setting # the :include_priv_for option. Then, on escript boot, we extract # the priv files into a temporary directory. # # To account for both cases, we configure Plug.Static :from as MFA # and return the accessible priv/ location in both scenarios. # # The priv/ static files are generated by the compile.livebook_priv # as part of compilation. We gzip the static files in priv/, since # we want to serve them gzipped, and we don't include the non-gzipped # ones to minimize app size. Note that we still have a separate # static/ directory with the CI-precompiled assets, which we keep # in Git so that people can install escript from GitHub or run # MIX_ENV=prod phx.server, without Node and NPM. Storing minified # assets is already not ideal, but we definitely want to avoid # storing the gzipped variants in Git. That's why we store the # assets uncompressed and then generate priv/static with their # compressed variants at compile time. if code_reloading? do # In development, we use assets from tmp/static_dev, which are # rebuilt on every change. We build to a different directory than # priv/static, to make sure it can be built concurrently. plug Plug.Static, at: "/", from: "tmp/static_dev", gzip: false, only: ["assets"] end plug Plug.Static, at: "/", from: {__MODULE__, :static_from, []}, gzip: true, only: LivebookWeb.static_paths() @doc false def static_from(), do: Path.join(Livebook.Config.priv_path(), "static") plug :force_ssl # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket plug Phoenix.LiveReloader plug Phoenix.CodeReloader end plug Phoenix.LiveDashboard.RequestLogger, param_key: "request_logger", cookie_key: "request_logger" plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] plug LivebookWeb.ProxyPlug plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Phoenix.json_library() plug Plug.MethodOverride plug Plug.Head plug :session plug :purge_cookies # Run custom plugs from the app configuration plug LivebookWeb.ConfiguredPlug plug LivebookWeb.Router @plug_session Plug.Session.init(@session_options ++ [same_site: "Lax"]) @plug_session_iframe Plug.Session.init(@session_options ++ [same_site: "None", secure: true]) def session(conn, _opts) do if Livebook.Config.within_iframe?() do Plug.Session.call(conn, @plug_session_iframe) else Plug.Session.call(conn, @plug_session) end end # Avoid compile-time deps on config as it is invoked as a MFA. @plug_ssl Plug.SSL.init( host: {:"Elixir.Livebook.Config", :force_ssl_host, []}, rewrite_on: {:"Elixir.Livebook.Config", :rewrite_on, []} ) def force_ssl(conn, _opts) do if Livebook.Config.force_ssl_host() do Plug.SSL.call(conn, @plug_ssl) else conn end end def cookie_options() do if Livebook.Config.within_iframe?() do [same_site: "None", secure: true] else [same_site: "Lax"] end end # Because we run on localhost, we may accumulate # cookies from several other apps. Our header limit # is set to 32kB. Once we are 75% of said limit, # we clear other cookies to make sure we don't go # over the limit. def purge_cookies(conn, _opts) do cookie_size = conn |> Plug.Conn.get_req_header("cookie") |> Enum.map(&byte_size/1) |> Enum.sum() if cookie_size > 24576 do conn.cookies |> Enum.reject(fn {key, _value} -> String.starts_with?(key, "lb_") end) |> Enum.take(10) |> Enum.reduce(conn, fn {key, _value}, conn -> Plug.Conn.delete_resp_cookie(conn, key) end) else conn end end def access_struct_url() do base = case struct_url() do %URI{scheme: "https", port: 0} = uri -> %{uri | port: port(:https, 433)} %URI{scheme: "http", port: 0} = uri -> %{uri | port: port(:http, 80)} %URI{} = uri -> uri end base = update_in(base.path, &(&1 || "/")) case Livebook.Config.authentication() do %{mode: :token, secret: token} -> %{base | query: "token=" <> token} _ -> base end end def access_url do URI.to_string(access_struct_url()) end defp port(scheme, default) do try do server_info(scheme) rescue _ -> default else {:ok, {_, port}} when is_integer(port) -> port _ -> default end end end