mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
650 lines
20 KiB
Elixir
650 lines
20 KiB
Elixir
defmodule Web.Auth do
|
|
use Web, :verified_routes
|
|
alias Domain.{Auth, Accounts, Tokens}
|
|
|
|
# This is the cookie which will store recent account ids
|
|
# that the user has signed in to.
|
|
@recent_accounts_cookie_name "fz_recent_account_ids"
|
|
@recent_accounts_cookie_options [
|
|
sign: true,
|
|
max_age: 365 * 24 * 60 * 60,
|
|
same_site: "Lax",
|
|
secure: true,
|
|
http_only: true
|
|
]
|
|
@remember_last_account_ids 5
|
|
|
|
# Session is stored as a list in a cookie so we want to limit numbers
|
|
# of items in the list to avoid hitting cookie size limit.
|
|
#
|
|
# Max cookie size is 4kb. One session is ~460 bytes.
|
|
# We also leave space for other cookies.
|
|
@remember_last_sessions 6
|
|
|
|
# Session Management
|
|
|
|
def put_account_session(%Plug.Conn{} = conn, :client, _account_id, _encoded_fragment) do
|
|
conn
|
|
end
|
|
|
|
def put_account_session(%Plug.Conn{} = conn, :browser, account_id, encoded_fragment) do
|
|
session = {:browser, account_id, encoded_fragment}
|
|
|
|
sessions =
|
|
Plug.Conn.get_session(conn, :sessions, [])
|
|
|> Enum.reject(fn {session_context_type, session_account_id, _encoded_fragment} ->
|
|
session_context_type == :browser and session_account_id == account_id
|
|
end)
|
|
|
|
sessions = Enum.take(sessions ++ [session], -1 * @remember_last_sessions)
|
|
|
|
Plug.Conn.put_session(conn, :sessions, sessions)
|
|
end
|
|
|
|
defp delete_account_session(conn, context_type, account_id) do
|
|
sessions =
|
|
Plug.Conn.get_session(conn, :sessions, [])
|
|
|> Enum.reject(fn {session_context_type, session_account_id, _encoded_fragment} ->
|
|
session_context_type == context_type and session_account_id == account_id
|
|
end)
|
|
|
|
Plug.Conn.put_session(conn, :sessions, sessions)
|
|
end
|
|
|
|
# Signing In and Out
|
|
|
|
@doc """
|
|
Returns non-empty parameters that should be persisted during sign in flow.
|
|
"""
|
|
def take_sign_in_params(params) do
|
|
params
|
|
|> Map.take(["as", "state", "nonce", "redirect_to"])
|
|
|> Map.reject(fn {_key, value} -> value in ["", nil] end)
|
|
end
|
|
|
|
@doc """
|
|
Takes sign in parameters returned by `take_sign_in_params/1` and
|
|
returns the appropriate auth context type for them.
|
|
"""
|
|
def fetch_auth_context_type!(%{"as" => "client"}), do: :client
|
|
def fetch_auth_context_type!(_params), do: :browser
|
|
|
|
def fetch_token_nonce!(%{"nonce" => nonce}), do: nonce
|
|
def fetch_token_nonce!(_params), do: nil
|
|
|
|
@doc """
|
|
Persists the token in the session and redirects the user depending on the
|
|
auth context type.
|
|
|
|
The browser users are sent to authenticated home or a return path if it's stored in params.
|
|
|
|
The account users are only expected to authenticate using client apps and are redirected
|
|
to the deep link.
|
|
"""
|
|
|
|
def signed_in(
|
|
%Plug.Conn{} = conn,
|
|
%Auth.Provider{} = provider,
|
|
%Auth.Identity{} = identity,
|
|
context,
|
|
encoded_fragment,
|
|
redirect_params
|
|
) do
|
|
redirect_params = take_sign_in_params(redirect_params)
|
|
conn = prepend_recent_account_ids(conn, provider.account_id)
|
|
|
|
if is_nil(redirect_params["as"]) and identity.actor.type == :account_user do
|
|
conn
|
|
|> Phoenix.Controller.put_flash(
|
|
:error,
|
|
"Please use a client application to access Firezone."
|
|
)
|
|
|> Phoenix.Controller.redirect(to: ~p"/#{conn.path_params["account_id_or_slug"]}")
|
|
|> Plug.Conn.halt()
|
|
else
|
|
conn
|
|
|> put_account_session(context.type, provider.account_id, encoded_fragment)
|
|
|> signed_in_redirect(identity, context, encoded_fragment, redirect_params)
|
|
end
|
|
end
|
|
|
|
defp signed_in_redirect(conn, identity, %Auth.Context{type: :client}, encoded_fragment, %{
|
|
"as" => "client",
|
|
"nonce" => _nonce,
|
|
"state" => state
|
|
}) do
|
|
query =
|
|
%{
|
|
fragment: encoded_fragment,
|
|
state: state,
|
|
actor_name: identity.actor.name,
|
|
account_slug: conn.assigns.account.slug,
|
|
account_name: conn.assigns.account.name,
|
|
identity_provider_identifier: identity.provider_identifier
|
|
}
|
|
|> Enum.reject(&is_nil(elem(&1, 1)))
|
|
|> URI.encode_query()
|
|
|
|
client_handler =
|
|
Domain.Config.fetch_env!(:web, :client_handler)
|
|
|
|
Phoenix.Controller.redirect(conn,
|
|
external: "#{client_handler}handle_client_sign_in_callback?#{query}"
|
|
)
|
|
end
|
|
|
|
defp signed_in_redirect(
|
|
conn,
|
|
_identity,
|
|
%Auth.Context{type: :client},
|
|
_encoded_fragment,
|
|
_params
|
|
) do
|
|
conn
|
|
|> Phoenix.Controller.put_flash(:error, "Please use a client application to access Firezone.")
|
|
|> Phoenix.Controller.redirect(to: ~p"/#{conn.path_params["account_id_or_slug"]}")
|
|
|> Plug.Conn.halt()
|
|
end
|
|
|
|
defp signed_in_redirect(
|
|
conn,
|
|
_identity,
|
|
%Auth.Context{type: :browser},
|
|
_encoded_fragment,
|
|
redirect_params
|
|
) do
|
|
account = conn.assigns.account
|
|
redirect_to = signed_in_path(account, redirect_params)
|
|
Phoenix.Controller.redirect(conn, to: redirect_to)
|
|
end
|
|
|
|
defp signed_in_path(%Accounts.Account{} = account, %{"redirect_to" => redirect_to})
|
|
when is_binary(redirect_to) do
|
|
if String.starts_with?(redirect_to, "/#{account.id}") or
|
|
String.starts_with?(redirect_to, "/#{account.slug}") do
|
|
redirect_to
|
|
else
|
|
signed_in_path(account)
|
|
end
|
|
end
|
|
|
|
defp signed_in_path(%Accounts.Account{} = account, _redirect_params) do
|
|
signed_in_path(account)
|
|
end
|
|
|
|
defp signed_in_path(%Accounts.Account{} = account) do
|
|
~p"/#{account}/sites"
|
|
end
|
|
|
|
@doc """
|
|
Logs the user out.
|
|
|
|
It clears all session data for safety. See `renew_session/1`.
|
|
"""
|
|
def sign_out(%Plug.Conn{} = conn, params) do
|
|
account_or_slug = Map.get(conn.assigns, :account) || params["account_id_or_slug"]
|
|
|
|
conn
|
|
|> renew_session()
|
|
|> sign_out_redirect(account_or_slug, params)
|
|
end
|
|
|
|
defp sign_out_redirect(
|
|
%{assigns: %{subject: %Auth.Subject{} = subject}} = conn,
|
|
account_or_slug,
|
|
params
|
|
) do
|
|
post_sign_out_url = post_sign_out_url(account_or_slug, params)
|
|
{:ok, _identity, redirect_url} = Auth.sign_out(subject, post_sign_out_url)
|
|
Phoenix.Controller.redirect(conn, external: redirect_url)
|
|
end
|
|
|
|
defp sign_out_redirect(conn, account_or_slug, params) do
|
|
post_sign_out_url = post_sign_out_url(account_or_slug, params)
|
|
Phoenix.Controller.redirect(conn, external: post_sign_out_url)
|
|
end
|
|
|
|
defp post_sign_out_url(_account_or_slug, %{"as" => "client", "state" => state}) do
|
|
"firezone://handle_client_sign_out_callback?state=#{state}"
|
|
end
|
|
|
|
defp post_sign_out_url(account_or_slug, _params) do
|
|
url(~p"/#{account_or_slug}")
|
|
end
|
|
|
|
@doc """
|
|
This function renews the session ID to avoid fixation attacks
|
|
and erases the session token from the sessions list.
|
|
"""
|
|
def renew_session(%Plug.Conn{} = conn) do
|
|
preferred_locale = Plug.Conn.get_session(conn, :preferred_locale)
|
|
account_id = if Map.get(conn.assigns, :account), do: conn.assigns.account.id
|
|
|
|
sessions =
|
|
Plug.Conn.get_session(conn, :sessions, [])
|
|
|> Enum.reject(fn {_, session_account_id, _} ->
|
|
session_account_id == account_id
|
|
end)
|
|
|> Enum.take(-1 * @remember_last_sessions)
|
|
|
|
conn
|
|
|> Plug.Conn.configure_session(renew: true)
|
|
|> Plug.Conn.clear_session()
|
|
|> Plug.Conn.put_session(:preferred_locale, preferred_locale)
|
|
|> Plug.Conn.put_session(:sessions, sessions)
|
|
end
|
|
|
|
###########################
|
|
## Controller Helpers
|
|
###########################
|
|
|
|
def list_recent_account_ids(conn) do
|
|
conn = Plug.Conn.fetch_cookies(conn, signed: [@recent_accounts_cookie_name])
|
|
|
|
if recent_account_ids = Map.get(conn.cookies, @recent_accounts_cookie_name) do
|
|
{:ok, :erlang.binary_to_term(recent_account_ids, [:safe]), conn}
|
|
else
|
|
{:ok, [], conn}
|
|
end
|
|
end
|
|
|
|
defp prepend_recent_account_ids(conn, account_id) do
|
|
update_recent_account_ids(conn, fn recent_account_ids ->
|
|
[account_id] ++ recent_account_ids
|
|
end)
|
|
end
|
|
|
|
def update_recent_account_ids(conn, callback) when is_function(callback, 1) do
|
|
{:ok, recent_account_ids, conn} = list_recent_account_ids(conn)
|
|
|
|
recent_account_ids =
|
|
recent_account_ids
|
|
|> callback.()
|
|
|> Enum.take(@remember_last_account_ids)
|
|
|> :erlang.term_to_binary()
|
|
|
|
Plug.Conn.put_resp_cookie(
|
|
conn,
|
|
@recent_accounts_cookie_name,
|
|
recent_account_ids,
|
|
@recent_accounts_cookie_options
|
|
)
|
|
end
|
|
|
|
###########################
|
|
## Plugs
|
|
###########################
|
|
|
|
@doc """
|
|
Fetches the user agent value from headers and assigns it the connection.
|
|
"""
|
|
def fetch_user_agent(%Plug.Conn{} = conn, _opts) do
|
|
case Plug.Conn.get_req_header(conn, "user-agent") do
|
|
[user_agent | _] -> Plug.Conn.assign(conn, :user_agent, user_agent)
|
|
_ -> conn
|
|
end
|
|
end
|
|
|
|
def fetch_account(%Plug.Conn{path_info: [account_id_or_slug | _]} = conn, _opts) do
|
|
case Accounts.fetch_account_by_id_or_slug(account_id_or_slug) do
|
|
{:ok, account} -> Plug.Conn.assign(conn, :account, account)
|
|
_ -> conn
|
|
end
|
|
end
|
|
|
|
def fetch_account(%Plug.Conn{} = conn, _opts) do
|
|
conn
|
|
end
|
|
|
|
@doc """
|
|
Fetches the session token from the session and assigns the subject to the connection.
|
|
"""
|
|
def fetch_subject(%Plug.Conn{} = conn, _opts) do
|
|
params = take_sign_in_params(conn.params)
|
|
context_type = fetch_auth_context_type!(params)
|
|
context = get_auth_context(conn, context_type)
|
|
|
|
if account = Map.get(conn.assigns, :account) do
|
|
sessions = Plug.Conn.get_session(conn, :sessions, [])
|
|
|
|
with {:ok, encoded_fragment} <- fetch_token(sessions, account.id, context.type),
|
|
{:ok, subject} <- Auth.authenticate(encoded_fragment, context),
|
|
true <- subject.account.id == account.id do
|
|
conn
|
|
|> Plug.Conn.put_session(:live_socket_id, Tokens.socket_id(subject.token_id))
|
|
|> Plug.Conn.assign(:subject, subject)
|
|
else
|
|
{:error, :unauthorized} ->
|
|
delete_account_session(conn, context.type, account.id)
|
|
|
|
_ ->
|
|
conn
|
|
end
|
|
else
|
|
conn
|
|
end
|
|
end
|
|
|
|
defp fetch_token(sessions, account_id, context_type) do
|
|
sessions
|
|
|> Enum.find(fn {session_context_type, session_account_id, _encoded_fragment} ->
|
|
session_context_type == context_type and session_account_id == account_id
|
|
end)
|
|
|> case do
|
|
{_context_type, _account_id, encoded_fragment} -> {:ok, encoded_fragment}
|
|
_ -> :error
|
|
end
|
|
end
|
|
|
|
def get_auth_context(%Plug.Conn{} = conn, type) do
|
|
{location_region, location_city, {location_lat, location_lon}} =
|
|
get_load_balancer_ip_location(conn)
|
|
|
|
%Auth.Context{
|
|
type: type,
|
|
user_agent: Map.get(conn.assigns, :user_agent),
|
|
remote_ip: conn.remote_ip,
|
|
remote_ip_location_region: location_region,
|
|
remote_ip_location_city: location_city,
|
|
remote_ip_location_lat: location_lat,
|
|
remote_ip_location_lon: location_lon
|
|
}
|
|
end
|
|
|
|
defp get_load_balancer_ip_location(%Plug.Conn{} = conn) do
|
|
location_region =
|
|
case Plug.Conn.get_req_header(conn, "x-geo-location-region") do
|
|
["" | _] -> nil
|
|
[location_region | _] -> location_region
|
|
[] -> nil
|
|
end
|
|
|
|
location_city =
|
|
case Plug.Conn.get_req_header(conn, "x-geo-location-city") do
|
|
["" | _] -> nil
|
|
[location_city | _] -> location_city
|
|
[] -> nil
|
|
end
|
|
|
|
{location_lat, location_lon} =
|
|
case Plug.Conn.get_req_header(conn, "x-geo-location-coordinates") do
|
|
["" | _] ->
|
|
{nil, nil}
|
|
|
|
["," | _] ->
|
|
{nil, nil}
|
|
|
|
[coordinates | _] ->
|
|
[lat, lon] = String.split(coordinates, ",", parts: 2)
|
|
lat = String.to_float(lat)
|
|
lon = String.to_float(lon)
|
|
{lat, lon}
|
|
|
|
[] ->
|
|
{nil, nil}
|
|
end
|
|
|
|
{location_lat, location_lon} =
|
|
Domain.Geo.maybe_put_default_coordinates(location_region, {location_lat, location_lon})
|
|
|
|
{location_region, location_city, {location_lat, location_lon}}
|
|
end
|
|
|
|
defp get_load_balancer_ip_location(x_headers) do
|
|
location_region =
|
|
case get_socket_header(x_headers, "x-geo-location-region") do
|
|
{"x-geo-location-region", ""} -> nil
|
|
{"x-geo-location-region", location_region} -> location_region
|
|
_other -> nil
|
|
end
|
|
|
|
location_city =
|
|
case get_socket_header(x_headers, "x-geo-location-city") do
|
|
{"x-geo-location-city", ""} -> nil
|
|
{"x-geo-location-city", location_city} -> location_city
|
|
_other -> nil
|
|
end
|
|
|
|
{location_lat, location_lon} =
|
|
case get_socket_header(x_headers, "x-geo-location-coordinates") do
|
|
{"x-geo-location-coordinates", ""} ->
|
|
{nil, nil}
|
|
|
|
{"x-geo-location-coordinates", coordinates} ->
|
|
[lat, lon] = String.split(coordinates, ",", parts: 2)
|
|
lat = String.to_float(lat)
|
|
lon = String.to_float(lon)
|
|
{lat, lon}
|
|
|
|
_other ->
|
|
{nil, nil}
|
|
end
|
|
|
|
{location_lat, location_lon} =
|
|
Domain.Geo.maybe_put_default_coordinates(location_region, {location_lat, location_lon})
|
|
|
|
{location_region, location_city, {location_lat, location_lon}}
|
|
end
|
|
|
|
defp get_socket_header(x_headers, key) do
|
|
List.keyfind(x_headers, key, 0)
|
|
end
|
|
|
|
@doc """
|
|
Used for routes that require the user to not be authenticated.
|
|
"""
|
|
def redirect_if_user_is_authenticated(%Plug.Conn{} = conn, _opts) do
|
|
if subject = conn.assigns[:subject] do
|
|
redirect_params = take_sign_in_params(conn.params)
|
|
encoded_fragment = fetch_subject_token!(conn, subject)
|
|
identity = %{subject.identity | actor: subject.actor}
|
|
|
|
conn
|
|
|> signed_in_redirect(identity, subject.context, encoded_fragment, redirect_params)
|
|
|> Plug.Conn.halt()
|
|
else
|
|
conn
|
|
end
|
|
end
|
|
|
|
defp fetch_subject_token!(conn, %Auth.Subject{} = subject) do
|
|
sessions = Plug.Conn.get_session(conn, :sessions, [])
|
|
{:ok, encoded_fragment} = fetch_token(sessions, subject.account.id, subject.context.type)
|
|
encoded_fragment
|
|
end
|
|
|
|
@doc """
|
|
Used for routes that require the user to be authenticated.
|
|
|
|
This plug will only work if there is an `account_id` in the path params.
|
|
"""
|
|
def ensure_authenticated(%Plug.Conn{} = conn, _opts) do
|
|
if conn.assigns[:subject] do
|
|
conn
|
|
else
|
|
redirect_params = maybe_store_return_to(conn)
|
|
|
|
conn
|
|
|> Phoenix.Controller.put_flash(:error, "You must sign in to access this page.")
|
|
|> Phoenix.Controller.redirect(
|
|
to: ~p"/#{conn.path_params["account_id_or_slug"]}?#{redirect_params}"
|
|
)
|
|
|> Plug.Conn.halt()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Used for routes that require the user to be authenticated as a specific kind of actor.
|
|
|
|
This plug will only work if there is an `account_id` in the path params.
|
|
"""
|
|
def ensure_authenticated_actor_type(%Plug.Conn{} = conn, type) do
|
|
if not is_nil(conn.assigns[:subject]) and conn.assigns[:subject].actor.type == type do
|
|
conn
|
|
else
|
|
conn
|
|
|> Web.FallbackController.call({:error, :not_found})
|
|
|> Plug.Conn.halt()
|
|
end
|
|
end
|
|
|
|
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
|
%{"redirect_to" => Phoenix.Controller.current_path(conn)}
|
|
end
|
|
|
|
defp maybe_store_return_to(_conn) do
|
|
%{}
|
|
end
|
|
|
|
###########################
|
|
## LiveView
|
|
###########################
|
|
|
|
@doc """
|
|
Handles mounting and authenticating the actor in LiveViews.
|
|
|
|
Notice: every protected route should have `account_id` in the path params.
|
|
|
|
## `on_mount` arguments
|
|
|
|
* `:mount_subject` - assigns user_agent and subject to the socket assigns based on
|
|
session_token, or nil if there's no session_token or no matching user.
|
|
|
|
* `:ensure_authenticated` - authenticates the user from the session,
|
|
and assigns the subject to socket assigns based on session_token.
|
|
Redirects to login page if there's no logged user.
|
|
|
|
* `:redirect_if_user_is_authenticated` - authenticates the user from the session.
|
|
Redirects to signed in path if there's a logged user.
|
|
|
|
* `:mount_account` - takes `account_id` from path params and loads the given account
|
|
into the socket assigns using the `subject` mounted via `:mount_subject`. This is useful
|
|
because some actions can be performed by superadmin users on behalf of other accounts
|
|
so we can't really rely on `subject.account` in a lot of places.
|
|
|
|
## Examples
|
|
|
|
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
|
|
the subject:
|
|
|
|
defmodule Web.Page do
|
|
use Web, :live_view
|
|
|
|
on_mount {Web.UserAuth, :mount_subject}
|
|
...
|
|
end
|
|
|
|
Or use the `live_session` of your router to invoke the on_mount callback:
|
|
|
|
live_session :authenticated, on_mount: [{Web.UserAuth, :ensure_authenticated}] do
|
|
live "/:account_id/profile", ProfileLive, :index
|
|
end
|
|
"""
|
|
def on_mount(:mount_subject, params, session, socket) do
|
|
socket = mount_account(socket, params, session)
|
|
{:cont, mount_subject(socket, params, session)}
|
|
end
|
|
|
|
def on_mount(:mount_account, params, session, socket) do
|
|
{:cont, mount_account(socket, params, session)}
|
|
end
|
|
|
|
def on_mount(:ensure_authenticated, params, session, socket) do
|
|
socket = mount_account(socket, params, session)
|
|
socket = mount_subject(socket, params, session)
|
|
|
|
if socket.assigns[:subject] do
|
|
{:cont, socket}
|
|
else
|
|
socket =
|
|
socket
|
|
|> Phoenix.LiveView.put_flash(:error, "You must sign in to access this page.")
|
|
|> Phoenix.LiveView.redirect(to: ~p"/#{params["account_id_or_slug"]}")
|
|
|
|
{:halt, socket}
|
|
end
|
|
end
|
|
|
|
def on_mount(:ensure_account_admin_user_actor, params, session, socket) do
|
|
socket = mount_account(socket, params, session)
|
|
socket = mount_subject(socket, params, session)
|
|
|
|
if socket.assigns[:subject].actor.type == :account_admin_user do
|
|
{:cont, socket}
|
|
else
|
|
raise Web.LiveErrors.NotFoundError
|
|
end
|
|
end
|
|
|
|
def on_mount(:redirect_if_user_is_authenticated, params, session, socket) do
|
|
socket = mount_account(socket, params, session)
|
|
socket = mount_subject(socket, params, session)
|
|
|
|
if socket.assigns[:subject] do
|
|
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket.assigns.account))}
|
|
else
|
|
{:cont, socket}
|
|
end
|
|
end
|
|
|
|
# TODO: we need to schedule socket expiration for this subject, so that when it expires
|
|
# LiveView socket will be disconnected. Otherwise, you can keep using the system as long as
|
|
# socket is active extending the session.
|
|
defp mount_subject(socket, params, session) do
|
|
Phoenix.Component.assign_new(socket, :subject, fn ->
|
|
params = take_sign_in_params(params)
|
|
context_type = fetch_auth_context_type!(params)
|
|
user_agent = Phoenix.LiveView.get_connect_info(socket, :user_agent)
|
|
real_ip = real_ip(socket)
|
|
x_headers = Phoenix.LiveView.get_connect_info(socket, :x_headers) || []
|
|
|
|
{location_region, location_city, {location_lat, location_lon}} =
|
|
get_load_balancer_ip_location(x_headers)
|
|
|
|
context = %Auth.Context{
|
|
type: context_type,
|
|
user_agent: user_agent,
|
|
remote_ip: real_ip,
|
|
remote_ip_location_region: location_region,
|
|
remote_ip_location_city: location_city,
|
|
remote_ip_location_lat: location_lat,
|
|
remote_ip_location_lon: location_lon
|
|
}
|
|
|
|
sessions = session["sessions"] || []
|
|
|
|
with account when not is_nil(account) <- Map.get(socket.assigns, :account),
|
|
{:ok, encoded_fragment} <- fetch_token(sessions, account.id, context.type),
|
|
{:ok, subject} <- Auth.authenticate(encoded_fragment, context),
|
|
true <- subject.account.id == account.id do
|
|
subject
|
|
else
|
|
_ -> nil
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp mount_account(socket, %{"account_id_or_slug" => account_id_or_slug}, _session) do
|
|
Phoenix.Component.assign_new(socket, :account, fn ->
|
|
with {:ok, account} <-
|
|
Accounts.fetch_account_by_id_or_slug(account_id_or_slug) do
|
|
account
|
|
else
|
|
_ -> nil
|
|
end
|
|
end)
|
|
end
|
|
|
|
def real_ip(socket) do
|
|
peer_data = Phoenix.LiveView.get_connect_info(socket, :peer_data)
|
|
x_headers = Phoenix.LiveView.get_connect_info(socket, :x_headers)
|
|
|
|
real_ip =
|
|
if is_list(x_headers) and length(x_headers) > 0 do
|
|
RemoteIp.from(x_headers, Web.Endpoint.real_ip_opts())
|
|
end
|
|
|
|
real_ip || peer_data.address
|
|
end
|
|
end
|