lib/remote_ip/options.ex

defmodule RemoteIp.Options do
  @headers ~w[forwarded x-forwarded-for x-client-ip x-real-ip]
  @parsers %{"forwarded" => RemoteIp.Parsers.Forwarded}
  @proxies []
  @clients []

  @moduledoc """
  The keyword options given to `RemoteIp.init/1` or `RemoteIp.from/2`.

  You shouldn't need to use this module directly. Its functions are used
  internally by `RemoteIp` to process configurations and support MFA-style
  [runtime options](#module-runtime-options).

  You may pass any of the following keyword arguments into the plug (they get
  passed to `RemoteIp.init/1`). You can also pass the same keywords directly to
  `RemoteIp.from/2`.

  ## `:headers`

  The `:headers` option should be a list of strings. These are the names of
  headers that contain forwarding information. The default is

  ```elixir
  #{inspect(@headers, pretty: true)}
  ```

  Every request header whose name exactly matches one of these strings will be
  parsed for IP addresses, which are then used to determine the routing
  information and ultimately the original client IP. Note that `Plug`
  normalizes headers to lowercase, so this option should consist of lowercase
  names.

  In production, you likely want this to be a singleton - a list of only one
  string. There are a couple reasons:

  1. You usually can't rely on servers to preserve the relative ordering of
     headers in the HTTP request. For example, the
     [Cowboy](https://github.com/ninenines/cowboy/) server presently [uses
     maps](https://github.com/elixir-plug/plug_cowboy/blob/f82f2ff982f04fb4faa3a12fd2b08a7cc56ebe15/lib/plug/cowboy/conn.ex#L125-L127)
     to represent headers, which don't preserve key order. The order in which
     we process IPs matters because we take that as the routing information for
     the request. So if you have multiple competing headers, the routing might
     be ambiguous, and you could get bad results.

  2. It could also be a security issue. Say you're only expecting one header
     like `X-Forwarded-For`, but configure multiple headers like
     `["x-forwarded-for", "x-real-ip"]`. Then it'd be easy for a malicious user
     to just set an extra `X-Real-Ip` header and interfere with the IP parsing
     (again, due to the sensitive nature of header ordering).

  We still allow multiple headers because:

  1. Users can get up & running faster if the default configuration recognizes
     all of the common headers.

  2. You shouldn't be relying that heavily on IP addresses for security. Even a
     single plain-text header has enough problems on its own that we can't
     guarantee its results are accurate. For more details, see the
     documentation for [the algorithm](algorithm.md).

  3. It's more general. Networking setups are often very idiosyncratic, and we
     want to give users the option to use multiple headers if that's what they
     need.

  ## `:parsers`

  The `:parsers` option should be a map from strings to modules. Each string
  should be a header name (lowercase), and each module should implement the
  `RemoteIp.Parser` behaviour. The default is


  ```elixir
  #{inspect(@parsers, pretty: true)}
  ```

  Headers with the given name are parsed using the given module. If a header is
  not found in this map, it will be parsed by `RemoteIp.Parsers.Generic`. So
  you can use this option to:

  * add a parser for your own custom header

  * specialize on the generic parsing of headers like `"x-forwarded-for"`

  * replace any of the default parsers with one of your own

  The map you provide for this option is automatically merged into the default
  using `Map.merge/2`. That way, the stock parsers won't be overridden unless
  you explicitly provide your own replacement.

  ## `:proxies`

  The `:proxies` option should be a list of strings - either individual IPs or
  ranges in
  [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing)
  notation. The default is


  ```elixir
  #{inspect(@proxies, pretty: true)}
  ```

  For the sake of efficiency, you should prefer CIDR notation where possible.
  So instead of listing out 256 different addresses for the `1.2.3.x` block,
  you should say `"1.2.3.0/24"`.

  These proxies are skipped by [the algorithm](algorithm.md) and are never
  considered the original client IP, unless specifically overruled by the
  `:clients` option.

  In addition to the proxies listed here, note that the following [reserved IP
  addresses](https://en.wikipedia.org/wiki/Reserved_IP_addresses) are also
  skipped automatically, as they are presumed to be internal addresses that
  don't belong to the client:

  * IPv4 loopback: `127.0.0.0/8`
  * IPv6 loopback: `::1/128`
  * IPv4 private network: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`
  * IPv6 unique local address: `fc00::/7`

  ## `:clients`

  The `:clients` option should be a list of strings - either individual IPs or
  ranges in
  [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing)
  notation. The default is


  ```elixir
  #{inspect(@clients, pretty: true)}
  ```

  For the sake of efficiency, you should prefer CIDR notation where possible.
  So instead of listing out 256 different addresses for the `1.2.3.x` block,
  you should say `"1.2.3.0/24"`.

  These addresses are never considered to be proxies by [the
  algorithm](algorithm.md). For example, if you configure the `:proxies` option
  to include `"1.2.3.0/24"` and the `:clients` option to include `"1.2.3.4"`,
  then every IP in the `1.2.3.x` block would be considered a proxy *except* for
  `1.2.3.4`.

  This option can also be used on reserved IP addresses that would otherwise be
  skipped automatically. For example, if your routing works through a local
  network, you might actually consider addresses in the `10.x.x.x` block to be
  clients. You could permit the entire block with `"10.0.0.0/8"`, or even
  specific IPs in this range like `"10.1.2.3"`.

  ## Runtime options

  Every option can also accept a tuple of three elements: `{module, function,
  arguments}` (MFA). These are passed to `Kernel.apply/3` at runtime, allowing
  you to dynamically configure the plug, even though the `Plug.Builder`
  generally calls `c:Plug.init/1` at compilation time.

  The return value from an MFA should be the same as if you were passing the
  literal into that option. For instance, the `:proxies` MFA should return a
  list of IP/CIDR strings.

  The MFAs you give are re-evaluated on *each call* to `RemoteIp.call/2` or
  `RemoteIp.from/2`. So be careful not to do anything too expensive at runtime.
  For example, don't download a list of known proxies, or else it will be
  re-downloaded on every request. Consider caching the download instead,
  perhaps using a library like [`Cachex`](https://hexdocs.pm/cachex).

  ## Examples

  ### Basic usage

  Suppose you know:
  * you are behind proxies in the `1.2.x.x` block
  * the proxies use the `X-Real-Ip` header
  * but the IP `1.2.3.4` is actually a client, not one of the proxies

  Then you could say:

  ```elixir
  defmodule MyApp do
    use Plug.Router

    plug RemoteIp,
      headers: ~w[x-real-ip],
      proxies: ~w[1.2.0.0/16],
      clients: ~w[1.2.3.4]

    plug :match
    plug :dispatch

    # get "/" do ...
  end
  ```

  The same options may also be passed into `RemoteIp.from/2`:

  ```elixir
  defmodule MySocket do
    use Phoenix.Socket

    @options [
      headers: ~w[x-real-ip],
      proxies: ~w[1.2.0.0/16],
      clients: ~w[1.2.3.4]
    ]

    def connect(params, socket, connect_info) do
      ip = RemoteIp.from(connect_info[:x_headers], @options)
      # ...
    end
  end
  ```

  ### Custom parser

  Suppose your proxies are using a header with a special format. The name of
  the header is `X-Special` and the format looks like `ip=127.0.0.1`.

  First, you'd implement a custom parser:

  ```elixir
  defmodule SpecialParser do
    @behaviour RemoteIp.Parser

    @impl RemoteIp.Parser
    def parse(header) do
      ip = String.replace_prefix(header, "ip=", "")
      case :inet.parse_strict_address(ip |> to_charlist()) do
        {:ok, parsed} -> [parsed]
        _ -> []
      end
    end
  end
  ```

  Then you would configure the plug with that parser. Make sure to also specify
  the `:headers` option so that the `X-Special` header actually gets passed to
  the parser.

  ```elixir
  defmodule SpecialApp do
    use Plug.Router

    plug RemoteIp,
      headers: ~w[x-special],
      parsers: %{"x-special" => SpecialParser}

    plug :match
    plug :dispatch

    # get "/" do ...
  end
  ```

  ### Using MFAs

  Suppose you're deploying a release and you want to get the proxy IPs from an
  environment variable. Because the release is compiled ahead of time, you
  shouldn't do a `System.get_env/1` inline - it'll just be the value of the
  environment variable circa compilation time (probably empty!).

  ```elixir
  defmodule CompiledApp do
    use Plug.Router

    # DON'T DO THIS: the value of the env var gets compiled into the release
    plug RemoteIp, proxies: System.get_env("PROXIES") |> String.split(",")

    plug :match
    plug :dispatch

    # get "/" do ...
  end
  ```

  Instead, you can use an MFA to look up the variable at runtime:

  ```elixir
  defmodule RuntimeApp do
    use Plug.Router

    plug RemoteIp, proxies: {__MODULE__, :proxies, []}

    def proxies do
      System.get_env("PROXIES") |> String.split(",", trim: true)
    end

    plug :match
    plug :dispatch

    # get "/" do ...
  end
  ```
  """

  @doc """
  The default value for the given option.
  """

  def default(option)
  def default(:headers), do: @headers
  def default(:parsers), do: @parsers
  def default(:proxies), do: @proxies
  def default(:clients), do: @clients

  @doc """
  Processes keyword options, delaying the evaluation of MFAs until `unpack/1`.
  """

  def pack(options) do
    [
      headers: pack(options, :headers),
      parsers: pack(options, :parsers),
      proxies: pack(options, :proxies),
      clients: pack(options, :clients)
    ]
  end

  defp pack(options, option) do
    case Keyword.get(options, option, default(option)) do
      {m, f, a} -> {m, f, a}
      value -> evaluate(option, value)
    end
  end

  @doc """
  Evaluates options processed by `pack/1`, applying MFAs as needed.
  """

  def unpack(options) do
    [
      headers: unpack(options, :headers),
      parsers: unpack(options, :parsers),
      proxies: unpack(options, :proxies),
      clients: unpack(options, :clients)
    ]
  end

  defp unpack(options, option) do
    case Keyword.get(options, option) do
      {m, f, a} -> evaluate(option, apply(m, f, a))
      value -> value
    end
  end

  defp evaluate(:headers, headers) do
    headers
  end

  defp evaluate(:parsers, parsers) do
    Map.merge(default(:parsers), parsers)
  end

  defp evaluate(:proxies, proxies) do
    proxies |> Enum.map(&RemoteIp.Block.parse!/1)
  end

  defp evaluate(:clients, clients) do
    clients |> Enum.map(&RemoteIp.Block.parse!/1)
  end
end