aprs.me/lib/aprsme_web/plugs/remote_ip.ex

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