From 06ca198de16280ab5ba13b2a2feb454b8dfa9fd2 Mon Sep 17 00:00:00 2001 From: Alex Vondrak Date: Mon, 14 Oct 2019 14:14:49 -0700 Subject: [PATCH] Teach RemoteIp the :clients option Removing the automatic addition of reserved addresses to the proxies would introduce breaking changes (e.g., when users start getting back 127.0.0.1 because it's not being filtered as a proxy anymore). Passing in reserved IPs as an option also seems less than ideal to me, since you'd have to take care to add everything *except* the particular clients you're expecting. This wasn't really the use case described by issues like #8. What's more, the name `:clients` has the same number of characters as `:headers` and `:proxies`, so everything lines up all pretty. :sparkles: The bulk of this commit isn't so much in the logic or even the documentation. Mostly, I just found the tests inscrutable. So I tried to refactor them so that they'd be moderately less painful while also adding tests for `:proxies` vs `:clients`. Essentially, instead of cramming a million assertions into each test, I separated the tests out from each other, grouped by forwarding header (or other general use cases, as in the two-hop tests). Then I can just make basically one assertion per test. Plus I fit everything back into tidy 80-character lines. :rainbow: This closes #8, closes #10, and closes #11. --- README.md | 65 +++-- lib/remote_ip.ex | 86 +++++-- test/remote_ip_test.exs | 550 +++++++++++++++++++++++++--------------- 3 files changed, 458 insertions(+), 243 deletions(-) diff --git a/README.md b/README.md index 5fd3fa1..130815b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A [plug](https://github.com/elixir-lang/plug) to overwrite the [`Conn`'s](https://hexdocs.pm/plug/Plug.Conn.html) `remote_ip` based on headers such as `X-Forwarded-For`. -IPs are processed last-to-first to prevent IP spoofing, as thoroughly explained in [a blog post](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) by [@gingerlime](https://github.com/gingerlime). Loopback/private IPs are always ignored and known proxies are configurable, so neither type will be erroneously treated as the original client IP. You can configure any number of arbitrary forwarding headers to use. If there's a special way to parse your particular header, the architecture of this project should [make it easy](#contributing) to open a pull request so `RemoteIp` can accommodate. +IPs are processed last-to-first to prevent IP spoofing, as thoroughly explained in [a blog post](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) by [@gingerlime](https://github.com/gingerlime). Loopback/private IPs are ignored by default, but known proxies & clients are configurable, so you have full control over which IPs are considered legitimate. You can configure any number of arbitrary forwarding headers to use. If there's a special way to parse your particular header, the architecture of this project should [make it easy](#contributing) to open a pull request so `RemoteIp` can accommodate. **If your app is not behind at least one proxy, you should not use this plug.** See [below](#algorithm) for more detailed reasoning. @@ -30,23 +30,46 @@ end Keep in mind the order of plugs in your pipeline and place `RemoteIp` as early as possible. For example, if you were to add `RemoteIp` *after* [the Plug Router](https://github.com/elixir-lang/plug#the-plug-router), your route action's logic would be executed *before* the `remote_ip` actually gets modified - not very useful! -There are 2 options that can be passed in: +There are 3 options that can be passed in: * `:headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`. * `:proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`. -For example, if you know you are behind proxies in the IP block 1.2.x.x that use the `X-Foo`, `X-Bar`, and `X-Baz` headers, you could say + [Loopback](https://en.wikipedia.org/wiki/Loopback) and [private](https://en.wikipedia.org/wiki/Private_network) IPs are always appended to this list: + + * 127.0.0.0/8 + * ::1/128 + * fc00::/7 + * 10.0.0.0/8 + * 172.16.0.0/12 + * 192.168.0.0/16 + + Since these IPs are internal, they often are not the actual client address in production, so we add them by default. To override this behavior, whitelist known client IPs using the `:clients` option. + +* `:clients` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known clients. Defaults to `[]`. + + An IP in any of the ranges listed here will never be considered a proxy. This takes precedence over the `:proxies` option, including loopback/private addresses. Any IP that is **not** covered by `:clients` or `:proxies` is assumed to be a client IP. + +For example, suppose you know: +* you are behind proxies in the 1.2.x.x block +* the proxies use the `X-Foo`, `X-Bar`, and `X-Baz` headers +* 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.Builder - plug RemoteIp, headers: ~w[x-foo x-bar x-baz], proxies: ~w[1.2.0.0/16] + plug RemoteIp, + headers: ~w[x-foo x-bar x-baz], + proxies: ~w[1.2.0.0/16], + clients: ~w[1.2.3.4/32] end ``` -Note that, due to limitations in the [inet_cidr](https://github.com/Cobenian/inet_cidr) library used to parse them, `:proxies` **must** be written in full CIDR notation, even if specifying just a single IP. So instead of `"127.0.0.1"` and `"a:b::c:d"`, you would use `"127.0.0.1/32"` and `"a:b::c:d/128"`. +Note that, due to limitations in the [inet_cidr](https://github.com/Cobenian/inet_cidr) library used to parse them, `:proxies` and `:clients` **must** be written in full CIDR notation, even if specifying just a single IP. So instead of `"127.0.0.1"` and `"a:b::c:d"`, you would use `"127.0.0.1/32"` and `"a:b::c:d/128"`. ## Background @@ -80,7 +103,7 @@ Note that the field is _meant_ to be overwritten. Plug does not actually do any None of the available solutions I have seen are ideal. In this sort of plug, you want: * **Configurable Headers:** With so many different headers being used, you should be able to configure the ones you need with minimal work. -* **Configurable Proxies:** With multiple proxy hops, there may be several IPs in the forwarding headers. Without being able to tell the plug which of those IPs are actually known to be proxies, you may get one of them back as the `remote_ip`. +* **Configurable Proxies and Clients:** With multiple proxy hops, there may be several IPs in the forwarding headers. Without being able to tell the plug which of those IPs are actually known to be proxies, you may get one of them back as the `remote_ip`. * **Correctness:** Parsing forwarding headers can be surprisingly subtle. Most available libraries get it wrong. The table below summarizes the problems with existing packages. @@ -158,7 +181,9 @@ Not only are known proxies' headers trusted, but also requests forwarded for [lo * 172.16.0.0/12 * 192.168.0.0/16 -These IPs are filtered because they are used internally and are thus guaranteed not to be the actual client address in production. +These IPs are filtered because they are used internally and are thus generally not the actual client address in production. + +However, if (say) your app is only deployed in a [VPN](https://en.wikipedia.org/wiki/Virtual_private_network)/[LAN](https://en.wikipedia.org/wiki/Local_area_network), then your clients might actually have these internal IPs. To prevent loopback/private addresses from being considered proxies, configure them as known clients using the [`:clients` option](#usage). ### :warning: Caveats :warning: @@ -166,24 +191,24 @@ These IPs are filtered because they are used internally and are thus guaranteed 2. The relative order of IPs can still be messed up by proxies amending prior headers. For instance, -* Request starts from IP 1.1.1.1 (no forwarding headers) -* Proxy 1 with IP 2.2.2.2 adds `Forwarded: for=1.1.1.1` -* Proxy 2 with IP 3.3.3.3 adds `X-Forwarded-For: 2.2.2.2` -* Proxy 3 with IP 4.4.4.4 adds to `Forwarded` so it says `Forwarded: for=1.1.1.1, for=3.3.3.3` + * Request starts from IP 1.1.1.1 (no forwarding headers) + * Proxy 1 with IP 2.2.2.2 adds `Forwarded: for=1.1.1.1` + * Proxy 2 with IP 3.3.3.3 adds `X-Forwarded-For: 2.2.2.2` + * Proxy 3 with IP 4.4.4.4 adds to `Forwarded` so it says `Forwarded: for=1.1.1.1, for=3.3.3.3` -Thus, `RemoteIp` processes the request from 4.4.4.4 with the first-to-last list of forwarded IPs + Thus, `RemoteIp` processes the request from 4.4.4.4 with the first-to-last list of forwarded IPs -```elixir -[{1, 1, 1, 1}, {3, 3, 3, 3}, {2, 2, 2, 2}] # what we get -``` + ```elixir + [{1, 1, 1, 1}, {3, 3, 3, 3}, {2, 2, 2, 2}] # what we get + ``` -even though the _actual_ order was + even though the _actual_ order was -```elixir -[{1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}] # actual forwarding order -``` + ```elixir + [{1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}] # actual forwarding order + ``` -The solution to this problem is to add both 2.2.2.2 and 3.3.3.3 as known proxies. Then either way the original client address will be reported as 1.1.1.1. As always, be sure to test in your particular environment. + The solution to this problem is to add both 2.2.2.2 and 3.3.3.3 as known proxies. Then either way the original client address will be reported as 1.1.1.1. As always, be sure to test in your particular environment. ## Contributing diff --git a/lib/remote_ip.ex b/lib/remote_ip.ex index 5021627..31838ff 100644 --- a/lib/remote_ip.ex +++ b/lib/remote_ip.ex @@ -1,7 +1,6 @@ defmodule RemoteIp do @moduledoc """ - A plug to overwrite the `Plug.Conn`'s `remote_ip` based on headers such as - `X-Forwarded-For`. + A plug to overwrite the `Plug.Conn`'s `remote_ip` based on request headers. To use, add the `RemoteIp` plug to your app's plug pipeline: @@ -13,7 +12,13 @@ defmodule RemoteIp do end ``` - There are 2 options that can be passed in: + Keep in mind the order of plugs in your pipeline and place `RemoteIp` as + early as possible. For example, if you were to add `RemoteIp` *after* [the + Plug Router](https://github.com/elixir-lang/plug#the-plug-router), your route + action's logic would be executed *before* the `remote_ip` actually gets + modified - not very useful! + + There are 3 options that can be passed in: * `:headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded @@ -23,22 +28,57 @@ defmodule RemoteIp do [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`. - For example, if you know you are behind proxies in the IP block 1.2.x.x that - use the `X-Foo`, `X-Bar`, and `X-Baz` headers, you could say + [Loopback](https://en.wikipedia.org/wiki/Loopback) and + [private](https://en.wikipedia.org/wiki/Private_network) IPs are always + appended to this list: + + * 127.0.0.0/8 + * ::1/128 + * fc00::/7 + * 10.0.0.0/8 + * 172.16.0.0/12 + * 192.168.0.0/16 + + Since these IPs are internal, they often are not the actual client + address in production, so we add them by default. To override this + behavior, whitelist known client IPs using the `:clients` option. + + * `:clients` - A list of strings in + [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of + known clients. Defaults to `[]`. + + An IP in any of the ranges listed here will never be considered a proxy. + This takes precedence over the `:proxies` option, including + loopback/private addresses. Any IP that is **not** covered by `:clients` + or `:proxies` is assumed to be a client IP. + + For example, suppose you know: + * you are behind proxies in the 1.2.x.x block + * the proxies use the `X-Foo`, `X-Bar`, and `X-Baz` headers + * 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.Builder - plug RemoteIp, headers: ~w[x-foo x-bar x-baz], proxies: ~w[1.2.0.0/16] + plug RemoteIp, + headers: ~w[x-foo x-bar x-baz], + proxies: ~w[1.2.0.0/16], + clients: ~w[1.2.3.4/32] end ``` Note that, due to limitations in the [inet_cidr](https://github.com/Cobenian/inet_cidr) library used to parse - them, `:proxies` **must** be written in full CIDR notation, even if - specifying just a single IP. So instead of `"127.0.0.1"` and `"a:b::c:d"`, - you would use `"127.0.0.1/32"` and `"a:b::c:d/128"`. + them, `:proxies` and `:clients` **must** be written in full CIDR notation, + even if specifying just a single IP. So instead of `"127.0.0.1"` and + `"a:b::c:d"`, you would use `"127.0.0.1/32"` and `"a:b::c:d/128"`. + + For more details, refer to the + [README](https://github.com/ajvondrak/remote_ip/blob/master/README.md) on + GitHub. """ @behaviour Plug @@ -52,6 +92,8 @@ defmodule RemoteIp do @proxies [] + @clients [] + # https://en.wikipedia.org/wiki/Loopback # https://en.wikipedia.org/wiki/Private_network @reserved ~w[ @@ -66,37 +108,45 @@ defmodule RemoteIp do def init(opts \\ []) do headers = Keyword.get(opts, :headers, @headers) headers = MapSet.new(headers) + proxies = Keyword.get(opts, :proxies, @proxies) ++ @reserved proxies = proxies |> Enum.map(&InetCidr.parse/1) - {headers, proxies} + clients = Keyword.get(opts, :clients, @clients) + clients = clients |> Enum.map(&InetCidr.parse/1) + + {headers, proxies, clients} end - def call(conn, {headers, proxies}) do - case last_forwarded_ip(conn, headers, proxies) do + def call(conn, {headers, proxies, clients}) do + case last_forwarded_ip(conn, headers, proxies, clients) do nil -> conn ip -> %{conn | remote_ip: ip} end end - defp last_forwarded_ip(conn, headers, proxies) do + defp last_forwarded_ip(conn, headers, proxies, clients) do conn |> ips_from(headers) - |> last_ip_forwarded_through(proxies) + |> last_ip_forwarded_through(proxies, clients) end defp ips_from(%Plug.Conn{req_headers: headers}, allowed) do RemoteIp.Headers.parse(headers, allowed) end - defp last_ip_forwarded_through(ips, proxies) do + defp last_ip_forwarded_through(ips, proxies, clients) do ips |> Enum.reverse - |> Enum.find(&forwarded?(&1, proxies)) + |> Enum.find(&forwarded?(&1, proxies, clients)) + end + + defp forwarded?(ip, proxies, clients) do + client?(ip, clients) || !proxy?(ip, proxies) end - defp forwarded?(ip, proxies) do - !proxy?(ip, proxies) + defp client?(ip, clients) do + Enum.any?(clients, fn client -> InetCidr.contains?(client, ip) end) end defp proxy?(ip, proxies) do diff --git a/test/remote_ip_test.exs b/test/remote_ip_test.exs index 961dd15..076fa33 100644 --- a/test/remote_ip_test.exs +++ b/test/remote_ip_test.exs @@ -2,302 +2,442 @@ defmodule RemoteIpTest do use ExUnit.Case, async: true use Plug.Test - # put_req_header/3 will obliterate existing values, whereas we want to - # append multiple values for the same header. - # - def add_req_header(%Plug.Conn{req_headers: headers} = conn, header, value) do - %{conn | req_headers: headers ++ [{header, value}]} + def remote_ip(conn, opts \\ []) do + RemoteIp.call(conn, RemoteIp.init(opts)).remote_ip end - def forwarded(conn, value), do: add_req_header(conn, "forwarded", value) - def x_forwarded_for(conn, ip), do: add_req_header(conn, "x-forwarded-for", ip) - def x_client_ip(conn, ip), do: add_req_header(conn, "x-client-ip", ip) - def x_real_ip(conn, ip), do: add_req_header(conn, "x-real-ip", ip) - def custom(conn, ip), do: add_req_header(conn, "custom", ip) + test "no forwarding headers" do + assert nil == remote_ip(%Plug.Conn{}) + end - def remote_ip(conn, opts \\ []) do - RemoteIp.call(conn, RemoteIp.init(opts)).remote_ip + describe "Forwarded" do + test "from an unknown IP" do + head = [{"forwarded", "for=unknown"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a loopback IP" do + head = [{"forwarded", "for=127.0.0.1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a private IP" do + head = [{"forwarded", "for=10.0.0.1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a public IP" do + head = [{"forwarded", "for=1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + assert {1, 2, 3, 4} == remote_ip(conn) + end + + test "from a known proxy" do + head = [{"forwarded", "for=1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[1.2.0.0/16]] + assert nil == remote_ip(conn, opts) + end + + test "from a known client" do + head = [{"forwarded", "for=1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[1.2.0.0/16], clients: ~w[1.2.3.4/32]] + assert {1, 2, 3, 4} == remote_ip(conn, opts) + end end - # Not a real IP address, but RemoteIp shouldn't ever be actually manipulating - # this value. So, in this Conn, we use :peer as a canary in the coalmine. - # - @conn %Plug.Conn{remote_ip: :peer} - - test "zero hops (i.e., no forwarding headers)" do - assert :peer == @conn |> remote_ip - assert :peer == @conn |> remote_ip(headers: ~w[]) - assert :peer == @conn |> remote_ip(headers: ~w[custom]) - assert :peer == @conn |> remote_ip(proxies: ~w[]) - assert :peer == @conn |> remote_ip(proxies: ~w[0.0.0.0/0 ::/0]) - assert :peer == @conn |> remote_ip(headers: ~w[], proxies: ~w[]) - assert :peer == @conn |> remote_ip(headers: ~w[], proxies: ~w[0.0.0.0/0 ::/0]) - assert :peer == @conn |> remote_ip(headers: ~w[custom], proxies: ~w[]) - assert :peer == @conn |> remote_ip(headers: ~w[custom], proxies: ~w[0.0.0.0/0 ::/0]) + describe "X-Forwarded-For" do + test "from an unknown IP" do + head = [{"x-forwarded-for", "not_an_ip"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a loopback IP" do + head = [{"x-forwarded-for", "::1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a private IP" do + head = [{"x-forwarded-for", "172.16.0.1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a public IP" do + head = [{"x-forwarded-for", "4.5.6.7"}] + conn = %Plug.Conn{req_headers: head} + assert {4, 5, 6, 7} == remote_ip(conn) + end + + test "from a known proxy" do + head = [{"x-forwarded-for", "::a"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[::a/128]] + assert nil == remote_ip(conn, opts) + end + + test "from a known client" do + head = [{"x-forwarded-for", "::1:2:3:4"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[::/64], clients: ~w[::1:2:0:0/96]] + assert {0, 0, 0, 0, 1, 2, 3, 4} == remote_ip(conn, opts) + end end - describe "one hop" do + describe "X-Client-IP" do test "from an unknown IP" do - assert :peer == @conn |> forwarded("for=unknown") |> remote_ip - assert :peer == @conn |> x_forwarded_for("not_an_ip") |> remote_ip - assert :peer == @conn |> custom("_obf") |> remote_ip(headers: ~w[custom]) + head = [{"x-client-ip", "_obf"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from a loopback IP" do - assert :peer == @conn |> forwarded("for=127.0.0.1") |> remote_ip - assert :peer == @conn |> x_client_ip("::1") |> remote_ip - assert :peer == @conn |> custom("127.0.0.2") |> remote_ip(headers: ~w[custom]) + head = [{"x-client-ip", "127.0.0.2"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from a private IP" do - assert :peer == @conn |> forwarded("for=10.0.0.1") |> remote_ip - assert :peer == @conn |> x_real_ip("172.16.0.1") |> remote_ip - assert :peer == @conn |> x_forwarded_for("fd00::") |> remote_ip - assert :peer == @conn |> custom("192.168.0.1") |> remote_ip(headers: ~w[custom]) + head = [{"x-client-ip", "fd00::"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a public IP" do + head = [{"x-client-ip", "1:2:3:4:5:6:7:8"}] + conn = %Plug.Conn{req_headers: head} + assert {1, 2, 3, 4, 5, 6, 7, 8} == remote_ip(conn) end - test "from a public IP configured as a known proxy" do - assert :peer == @conn |> forwarded("for=1.2.3.4") |> remote_ip(proxies: ~w[1.2.3.4/32]) - assert :peer == @conn |> x_client_ip("::a") |> remote_ip(proxies: ~w[::a/128]) - assert :peer == @conn |> custom("1.2.3.4") |> remote_ip(headers: ~w[custom], proxies: ~w[1.2.0.0/16]) + test "from a known proxy" do + head = [{"x-client-ip", "1:2:3:4:5:6:7:8"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[::/0]] + assert nil == remote_ip(conn, opts) end - test "from a public IP not configured as a known proxy" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=1.2.3.4") |> remote_ip(proxies: ~w[::/0]) - assert {1, 2, 3, 4, 5, 6, 7, 8} == @conn |> x_real_ip("1:2:3:4:5:6:7:8") |> remote_ip(proxies: ~w[1:1::/64]) - assert {1, 2, 3, 4} == @conn |> custom("1.2.3.4") |> remote_ip(headers: ~w[custom]) + test "from a known client" do + head = [{"x-client-ip", "127.0.0.1"}] + conn = %Plug.Conn{req_headers: head} + opts = [clients: ~w[127.0.0.0/8]] + assert {127, 0, 0, 1} == remote_ip(conn, opts) end end - describe "two hops" do + describe "X-Real-IP" do + test "from an unknown IP" do + head = [{"x-real-ip", "1.2.3"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a loopback IP" do + head = [{"x-real-ip", "::::::1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a private IP" do + head = [{"x-real-ip", "192.168.10.10"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) + end + + test "from a public IP" do + head = [{"x-real-ip", "8.9.10.11"}] + conn = %Plug.Conn{req_headers: head} + assert {8, 9, 10, 11} == remote_ip(conn) + end + + test "from a known proxy" do + head = [{"x-real-ip", "4.4.4.4"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[0.0.0.0/0]] + assert nil == remote_ip(conn, opts) + end + + test "from a known client" do + head = [{"x-real-ip", "10.45.90.135"}] + conn = %Plug.Conn{req_headers: head} + opts = [clients: ~w[10.45.0.0/16]] + assert {10, 45, 90, 135} == remote_ip(conn, opts) + end + end + + describe "custom forwarding header" do + test "from an unknown IP" do + head = [{"custom", "::g"}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom]] + assert nil == remote_ip(conn, opts) + end + + test "from a loopback IP" do + head = [{"custom", "127.127.127.127"}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom]] + assert nil == remote_ip(conn, opts) + end + + test "from a private IP" do + head = [{"custom", "172.31.41.59"}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom]] + assert nil == remote_ip(conn, opts) + end + + test "from a public IP" do + head = [{"custom", "86.75.30.9"}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom]] + assert {86, 75, 30, 9} == remote_ip(conn, opts) + end + + test "from a known proxy" do + head = [{"custom", "8.8.8.8"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[8.8.8.8/32], headers: ~w[custom]] + assert nil == remote_ip(conn, opts) + end + + test "from a known client" do + head = [{"custom", "192.168.0.1"}] + conn = %Plug.Conn{req_headers: head} + opts = [clients: ~w[192.168.0.0/24], headers: ~w[custom]] + assert {192, 168, 0, 1} == remote_ip(conn, opts) + end + end + + describe "multiple headers" do test "from unknown to unknown" do - assert :peer == @conn |> forwarded("for=unknown,for=_obf") |> remote_ip - assert :peer == @conn |> x_forwarded_for("_obf,not_an_ip") |> remote_ip - assert :peer == @conn |> custom("unknown,unknown") |> remote_ip(headers: ~w[custom]) + head = [{"forwarded", "for=unknown,for=_obf"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from unknown to loopback" do - assert :peer == @conn |> forwarded("for=_obf,for=127.0.0.1") |> remote_ip - assert :peer == @conn |> x_client_ip("unknown,::1") |> remote_ip - assert :peer == @conn |> custom("not_an_ip, 127.0.0.2") |> remote_ip(headers: ~w[custom]) + head = [{"x-forwarded-for", "unknown,::1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from unknown to private" do - assert :peer == @conn |> forwarded("for=unknown,for=10.10.10.10") |> remote_ip - assert :peer == @conn |> x_real_ip("_obf, fc00::ABCD") |> remote_ip - assert :peer == @conn |> x_forwarded_for("not_an_ip,192.168.0.4") |> remote_ip - assert :peer == @conn |> custom("unknown,172.16.72.1") |> remote_ip(headers: ~w[custom]) + head = [{"x-client-ip", "_obf, fc00:ABCD"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from unknown to proxy" do - assert :peer == @conn |> forwarded("for=_obf,for=1.2.3.4") |> remote_ip(proxies: ~w[1.2.3.4/32]) - assert :peer == @conn |> x_client_ip("unknown,a:b:c:d:e:f::") |> remote_ip(proxies: ~w[::/0]) - assert :peer == @conn |> custom("not_an_ip,1.2.3.4") |> remote_ip(headers: ~w[custom], proxies: ~w[1.0.0.0/8]) + head = [{"x-real-ip", "not_an_ip , 1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[1.0.0.0/12]] + assert nil == remote_ip(conn, opts) end - test "from unknown to non-proxy" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=unknown,for=1.2.3.4") |> remote_ip(proxies: ~w[1.2.3.5/32]) - assert {0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0, 0x0} == @conn |> x_real_ip("_obf,a:b:c:d:e:f::") |> remote_ip - assert {1, 2, 3, 4} == @conn |> custom("not_an_ip,1.2.3.4") |> remote_ip(headers: ~w[custom], proxies: ~w[8.6.7.5/32 3:0:9::/64]) + test "from unknown to client" do + head = [{"custom", "unknown ,1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom]] + assert {1, 2, 3, 4} == remote_ip(conn, opts) end test "from loopback to unknown" do - assert :peer == @conn |> forwarded("for=\"[::1]\",for=unknown") |> remote_ip - assert :peer == @conn |> x_forwarded_for("127.0.0.1,not_an_ip") |> remote_ip - assert :peer == @conn |> custom("127.0.0.2,_obfuscated_ipaddr") |> remote_ip(headers: ~w[custom]) + head = [{"forwarded", "for=\"[::1]\""}, {"x-forwarded-for", "_bogus"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from loopback to loopback" do - assert :peer == @conn |> forwarded("for=127.0.0.1, for=127.0.0.1") |> remote_ip - assert :peer == @conn |> x_client_ip("::1, ::1") |> remote_ip - assert :peer == @conn |> custom("::1, 127.0.0.1") |> remote_ip(headers: ~w[custom]) + head = [{"x-client-ip", "127.0.0.1"}, {"x-real-ip", "127.0.0.1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from loopback to private" do - assert :peer == @conn |> forwarded("for=127.0.0.10, for=\"[fc00::1]\"") |> remote_ip - assert :peer == @conn |> x_real_ip("::1, 192.168.1.2") |> remote_ip - assert :peer == @conn |> custom("127.0.0.1, 172.16.0.1") |> remote_ip(headers: ~w[custom]) - assert :peer == @conn |> custom("127.1.2.3, 10.10.10.1") |> remote_ip(headers: ~w[custom]) + head = [{"custom", "127.0.0.10"}, {"forwarded", "for=\"[fc00::1]\""}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[forwarded custom]] + assert nil == remote_ip(conn, opts) end test "from loopback to proxy" do - assert :peer == @conn |> forwarded("for=127.0.0.1 , for=1.2.3.4") |> remote_ip(proxies: ~w[1.2.3.4/32]) - assert :peer == @conn |> x_forwarded_for("::1, 1.2.3.4") |> remote_ip(proxies: ~w[1.2.3.0/24]) - assert :peer == @conn |> custom("127.0.0.2, 2001:0db8:85a3:0000:0000:8A2E:0370:7334") |> remote_ip(headers: ~w[custom], proxies: ~w[2001:0db8:85a3::8A2E:0370:7334/128]) + head = [{"forwarded", "for=127.0.0.1"}, {"forwarded", "for=1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[1.2.3.4/32]] + assert nil == remote_ip(conn, opts) end - test "from loopback to non-proxy" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=127.0.0.1, for=1.2.3.4") |> remote_ip - assert {1, 2, 3, 4} == @conn |> x_client_ip("::1, 1.2.3.4") |> remote_ip(proxies: ~w[2.0.0.0/8]) - assert {0x2001, 0x0db8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7334} == @conn |> custom("::1, 2001:0db8:85a3:0000:0000:8A2E:0370:7334") |> remote_ip(headers: ~w[custom], proxies: ~w[fe80:0000:0000:0000:0202:b3ff:fe1e:8329/128]) + test "from loopback to client" do + head = [{"x-forwarded-for", "127.0.0.1"}, {"x-forwarded-for", "1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + assert {1, 2, 3, 4} == remote_ip(conn) end test "from private to unknown" do - assert :peer == @conn |> forwarded("for=10.10.10.10,for=unknown") |> remote_ip - assert :peer == @conn |> x_forwarded_for("fc00::ABCD, _obf") |> remote_ip - assert :peer == @conn |> x_real_ip("192.168.0.4, not_an_ip") |> remote_ip - assert :peer == @conn |> custom("172.16.72.1, unknown") |> remote_ip(headers: ~w[custom]) + head = [{"x-client-ip", "fc00::ABCD"}, {"x-client-ip", "_obf"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from private to loopback" do - assert :peer == @conn |> forwarded("for=\"[fc00::1]\", for=127.0.0.10") |> remote_ip - assert :peer == @conn |> forwarded("for=10.10.10.1, for=127.1.2.3") |> remote_ip - assert :peer == @conn |> x_client_ip("192.168.1.2, ::1") |> remote_ip - assert :peer == @conn |> custom("172.16.0.1, 127.0.0.1") |> remote_ip(headers: ~w[custom]) + head = [{"x-real-ip", "192.168.1.2"}, {"x-real-ip", "::1"}] + conn = %Plug.Conn{req_headers: head} + assert nil == remote_ip(conn) end test "from private to private" do - assert :peer == @conn |> forwarded("for=172.16.0.1, for=\"[fc00::1]\"") |> remote_ip - assert :peer == @conn |> x_real_ip("192.168.0.1, 192.168.0.2") |> remote_ip - assert :peer == @conn |> custom("10.0.0.1, 10.0.0.2") |> remote_ip(headers: ~w[custom]) + head = [{"custom", "10.0.0.1"}, {"custom", "10.0.0.2"}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom]] + assert nil == remote_ip(conn, opts) end test "from private to proxy" do - assert :peer == @conn |> forwarded("for=\"[fc00::1:2:3]\", for=1.2.3.4") |> remote_ip(proxies: ~w[0.0.0.0/0]) - assert :peer == @conn |> forwarded("for=10.0.10.0, for=\"[::1.2.3.4]\"") |> remote_ip(proxies: ~w[::/64]) - assert :peer == @conn |> x_forwarded_for("192.168.0.1,1.2.3.4") |> remote_ip(proxies: ~w[1.2.0.0/16]) - assert :peer == @conn |> custom("172.16.1.2, 3.4.5.6") |> remote_ip(headers: ~w[custom], proxies: ~w[3.0.0.0/8]) + head = [{"forwarded", "for=10.0.10.0, for=\"[::1.2.3.4]\""}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[::/64]] + assert nil == remote_ip(conn, opts) end - test "from private to non-proxy" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=\"[fc00::1:2:3]\", for=1.2.3.4") |> remote_ip - assert {0, 0, 0, 0, 0, 0, 258, 772} == @conn |> forwarded("for=10.0.10.0, for=\"[::1.2.3.4]\"") |> remote_ip(proxies: ~w[255.0.0.0/8]) - assert {1, 2, 3, 4} == @conn |> x_client_ip("192.168.0.1,1.2.3.4") |> remote_ip - assert {3, 4, 5, 6} == @conn |> custom("172.16.1.2 , 3.4.5.6") |> remote_ip(headers: ~w[custom], proxies: ~w[1.2.3.4/32]) + test "from private to client" do + head = [{"x-forwarded-for", "10.0.10.0, ::1.2.3.4"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[255.0.0.0/8]] + assert {0, 0, 0, 0, 0, 0, 258, 772} == remote_ip(conn, opts) end test "from proxy to unknown" do - assert :peer == @conn |> forwarded("for=1.2.3.4,for=_obf") |> remote_ip(proxies: ~w[1.2.3.4/32]) - assert :peer == @conn |> x_real_ip("a:b:c:d:e:f::,unknown") |> remote_ip(proxies: ~w[::/0]) - assert :peer == @conn |> custom("1.2.3.4,not_an_ip") |> remote_ip(headers: ~w[custom], proxies: ~w[1.0.0.0/8]) + head = [{"x-client-ip", "a:b:c:d:e:f::,unknown"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[::/0]] + assert nil == remote_ip(conn, opts) end test "from proxy to loopback" do - assert :peer == @conn |> forwarded("for=1.2.3.4, for=127.0.0.1") |> remote_ip(proxies: ~w[1.2.3.4/32]) - assert :peer == @conn |> x_forwarded_for("1.2.3.4, ::1") |> remote_ip(proxies: ~w[1.2.3.0/24]) - assert :peer == @conn |> custom("2001:0db8:85a3:0000:0000:8A2E:0370:7334, 127.0.0.2") |> remote_ip(headers: ~w[custom], proxies: ~w[2001:0db8:85a3::8A2E:0370:7334/128]) + head = [ + {"x-real-ip", "2001:0db8:85a3:0000:0000:8A2E:0370:7334"}, + {"x-real-ip", "127.0.0.2"} + ] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[2001:0db8:85a3::8A2E:0370:7334/128]] + assert nil == remote_ip(conn, opts) end test "from proxy to private" do - assert :peer == @conn |> forwarded("for=1.2.3.4, for=\"[fc00::1:2:3]\"") |> remote_ip(proxies: ~w[0.0.0.0/0]) - assert :peer == @conn |> forwarded("for=\"[::1.2.3.4]\", for=10.0.10.0") |> remote_ip(proxies: ~w[::/64]) - assert :peer == @conn |> x_client_ip("1.2.3.4,192.168.0.1") |> remote_ip(proxies: ~w[1.2.0.0/16]) - assert :peer == @conn |> custom("3.4.5.6 , 172.16.1.2") |> remote_ip(headers: ~w[custom], proxies: ~w[3.0.0.0/8]) + head = [{"custom", "3.4.5.6 , 172.16.1.2"}] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom], proxies: ~w[3.0.0.0/8]] + assert nil == remote_ip(conn, opts) end test "from proxy to proxy" do - assert :peer == @conn |> forwarded("for=1.2.3.4, for=1.2.3.5") |> remote_ip(proxies: ~w[1.2.3.0/24]) - assert :peer == @conn |> x_real_ip("a:b:c:d::,1:2:3:4::") |> remote_ip(proxies: ~w[a:b:c:d::/128 1:2:3:4::/64]) - assert :peer == @conn |> custom("1.2.3.4, 3.4.5.6") |> remote_ip(headers: ~w[custom], proxies: ~w[1.2.3.4/32 3.4.5.6/32]) + head = [{"forwarded", "for=1.2.3.4, for=1.2.3.5"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[1.2.3.0/24]] + assert nil == remote_ip(conn, opts) end - test "from proxy to non-proxy" do - assert {3, 4, 5, 6} == @conn |> forwarded("for=1.2.3.4,for=3.4.5.6") |> remote_ip(proxies: ~w[1.2.3.4/32]) - assert {0, 0, 0, 0, 3, 4, 5, 6} == @conn |> x_forwarded_for("::1:2:3:4, ::3:4:5:6") |> remote_ip(proxies: ~w[::1:2:3:4/128]) - assert {3, 4, 5, 6} == @conn |> custom("1.2.3.4, 3.4.5.6") |> remote_ip(headers: ~w[custom], proxies: ~w[1.2.3.4/32]) + test "from proxy to client" do + head = [{"x-forwarded-for", "::1:2:3:4, ::3:4:5:6"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[::1:2:3:4/128]] + assert {0, 0, 0, 0, 3, 4, 5, 6} == remote_ip(conn, opts) end - test "from non-proxy to unknown" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=1.2.3.4,for=not_an_ip") |> remote_ip - assert {0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0, 0x0} == @conn |> x_client_ip("a:b:c:d:e:f::,unknown") |> remote_ip(proxies: ~w[b::/64]) - assert {1, 2, 3, 4} == @conn |> custom("1.2.3.4,_obf") |> remote_ip(headers: ~w[custom]) + test "from client to unknown" do + head = [{"x-client-ip", "a:b:c:d:e:f::,unknown"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[b::/64]] + assert {0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x0, 0x0} == remote_ip(conn, opts) end - test "from non-proxy to loopback" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=1.2.3.4, for=127.0.0.1") |> remote_ip(proxies: ~w[abcd::/32]) - assert {1, 2, 3, 4} == @conn |> x_real_ip("1.2.3.4, ::1") |> remote_ip(proxies: ~w[4.3.2.1/32]) - assert {0x2001, 0x0db8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7334} == @conn |> custom("2001:0db8:85a3:0000:0000:8A2E:0370:7334, 127.0.0.2") |> remote_ip(headers: ~w[custom]) + test "from client to loopback" do + head = [{"x-real-ip", "127.0.0.1"}, {"x-real-ip", "127.0.0.2"}] + conn = %Plug.Conn{req_headers: head} + opts = [clients: ~w[127.0.0.1/32]] + assert {127, 0, 0, 1} == remote_ip(conn, opts) end - test "from non-proxy to private" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=1.2.3.4, for=\"[fc00::1:2:3]\"") |> remote_ip - assert {0, 0, 0, 0, 0, 0, 258, 772} == @conn |> forwarded("for=\"[::1.2.3.4]\", for=10.0.10.0") |> remote_ip(proxies: ~w[1:2:3:4::/64]) - assert {1, 2, 3, 4} == @conn |> x_forwarded_for("1.2.3.4,192.168.0.1") |> remote_ip(proxies: ~w[1.2.3.5/32]) - assert {3, 4, 5, 6} == @conn |> custom("3.4.5.6 , 172.16.1.2") |> remote_ip(headers: ~w[custom]) + test "from client to private" do + head = [{"custom", "::1.2.3.4, 10.0.10.0"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[1:2:3:4::/64], headers: ~w[custom]] + assert {0, 0, 0, 0, 0, 0, 258, 772} == remote_ip(conn, opts) end - test "from non-proxy to proxy" do - assert {1, 2, 3, 4} == @conn |> forwarded("for=1.2.3.4,for=3.4.5.6") |> remote_ip(proxies: ~w[3.4.5.6/32]) - assert {0, 0, 0, 0, 1, 2, 3, 4} == @conn |> x_client_ip("::1:2:3:4, ::3:4:5:6") |> remote_ip(proxies: ~w[::3:4:5:6/128]) - assert {1, 2, 3, 4} == @conn |> custom("1.2.3.4, 3.4.5.6") |> remote_ip(headers: ~w[custom], proxies: ~w[3.4.5.0/24]) + test "from client to proxy" do + head = [{"forwarded", "for=1.2.3.4,for=3.4.5.6"}] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[3.4.5.0/24]] + assert {1, 2, 3, 4} == remote_ip(conn, opts) end - test "from non-proxy to non-proxy" do - assert {3, 4, 5, 6} == @conn |> forwarded("for=1.2.3.4,for=3.4.5.6") |> remote_ip - assert {0, 0, 0, 0, 3, 4, 5, 6} == @conn |> x_real_ip("::1:2:3:4, ::3:4:5:6") |> remote_ip - assert {3, 4, 5, 6} == @conn |> custom("1.2.3.4, 3.4.5.6") |> remote_ip(headers: ~w[custom], proxies: ~w[5.6.7.8/32]) + test "from client to client" do + head = [{"x-forwarded-for", "1.2.3.4"}, {"x-forwarded-for", "10.45.0.1"}] + conn = %Plug.Conn{req_headers: head} + opts = [clients: ~w[10.45.0.0/16]] + assert {10, 45, 0, 1} == remote_ip(conn, opts) end - end - - test "several hops" do - conn = @conn |> forwarded("for=3.4.5.6") |> forwarded("for=10.0.0.1") |> forwarded("for=192.168.0.1") - assert {3, 4, 5, 6} == conn |> remote_ip - - conn = @conn |> x_real_ip("9.9.9.9, 172.31.4.4, 3.4.5.6, 10.0.0.1") - assert {3, 4, 5, 6} == conn |> remote_ip - conn = @conn |> custom("fe80::0202:b3ff:fe1e:8329") |> custom("::1") |> custom("::1") - assert {0xfe80, 0x0000, 0x0000, 0x0000, 0x0202, 0xb3ff, 0xfe1e, 0x8329} == conn |> remote_ip(headers: ~w[custom]) - - conn = @conn - |> x_forwarded_for("2001:0db8:85a3::8a2e:0370:7334") - |> x_forwarded_for("fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1") - |> x_forwarded_for("unknown, fc00::, fe00::, fdff::") - assert {0xfe80, 0x0000, 0x0000, 0x0000, 0x0202, 0xb3ff, 0xfe1e, 0x8329} == conn |> remote_ip(proxies: ~w[fe00::/128]) - end - - test "allowed headers" do - conn = @conn - |> put_req_header("a", "1.2.3.4") - |> put_req_header("b", "2.3.4.5") - |> put_req_header("c", "3.4.5.6") - - assert :peer == conn |> remote_ip(headers: ~w[]) - - assert {1, 2, 3, 4} == conn |> remote_ip(headers: ~w[a]) - assert {2, 3, 4, 5} == conn |> remote_ip(headers: ~w[a b]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[a c]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[a b c]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[a c b]) - - assert {2, 3, 4, 5} == conn |> remote_ip(headers: ~w[b]) - assert {2, 3, 4, 5} == conn |> remote_ip(headers: ~w[b a]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[b c]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[b a c]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[b c a]) - - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[c]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[c a]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[c b]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[c a b]) - assert {3, 4, 5, 6} == conn |> remote_ip(headers: ~w[c b a]) + test "more than two hops" do + head = [ + {"forwarded", "for=\"[fe80::0202:b3ff:fe1e:8329]\""}, + {"forwarded", "for=1.2.3.4"}, + {"x-forwarded-for", "172.16.0.10"}, + {"x-client-ip", "::1, ::1"}, + {"x-real-ip", "2.3.4.5, fc00::1, 2.4.6.8"} + ] + conn = %Plug.Conn{req_headers: head} + opts = [proxies: ~w[2.0.0.0/8]] + assert {1, 2, 3, 4} == remote_ip(conn, opts) + end end - test "allowed headers maintain relative ordering" do - headers = ~w[a b c] - - a = fn conn -> put_req_header(conn, "a", "1.2.3.4") end - b = fn conn -> put_req_header(conn, "b", "2.3.4.5") end - c = fn conn -> put_req_header(conn, "c", "3.4.5.6") end - - assert :peer == @conn |> remote_ip(headers: headers) + describe ":headers option" do + test "overrides the defaults" do + head = [ + {"forwarded", "for=1.2.3.4"}, + {"x-forwarded-for", "1.2.3.4"}, + {"x-client-ip", "1.2.3.4"}, + {"x-real-ip", "1.2.3.4"} + ] + conn = %Plug.Conn{req_headers: head} + opts = [headers: ~w[custom]] + fail = "default headers are still being parsed" + refute {1, 2, 3, 4} == remote_ip(conn, opts), fail + end - assert {1, 2, 3, 4} == @conn |> a.() |> remote_ip(headers: headers) - assert {2, 3, 4, 5} == @conn |> a.() |> b.() |> remote_ip(headers: headers) - assert {3, 4, 5, 6} == @conn |> a.() |> c.() |> remote_ip(headers: headers) - assert {3, 4, 5, 6} == @conn |> a.() |> b.() |> c.() |> remote_ip(headers: headers) - assert {2, 3, 4, 5} == @conn |> a.() |> c.() |> b.() |> remote_ip(headers: headers) + test "order doesn't matter" do + head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}] + conn = %Plug.Conn{req_headers: head} - assert {2, 3, 4, 5} == @conn |> b.() |> remote_ip(headers: headers) - assert {1, 2, 3, 4} == @conn |> b.() |> a.() |> remote_ip(headers: headers) - assert {3, 4, 5, 6} == @conn |> b.() |> c.() |> remote_ip(headers: headers) - assert {3, 4, 5, 6} == @conn |> b.() |> a.() |> c.() |> remote_ip(headers: headers) - assert {1, 2, 3, 4} == @conn |> b.() |> c.() |> a.() |> remote_ip(headers: headers) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[a b c]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[a c b]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[b a c]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[b c a]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[c a b]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[c b a]) + end - assert {3, 4, 5, 6} == @conn |> c.() |> remote_ip(headers: headers) - assert {1, 2, 3, 4} == @conn |> c.() |> a.() |> remote_ip(headers: headers) - assert {2, 3, 4, 5} == @conn |> c.() |> b.() |> remote_ip(headers: headers) - assert {2, 3, 4, 5} == @conn |> c.() |> a.() |> b.() |> remote_ip(headers: headers) - assert {1, 2, 3, 4} == @conn |> c.() |> b.() |> a.() |> remote_ip(headers: headers) + test "ignores unspecified headers" do + head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}] + conn = %Plug.Conn{req_headers: head} + + assert {2, 3, 4, 5} = remote_ip(conn, headers: ~w[a b]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[a c]) + assert {2, 3, 4, 5} = remote_ip(conn, headers: ~w[b a]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[b c]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[c a]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[c b]) + assert {1, 2, 3, 4} = remote_ip(conn, headers: ~w[a]) + assert {2, 3, 4, 5} = remote_ip(conn, headers: ~w[b]) + assert {3, 4, 5, 6} = remote_ip(conn, headers: ~w[c]) + end end end