defmodule DiogramosWeb.PublicUrl do
@moduledoc """
Derives the public-facing origin (`scheme://host[:port]`) a native
client should dial back to.
Resolution, highest precedence first:
1. `DIOGRAMOS_PUBLIC_URL` (explicit override for reverse-proxy setups).
2. Composed from forwarded headers with conn/socket fallbacks:
* scheme: `X-Forwarded-Proto` if present, else the request's scheme
* host: `X-Forwarded-Host` if present (it may already carry `:port`),
else the request's host, with the port appended when non-default
3. The endpoint's configured URL, as a last resort.
Scheme and host are sourced independently, which is the whole point: a
proxy that sets only `X-Forwarded-Proto: https` and passes Host through
unchanged — the common Traefik/nginx default — must still yield an
`https` origin. Treating the two as a unit encodes `http://` into the
pairing QR behind a TLS-terminating proxy, and the phone then fails its
WebSocket upgrade.
The result never has a trailing slash, so callers can concatenate
`"\#{base}/api/v1/..."` without producing a double slash.
"""
import Plug.Conn, only: [get_req_header: 2]
@spec derive(Plug.Conn.t() | Phoenix.LiveView.Socket.t()) :: String.t()
def derive(%Plug.Conn{} = conn) do
env_override() || compose(scheme_from_conn(conn), host_from_conn(conn)) || endpoint_url()
end
@doc """
LiveView flavour. Must be called from `mount/3` — LiveView discards
connect_info afterwards — and the endpoint must expose `:uri` and
`:x_headers` in the socket's `connect_info`.
On the dead render there is no connect_info at all, so this falls
through to the endpoint URL; callers that need the real origin should
derive it once the socket is connected.
"""
def derive(%Phoenix.LiveView.Socket{} = socket) do
env_override() || compose(scheme_from_socket(socket), host_from_socket(socket)) ||
endpoint_url()
end
## Scheme and host extraction
defp scheme_from_conn(conn) do
forwarded_proto(get_req_header(conn, "x-forwarded-proto")) || to_string(conn.scheme)
end
defp host_from_conn(conn) do
case get_req_header(conn, "x-forwarded-host") do
[host | _] -> first_value(host)
_ -> host_with_port(conn.host, to_string(conn.scheme), conn.port)
end
end
defp scheme_from_socket(socket) do
forwarded_proto([header(socket, "x-forwarded-proto")]) ||
case connect_uri(socket) do
%URI{scheme: scheme} when is_binary(scheme) -> scheme
_ -> nil
end
end
defp host_from_socket(socket) do
case header(socket, "x-forwarded-host") do
nil ->
case connect_uri(socket) do
%URI{host: host, scheme: scheme, port: port} when is_binary(host) ->
host_with_port(host, scheme, port)
_ ->
nil
end
host ->
first_value(host)
end
end
defp connect_uri(socket), do: Phoenix.LiveView.get_connect_info(socket, :uri)
defp header(socket, name) do
socket
|> Phoenix.LiveView.get_connect_info(:x_headers)
|> Kernel.||([])
|> Enum.find_value(fn
{^name, value} -> value
_ -> nil
end)
end
## Helpers
defp compose(nil, _host), do: nil
defp compose(_scheme, nil), do: nil
defp compose(scheme, host), do: strip_trailing_slash("#{scheme}://#{host}")
defp forwarded_proto([proto | _]) when is_binary(proto), do: first_value(proto)
defp forwarded_proto(_), do: nil
# Chained proxies append rather than replace, so these headers can be a
# comma-separated list. The first value is the one closest to the client.
defp first_value(value), do: value |> String.split(",") |> List.first() |> String.trim()
defp host_with_port(host, _scheme, nil), do: host
defp host_with_port(host, scheme, port) do
if default_port?(scheme, port), do: host, else: "#{host}:#{port}"
end
defp default_port?("http", 80), do: true
defp default_port?("https", 443), do: true
defp default_port?(_scheme, _port), do: false
defp env_override do
case System.get_env("DIOGRAMOS_PUBLIC_URL") do
nil -> nil
"" -> nil
url -> strip_trailing_slash(url)
end
end
defp endpoint_url, do: strip_trailing_slash(DiogramosWeb.Endpoint.url())
defp strip_trailing_slash(url), do: String.replace_suffix(url, "/", "")
end
neiam
/diogramos
diogramos
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
4.2 KiB · text
History
92c48b1