defmodule DiffuserWeb.Plugs.PublicIp do @moduledoc "Get public IP address of request from x-forwarded-for header" def init(opts), do: opts def call(%{assigns: %{ip: _}} = conn, _opts), do: conn def call(conn, _opts) do process(conn, Plug.Conn.get_req_header(conn, "x-forwarded-for")) end def process(%{remote_ip: remote_ip} = conn, []) do Plug.Conn.assign(conn, :ip, to_string(:inet_parse.ntoa(remote_ip))) end def process(conn, vals) do # Rewrite standard remote_ip field with value from header ip_address = get_ip_address(conn, vals) # See https://hexdocs.pm/plug/Plug.Conn.html conn = %{conn | remote_ip: ip_address} Plug.Conn.assign(conn, :ip, to_string(:inet_parse.ntoa(ip_address))) end defp get_ip_address(conn, vals) defp get_ip_address(%{remote_ip: remote_ip}, []), do: remote_ip defp get_ip_address(%{remote_ip: remote_ip} = _conn, [val | _]) do # Split into multiple values comps = val |> String.split(~r{\s*,\s*}, trim: true) # Get rid of "unknown" values |> Enum.filter(&(&1 != "unknown")) # Split IP from port, if any |> Enum.map(&hd(String.split(&1, ":"))) # Filter out blanks |> Enum.filter(&(&1 != "")) # Parse address into :inet.ip_address tuple |> Enum.map(&parse_address(&1)) # Elminate internal IP addreses, e.g. 192.168.1.1 |> Enum.filter(&is_public_ip(&1)) case comps do [] -> remote_ip [comp | _] -> comp end end defp parse_address(ip) do case :inet.parse_ipv4strict_address(to_charlist(ip)) do {:ok, ip_address} -> ip_address {:error, :einval} -> :einval end end defp is_public_ip(ip_address) do case ip_address do {10, _, _, _} -> false {192, 168, _, _} -> false {172, second, _, _} when second >= 16 and second <= 31 -> false {127, 0, 0, _} -> false {_, _, _, _} -> true :einval -> false end end end