defmodule GitGudWeb.Plugs.ClientIp do
@moduledoc """
Sets `conn.remote_ip` to the client's address when the request came
through a reverse proxy we trust (Traefik, in the deployment), so
rate limits and logs see the client rather than the proxy.
The address is read from `X-Forwarded-For` right to left: each hop a
trusted proxy appended is skipped, and the first address that isn't a
trusted proxy is the client. Entries left of that were written by the
client itself and are ignored, so the header can't be used to pose as
someone else. A request whose peer isn't a trusted proxy keeps its
peer address.
Trusted proxies default to loopback and the private ranges a cluster
runs on (10/8, 172.16/12, 192.168/16, fc00::/7). Override with:
config :git_gud, GitGudWeb.Plugs.ClientIp, trusted_proxies: ["10.42.0.0/16"]
(`GITGUD_TRUSTED_PROXIES`, comma-separated, in the release.)
"""
@behaviour Plug
import Bitwise
@default_trusted [
"127.0.0.0/8",
"::1/128",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7"
]
@impl true
def init(opts), do: opts
@impl true
def call(%Plug.Conn{remote_ip: peer} = conn, _opts) do
%{conn | remote_ip: client_ip(peer, Plug.Conn.get_req_header(conn, "x-forwarded-for"))}
end
@doc """
The client's address for a LiveView, from the socket's connect info
(`:peer_data` and `:x_headers`, both enabled on the `/live` socket).
Call it from `mount/3`; nil when the connect info isn't there.
"""
def from_socket(socket) do
case connect_info(socket, :peer_data) do
%{address: peer} ->
forwarded =
for {"x-forwarded-for", v} <- connect_info(socket, :x_headers) || [], do: v
client_ip(peer, forwarded)
_ ->
nil
end
end
@doc """
`Phoenix.LiveView.get_connect_info/2`, or nil where there's none to
read (a socket built by hand, as in `on_mount` unit tests) instead of
a raise.
"""
def connect_info(%Phoenix.LiveView.Socket{private: %{connect_info: info}} = socket, key)
when not is_nil(info),
do: Phoenix.LiveView.get_connect_info(socket, key)
def connect_info(_socket, _key), do: nil
@doc """
The client address given the peer and the `X-Forwarded-For` header
values, per the rules in the moduledoc.
"""
def client_ip(peer, forwarded) do
trusted = trusted()
if trusted?(peer, trusted) do
forwarded
|> Enum.flat_map(&String.split(&1, ","))
|> Enum.map(&parse/1)
|> Enum.reverse()
|> Enum.reduce_while(peer, fn
nil, _acc -> {:halt, peer}
ip, _acc -> if trusted?(ip, trusted), do: {:cont, ip}, else: {:halt, ip}
end)
else
peer
end
end
defp parse(raw) do
case raw |> String.trim() |> String.to_charlist() |> :inet.parse_strict_address() do
{:ok, ip} -> ip
_ -> nil
end
end
defp trusted do
:git_gud
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:trusted_proxies, @default_trusted)
|> Enum.flat_map(fn cidr ->
case parse_cidr(cidr) do
nil -> []
range -> [range]
end
end)
end
defp parse_cidr(cidr) do
{addr, bits} =
case String.split(String.trim(cidr), "/", parts: 2) do
[addr, bits] -> {addr, Integer.parse(bits)}
[addr] -> {addr, :all}
end
with {:ok, ip} <- :inet.parse_strict_address(String.to_charlist(addr)) do
size = tuple_size(ip) * if(tuple_size(ip) == 4, do: 8, else: 16)
case bits do
:all -> {ip, size}
{n, ""} when n in 0..size//1 -> {ip, n}
_ -> nil
end
else
_ -> nil
end
end
defp trusted?(ip, ranges), do: Enum.any?(ranges, &in_range?(ip, &1))
defp in_range?(ip, {net, bits}) when tuple_size(ip) == tuple_size(net) do
width = if tuple_size(ip) == 4, do: 8, else: 16
size = tuple_size(ip) * width
<<a::bitstring-size(^bits), _::bitstring>> = to_bits(ip, width, size)
<<b::bitstring-size(^bits), _::bitstring>> = to_bits(net, width, size)
a == b
end
defp in_range?(_ip, _range), do: false
defp to_bits(ip, width, size) do
int = ip |> Tuple.to_list() |> Enum.reduce(0, fn part, acc -> acc * (1 <<< width) + part end)
<<int::size(size)>>
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
4.2 KiB · text
History
8ccf537