71 lines
1.5 KiB
Elixir
71 lines
1.5 KiB
Elixir
defmodule AprsmeWeb.Plugs.RemoteIp do
|
|
@moduledoc """
|
|
Plug that extracts the real client IP from Cloudflare or proxy headers
|
|
and sets `conn.remote_ip` so downstream logging shows the actual client address.
|
|
|
|
Header priority:
|
|
1. `CF-Connecting-IP` (Cloudflare)
|
|
2. `X-Forwarded-For` (first IP in the chain)
|
|
"""
|
|
@behaviour Plug
|
|
|
|
import Plug.Conn
|
|
|
|
@impl true
|
|
def init(opts), do: opts
|
|
|
|
@impl true
|
|
def call(conn, _opts) do
|
|
conn =
|
|
case get_client_ip(conn) do
|
|
{:ok, ip} -> %{conn | remote_ip: ip}
|
|
:error -> conn
|
|
end
|
|
|
|
user_agent =
|
|
case get_req_header(conn, "user-agent") do
|
|
[ua | _] -> ua
|
|
[] -> "-"
|
|
end
|
|
|
|
Logger.metadata(
|
|
remote_ip: conn.remote_ip |> :inet.ntoa() |> to_string(),
|
|
user_agent: user_agent
|
|
)
|
|
|
|
conn
|
|
end
|
|
|
|
defp get_client_ip(conn) do
|
|
with :error <- parse_cf_header(conn) do
|
|
parse_forwarded_for(conn)
|
|
end
|
|
end
|
|
|
|
defp parse_cf_header(conn) do
|
|
case get_req_header(conn, "cf-connecting-ip") do
|
|
[ip_str | _] -> parse_ip(ip_str)
|
|
[] -> :error
|
|
end
|
|
end
|
|
|
|
defp parse_forwarded_for(conn) do
|
|
case get_req_header(conn, "x-forwarded-for") do
|
|
[value | _] ->
|
|
value
|
|
|> String.split(",")
|
|
|> List.first()
|
|
|> parse_ip()
|
|
|
|
[] ->
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp parse_ip(ip_str) do
|
|
case ip_str |> String.trim() |> String.to_charlist() |> :inet.parse_address() do
|
|
{:ok, ip} -> {:ok, ip}
|
|
{:error, _} -> :error
|
|
end
|
|
end
|
|
end
|