Page MenuHomePhorge

No OneTemporary

Size
64 KB
Referenced Files
None
Subscribers
None
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6e1db0f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+# The directory Mix will write compiled artifacts to.
+/_build
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover
+
+# The directory Mix downloads your dependencies sources to.
+/deps
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..036bfc5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# RemoteIp
+
+**TODO: Add description**
+
+## Installation
+
+If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
+
+ 1. Add `remote_ip` to your list of dependencies in `mix.exs`:
+
+ ```elixir
+ def deps do
+ [{:remote_ip, "~> 0.1.0"}]
+ end
+ ```
+
+ 2. Ensure `remote_ip` is started before your application:
+
+ ```elixir
+ def application do
+ [applications: [:remote_ip]]
+ end
+ ```
+
diff --git a/config/config.exs b/config/config.exs
new file mode 100644
index 0000000..e9641b4
--- /dev/null
+++ b/config/config.exs
@@ -0,0 +1,30 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Mix.Config module.
+use Mix.Config
+
+# This configuration is loaded before any dependency and is restricted
+# to this project. If another project depends on this project, this
+# file won't be loaded nor affect the parent project. For this reason,
+# if you want to provide default values for your application for
+# 3rd-party users, it should be done in your "mix.exs" file.
+
+# You can configure for your application as:
+#
+# config :remote_ip, key: :value
+#
+# And access this configuration in your application as:
+#
+# Application.get_env(:remote_ip, :key)
+#
+# Or configure a 3rd-party app:
+#
+# config :logger, level: :info
+#
+
+# It is also possible to import configuration files, relative to this
+# directory. For example, you can emulate configuration per environment
+# by uncommenting the line below and defining dev.exs, test.exs and such.
+# Configuration from the imported file will override the ones defined
+# here (which is why it is important to import them last).
+#
+# import_config "#{Mix.env}.exs"
diff --git a/lib/remote_ip.ex b/lib/remote_ip.ex
new file mode 100644
index 0000000..acfb68b
--- /dev/null
+++ b/lib/remote_ip.ex
@@ -0,0 +1,75 @@
+defmodule RemoteIp do
+ @behaviour Plug
+
+ @headers ~w[
+ forwarded
+ x-forwarded-for
+ ]
+ # x-client-ip
+ # x-real-ip
+ # etc
+
+ @proxies []
+
+ # https://en.wikipedia.org/wiki/Loopback
+ # https://en.wikipedia.org/wiki/Private_network
+ @reserved ~w[
+ 127.0.0.0/8
+ ::1/128
+ fc00::/7
+ 10.0.0.0/8
+ 172.16.0.0/12
+ 192.168.0.0/16
+ ]
+
+ def init(opts \\ []) do
+ headers = Keyword.get(opts, :headers, @headers)
+ proxies = Keyword.get(opts, :proxies, @proxies) ++ @reserved
+ proxies = proxies |> Enum.map(&InetCidr.parse/1)
+
+ {headers, proxies}
+ end
+
+ def call(conn, {[], _proxies}) do
+ conn
+ end
+
+ def call(conn, {[header | next_headers], proxies}) when is_binary(header) do
+ case last_forwarded_ip(conn, header, proxies) do
+ nil -> call(conn, {next_headers, proxies})
+ ip -> %{conn | remote_ip: ip}
+ end
+ end
+
+ defp last_forwarded_ip(conn, header, proxies) do
+ conn
+ |> ips_from(header)
+ |> last_ip_forwarded_through(proxies)
+ end
+
+ defp last_ip_forwarded_through(ips, proxies) do
+ ips
+ |> Enum.reverse
+ |> Enum.find(&forwarded?(&1, proxies))
+ end
+
+ defp forwarded?(ip, proxies) do
+ !proxy?(ip, proxies)
+ end
+
+ defp proxy?(ip, proxies) do
+ Enum.any?(proxies, fn proxy -> InetCidr.contains?(proxy, ip) end)
+ end
+
+ defp ips_from(conn, "forwarded" = header) do
+ conn
+ |> Plug.Conn.get_req_header(header)
+ |> RemoteIp.Headers.Forwarded.parse
+ end
+
+ defp ips_from(conn, header) when is_binary(header) do
+ conn
+ |> Plug.Conn.get_req_header(header)
+ |> RemoteIp.Headers.Generic.parse
+ end
+end
diff --git a/lib/remote_ip/headers/forwarded.ex b/lib/remote_ip/headers/forwarded.ex
new file mode 100644
index 0000000..6d9e8e3
--- /dev/null
+++ b/lib/remote_ip/headers/forwarded.ex
@@ -0,0 +1,143 @@
+defmodule RemoteIp.Headers.Forwarded do
+ use Combine
+
+ def parse(headers) when is_list(headers) do
+ Enum.flat_map(headers, fn header ->
+ case Combine.parse(header, forwarded) do
+ [elements] -> Enum.flat_map(elements, &parse_forwarded_for/1)
+ _ -> []
+ end
+ end)
+ end
+
+ defp parse_forwarded_for(pairs) do
+ case pairs |> fors do
+ [string] -> parse_ip(string)
+ _ -> [] # no `for=`s or multiple `for=`s
+ end
+ end
+
+ defp fors(pairs) do
+ for {key, val} <- pairs, String.downcase(key) == "for", do: val
+ end
+
+ defp parse_ip(string) do
+ case Combine.parse(string, ip_address) do
+ [ip] -> [ip]
+ _ -> []
+ end
+ end
+
+ # https://tools.ietf.org/html/rfc7239#section-4
+
+ defp forwarded do
+ sep_by(forwarded_element, comma) |> eof
+ end
+
+ defp forwarded_element do
+ sep_by1(forwarded_pair, char(";"))
+ end
+
+ defp forwarded_pair do
+ pair = [token, ignore(char("=")), value]
+ pipe(pair, &List.to_tuple/1)
+ end
+
+ defp value do
+ either(token, quoted_string)
+ end
+
+ # https://tools.ietf.org/html/rfc7230#section-3.2.6
+
+ defp token do
+ word_of(~r/[!#$%&'*+\-.^_`|~0-9a-zA-Z]/)
+ end
+
+ defp quoted_string do
+ quoted(string_of(either(qdtext, quoted_pair)))
+ end
+
+ defp quoted(parser) do
+ between(char("\""), parser, char("\""))
+ end
+
+ defp string_of(parser) do
+ map(many(parser), &Enum.join/1)
+ end
+
+ defp qdtext do
+ word_of(~r/[\t \x21\x23-\x5B\x5D-\x7E\x80-\xFF]/)
+ end
+
+ @quotable [?\t]
+ ++ Enum.to_list(0x21..0x7E)
+ ++ Enum.to_list(0x80..0xFF)
+ |> Enum.map(&<<&1::utf8>>)
+
+ defp quoted_pair do
+ ignore(char("\\")) |> one_of(char, @quotable)
+ end
+
+ # https://tools.ietf.org/html/rfc7230#section-7
+
+ defp comma do
+ skip(many(either(space, tab)))
+ |> char(",")
+ |> skip(many(either(space, tab)))
+ end
+
+ # https://tools.ietf.org/html/rfc7239#section-6
+
+ defp ip_address do
+ node_name
+ |> ignore(option(ignore(char(":")) |> node_port))
+ |> eof
+ end
+
+ defp node_name do
+ choice [
+ ipv4_address,
+ between(char("["), ipv6_address, char("]")),
+ ignore(string("unknown")),
+ ignore(obfuscated),
+ ]
+ end
+
+ defp node_port(previous) do
+ previous |> either(port, obfuscated)
+ end
+
+ defp port do
+ # Have to try to parse the wider integers first due to greediness. For
+ # example, the port "12345" would be matched by fixed_integer(1) and the
+ # remaining "2345" would cause a parse error for the eof in ip_address/0.
+
+ choice(Enum.map(5..1, &fixed_integer/1))
+ end
+
+ defp obfuscated do
+ word_of(~r/^_[a-zA-Z0-9._\-]+/)
+ end
+
+ # Could follow the ABNF described in
+ # https://tools.ietf.org/html/rfc3986#section-3.2.2, but prefer to lean on
+ # the existing :inet parser - we want its output anyway.
+
+ defp ipv4_address do
+ map(word_of(~r/[0-9.]/), fn string ->
+ case :inet.parse_ipv4strict_address(string |> to_char_list) do
+ {:ok, ip} -> ip
+ {:error, :einval} -> {:error, "Invalid IPv4 address"}
+ end
+ end)
+ end
+
+ defp ipv6_address do
+ map(word_of(~r/[0-9a-f:.]/i), fn string ->
+ case :inet.parse_ipv6strict_address(string |> to_char_list) do
+ {:ok, ip} -> ip
+ {:error, :einval} -> {:error, "Invalid IPv6 address"}
+ end
+ end)
+ end
+end
diff --git a/lib/remote_ip/headers/generic.ex b/lib/remote_ip/headers/generic.ex
new file mode 100644
index 0000000..925fbe6
--- /dev/null
+++ b/lib/remote_ip/headers/generic.ex
@@ -0,0 +1,26 @@
+defmodule RemoteIp.Headers.Generic do
+ def parse(headers) when is_list(headers) do
+ headers
+ |> split_commas
+ |> parse_ips
+ end
+
+ defp split_commas(headers) do
+ Enum.flat_map(headers, fn header ->
+ header |> String.trim |> String.split(~r/\s*,\s*/)
+ end)
+ end
+
+ defp parse_ips(strings) do
+ Enum.reduce(strings, [], fn string, ips ->
+ case parse_ip(string) do
+ {:ok, ip} -> [ip | ips]
+ {:error, :einval} -> ips
+ end
+ end) |> Enum.reverse
+ end
+
+ defp parse_ip(string) do
+ string |> to_char_list |> :inet.parse_strict_address
+ end
+end
diff --git a/mix.exs b/mix.exs
new file mode 100644
index 0000000..72e42ba
--- /dev/null
+++ b/mix.exs
@@ -0,0 +1,22 @@
+defmodule RemoteIp.Mixfile do
+ use Mix.Project
+
+ def project do
+ [app: :remote_ip,
+ version: "0.1.0",
+ elixir: "~> 1.3",
+ build_embedded: Mix.env == :prod,
+ start_permanent: Mix.env == :prod,
+ deps: deps]
+ end
+
+ def application do
+ [applications: [:plug]]
+ end
+
+ defp deps do
+ [{:combine, "~> 0.9.2"},
+ {:plug, "~> 1.0"},
+ {:inet_cidr, "~> 1.0"}]
+ end
+end
diff --git a/mix.lock b/mix.lock
new file mode 100644
index 0000000..a4d9860
--- /dev/null
+++ b/mix.lock
@@ -0,0 +1,4 @@
+%{"combine": {:hex, :combine, "0.9.2", "cd3c8721f378ebe032487d8a4fa2ced3181a456a3c21b16464da8c46904bb552", [:mix], []},
+ "inet_cidr": {:hex, :inet_cidr, "1.0.1", "9038d0598eb2f7f4b4347c566b773c79e10edfb038633de9f55d538ba2703ac3", [:mix], []},
+ "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []},
+ "plug": {:hex, :plug, "1.2.2", "cfbda521b54c92ab8ddffb173fbaabed8d8fc94bec07cd9bb58a84c1c501b0bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}}
diff --git a/test/remote_ip/headers/forwarded_test.exs b/test/remote_ip/headers/forwarded_test.exs
new file mode 100644
index 0000000..7c896b2
--- /dev/null
+++ b/test/remote_ip/headers/forwarded_test.exs
@@ -0,0 +1,395 @@
+defmodule RemoteIp.Headers.ForwardedTest do
+ use ExUnit.Case, async: true
+ alias RemoteIp.Headers.Forwarded
+
+ describe "parsing" do
+ test "RFC 7239 examples" do
+ parsed = Forwarded.parse([~S'for="_gazonk"'])
+ assert parsed == []
+
+ parsed = Forwarded.parse([~S'For="[2001:db8:cafe::17]:4711"'])
+ assert parsed == [{8193, 3512, 51966, 0, 0, 0, 0, 23}]
+
+ parsed = Forwarded.parse([~S'for=192.0.2.60;proto=http;by=203.0.113.43'])
+ assert parsed == [{192, 0, 2, 60}]
+
+ parsed = Forwarded.parse([~S'for=192.0.2.43, for=198.51.100.17'])
+ assert parsed == [{192, 0, 2, 43}, {198, 51, 100, 17}]
+ end
+
+ test "an empty list of headers" do
+ assert [] == Forwarded.parse([])
+ end
+
+ test "case insensitivity" do
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'for=0.0.0.0'])
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'foR=0.0.0.0'])
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'fOr=0.0.0.0'])
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'fOR=0.0.0.0'])
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'For=0.0.0.0'])
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'FoR=0.0.0.0'])
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'FOr=0.0.0.0'])
+ assert [{0, 0, 0, 0}] == Forwarded.parse([~S'FOR=0.0.0.0'])
+
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'for="[::]"'])
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'foR="[::]"'])
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'fOr="[::]"'])
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'fOR="[::]"'])
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'For="[::]"'])
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'FoR="[::]"'])
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'FOr="[::]"'])
+ assert [{0, 0, 0, 0, 0, 0, 0, 0}] == Forwarded.parse([~S'FOR="[::]"'])
+ end
+
+ test "IPv4" do
+ assert [] == Forwarded.parse([~S'for='])
+ assert [] == Forwarded.parse([~S'for=1'])
+ assert [] == Forwarded.parse([~S'for=1.2'])
+ assert [] == Forwarded.parse([~S'for=1.2.3'])
+ assert [] == Forwarded.parse([~S'for=1000.2.3.4'])
+ assert [] == Forwarded.parse([~S'for=1.2000.3.4'])
+ assert [] == Forwarded.parse([~S'for=1.2.3000.4'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4000'])
+ assert [] == Forwarded.parse([~S'for=1abc.2.3.4'])
+ assert [] == Forwarded.parse([~S'for=1.2abc.3.4'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4abc'])
+ assert [] == Forwarded.parse([~S'for=1.2.3abc.4'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4abc'])
+ assert [] == Forwarded.parse([~S'for="1.2.3.4'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4"'])
+
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for=1.2.3.4'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="1.2.3.4"'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="\1.2\.3.\4"'])
+ end
+
+ test "IPv4 with port" do
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:1'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:12'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:123'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:1234'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:12345'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:123456'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:_underscore'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4:no_underscore'])
+
+ assert [] == Forwarded.parse([~S'for="1.2.3.4:"'])
+ assert [] == Forwarded.parse([~S'for="1.2.3.4:123456"'])
+ assert [] == Forwarded.parse([~S'for="1.2.3.4:no_underscore"'])
+ assert [] == Forwarded.parse([~S'for="1.2\.3.4\:no_un\der\score"'])
+
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="1.2.3.4:1"'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="1.2.3.4:12"'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="1.2.3.4:123"'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="1.2.3.4:1234"'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="1.2.3.4:12345"'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="1.2.3.4:_underscore"'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for="\1.2\.3.4\:_po\r\t"'])
+ end
+
+ test "improperly formatted IPv6" do
+ assert [] == Forwarded.parse([~S'for=[127.0.0.1]'])
+ assert [] == Forwarded.parse([~S'for="[127.0.0.1]"'])
+
+ assert [] == Forwarded.parse([~S'for=::127.0.0.1'])
+ assert [] == Forwarded.parse([~S'for=[::127.0.0.1]'])
+ assert [] == Forwarded.parse([~S'for="::127.0.0.1"'])
+ assert [] == Forwarded.parse([~S'for="[::127.0.0.1"'])
+ assert [] == Forwarded.parse([~S'for="::127.0.0.1]"'])
+
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8"'])
+ assert [] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8]"'])
+ end
+
+ test "IPv6 with port" do
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:1'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:12'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:123'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:1234'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:12345'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:123456'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:_underscore'])
+ assert [] == Forwarded.parse([~S'for=::1.2.3.4:no_underscore'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:1'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:12'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:123'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:1234'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:12345'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:123456'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:_underscore'])
+ assert [] == Forwarded.parse([~S'for=[::1.2.3.4]:no_underscore'])
+
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:"'])
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:123456"'])
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:no_underscore"'])
+ assert [] == Forwarded.parse([~S'for="::1.2\.3.4\:no_un\der\score"'])
+ assert [] == Forwarded.parse([~S'for="[::1.2.3.4]:"'])
+ assert [] == Forwarded.parse([~S'for="[::1.2.3.4]:123456"'])
+ assert [] == Forwarded.parse([~S'for="[::1.2.3.4]:no_underscore"'])
+ assert [] == Forwarded.parse([~S'for="\[::1.2\.3.4]\:no_un\der\score"'])
+
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:1"'])
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:12"'])
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:123"'])
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:1234"'])
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:12345"'])
+ assert [] == Forwarded.parse([~S'for="::1.2.3.4:_underscore"'])
+ assert [] == Forwarded.parse([~S'for="::\1.2\.3.4\:_po\r\t"'])
+
+ assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse([~S'for="[::1.2.3.4]:1"'])
+ assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse([~S'for="[::1.2.3.4]:12"'])
+ assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse([~S'for="[::1.2.3.4]:123"'])
+ assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse([~S'for="[::1.2.3.4]:1234"'])
+ assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse([~S'for="[::1.2.3.4]:12345"'])
+ assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse([~S'for="[::1.2.3.4]:_underscore"'])
+ assert [{0, 0, 0, 0, 0, 0, 258, 772}] == Forwarded.parse([~S'for="[::\1.2\.3.4\]\:_po\r\t"'])
+
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:1'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:12'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:123'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:1234'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:12345'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:123456'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:_underscore'])
+ assert [] == Forwarded.parse([~S'for=1:2:3:4:5:6:7:8:no_underscore'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:1'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:12'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:123'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:1234'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:12345'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:123456'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:_underscore'])
+ assert [] == Forwarded.parse([~S'for=[1:2:3:4:5:6:7:8]:no_underscore'])
+
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:123456"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:no_underscore"'])
+ assert [] == Forwarded.parse([~S'for="::1.2\.3.4\:no_un\der\score"'])
+ assert [] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:"'])
+ assert [] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:123456"'])
+ assert [] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:no_underscore"'])
+ assert [] == Forwarded.parse([~S'for="\[1:2\:3:4:5:6:7:8]\:no_un\der\score"'])
+
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:1"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:12"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:123"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:1234"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:12345"'])
+ assert [] == Forwarded.parse([~S'for="1:2:3:4:5:6:7:8:_underscore"'])
+ assert [] == Forwarded.parse([~S'for="\1:2\:3:4:5:6:7:8\:_po\r\t"'])
+
+ assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:1"'])
+ assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:12"'])
+ assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:123"'])
+ assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:1234"'])
+ assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:12345"'])
+ assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse([~S'for="[1:2:3:4:5:6:7:8]:_underscore"'])
+ assert [{1, 2, 3, 4, 5, 6, 7, 8}] == Forwarded.parse([~S'for="[1:2:3:4:\5:6\:7:8\]\:_po\r\t"'])
+ end
+
+ test "IPv6 without ::" do
+ assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456:7890:a:bc:def:d34d]"'])
+ assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23:456:7890:a:bc:1.2.3.4]"'])
+ end
+
+ test "IPv6 with :: at position 0" do
+ assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[::23:456:7890:a:bc:def:d34d]"'])
+ assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[::23:456:7890:a:bc:1.2.3.4]"'])
+ assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[::456:7890:a:bc:def:d34d]"'])
+ assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[::456:7890:a:bc:1.2.3.4]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[::7890:a:bc:def:d34d]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[::7890:a:bc:1.2.3.4]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[::a:bc:def:d34d]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[::a:bc:1.2.3.4]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[::bc:def:d34d]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[::bc:1.2.3.4]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[::def:d34d]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[::1.2.3.4]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Forwarded.parse([~S'for="[::d34d]"'])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[::]"'])
+ end
+
+ test "IPv6 with :: at position 1" do
+ assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1::456:7890:a:bc:def:d34d]"'])
+ assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1::456:7890:a:bc:1.2.3.4]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1::7890:a:bc:def:d34d]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1::7890:a:bc:1.2.3.4]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1::a:bc:def:d34d]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1::a:bc:1.2.3.4]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1::bc:def:d34d]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1::bc:1.2.3.4]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1::def:d34d]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1::1.2.3.4]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Forwarded.parse([~S'for="[1::d34d]"'])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[1::]"'])
+ end
+
+ test "IPv6 with :: at position 2" do
+ assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23::7890:a:bc:def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23::7890:a:bc:1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23::a:bc:def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23::a:bc:1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23::bc:def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23::bc:1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23::def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23::1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Forwarded.parse([~S'for="[1:23::d34d]"'])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[1:23::]"'])
+ end
+
+ test "IPv6 with :: at position 3" do
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456::a:bc:def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23:456::a:bc:1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456::bc:def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23:456::bc:1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456::def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23:456::1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456::d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[1:23:456::]"'])
+ end
+
+ test "IPv6 with :: at position 4" do
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456:7890::bc:def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00bc, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23:456:7890::bc:1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456:7890::def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23:456:7890::1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456:7890::d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[1:23:456:7890::]"'])
+ end
+
+ test "IPv6 with :: at position 5" do
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0def, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456:7890:a::def:d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0102, 0x0304}] == Forwarded.parse([~S'for="[1:23:456:7890:a::1.2.3.4]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0000, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456:7890:a::d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[1:23:456:7890:a::]"'])
+ end
+
+ test "IPv6 with :: at position 6" do
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0000, 0xd34d}] == Forwarded.parse([~S'for="[1:23:456:7890:a:bc::d34d]"'])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[1:23:456:7890:a:bc::]"'])
+ end
+
+ test "IPv6 with leading zeroes" do
+ assert [{0x0000, 0x0001, 0x0002, 0x0003, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[0:01:002:0003:0000::]"'])
+ assert [{0x000a, 0x001a, 0x002a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[0a:01a:002a::]"'])
+ assert [{0x00ab, 0x01ab, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[0ab:01ab::]"'])
+ assert [{0x0abc, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Forwarded.parse([~S'for="[0abc::]"'])
+ end
+
+ test "IPv6 with mixed case" do
+ assert [{0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd}] == Forwarded.parse([~S'for="[abcd:abcD:abCd:abCD:aBcd:aBcD:aBCd:aBCD]"'])
+ assert [{0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd}] == Forwarded.parse([~S'for="[Abcd:AbcD:AbCd:AbCD:ABcd:ABcD:ABCd:ABCD]"'])
+ end
+
+ test "semicolons" do
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'for=1.2.3.4;proto=http;by=2.3.4.5'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'proto=http;for=1.2.3.4;by=2.3.4.5'])
+ assert [{1, 2, 3, 4}] == Forwarded.parse([~S'proto=http;by=2.3.4.5;for=1.2.3.4'])
+
+ assert [] == Forwarded.parse([~S'for=1.2.3.4proto=http;by=2.3.4.5'])
+ assert [] == Forwarded.parse([~S'proto=httpfor=1.2.3.4;by=2.3.4.5'])
+ assert [] == Forwarded.parse([~S'proto=http;by=2.3.4.5for=1.2.3.4'])
+
+ assert [] == Forwarded.parse([~S'for=1.2.3.4;proto=http;by=2.3.4.5;'])
+ assert [] == Forwarded.parse([~S'proto=http;for=1.2.3.4;by=2.3.4.5;'])
+ assert [] == Forwarded.parse([~S'proto=http;by=2.3.4.5;for=1.2.3.4;'])
+
+ assert [] == Forwarded.parse([~S'for=1.2.3.4;proto=http;for=2.3.4.5'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4;for=2.3.4.5;proto=http'])
+ assert [] == Forwarded.parse([~S'proto=http;for=1.2.3.4;for=2.3.4.5'])
+ end
+
+ test "parameters other than `for`" do
+ assert [] == Forwarded.parse([~S'by=1.2.3.4'])
+ assert [] == Forwarded.parse([~S'host=example.com'])
+ assert [] == Forwarded.parse([~S'proto=http'])
+ assert [] == Forwarded.parse([~S'by=1.2.3.4;proto=http;host=example.com'])
+ end
+
+ test "bad whitespace" do
+ assert [] == Forwarded.parse([~S'for= 1.2.3.4'])
+ assert [] == Forwarded.parse([~S'for = 1.2.3.4'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4; proto=http'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4 ;proto=http'])
+ assert [] == Forwarded.parse([~S'for=1.2.3.4 ; proto=http'])
+ assert [] == Forwarded.parse([~S'proto=http; for=1.2.3.4'])
+ assert [] == Forwarded.parse([~S'proto=http ;for=1.2.3.4'])
+ assert [] == Forwarded.parse([~S'proto=http ; for=1.2.3.4'])
+ end
+
+ test "commas" do
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse([~S'for=1.2.3.4, for=2.3.4.5'])
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}] == Forwarded.parse([~S'for=1.2.3.4, for="[::2:3:4:5]"'])
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}] == Forwarded.parse([~S'for=1.2.3.4, for="[::2:3:4:5]"'])
+ assert [{0, 0, 0, 0, 1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse([~S'for="[::1:2:3:4]", for=2.3.4.5'])
+ assert [{0, 0, 0, 0, 1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}] == Forwarded.parse([~S'for="[::1:2:3:4]", for="[::2:3:4:5]"'])
+ end
+
+ test "optional whitespace" do
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}, {4, 5, 6, 7}, {5, 6, 7, 8}] == Forwarded.parse(["for=1.2.3.4,for=2.3.4.5,\sfor=3.4.5.6\s,for=4.5.6.7\s,\sfor=5.6.7.8"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}, {4, 5, 6, 7}, {5, 6, 7, 8}] == Forwarded.parse(["for=1.2.3.4,for=2.3.4.5,\tfor=3.4.5.6\t,for=4.5.6.7\t,\tfor=5.6.7.8"])
+
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\s,\s\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\s,\s\tfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\s,\t\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\s,\t\tfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\t,\s\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\t,\s\tfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\t,\t\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\s\t,\t\tfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\s,\s\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\s,\s\tfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\s,\t\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\s,\t\tfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\t,\s\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\t,\s\tfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\t,\t\sfor=2.3.4.5"])
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\t,\t\tfor=2.3.4.5"])
+
+ assert [{1, 2, 3, 4}, {2, 3, 4, 5}] == Forwarded.parse(["for=1.2.3.4\t\s\s\s\s\t\s\t\s\t,\t\s\s\t\tfor=2.3.4.5"])
+ end
+
+ test "commas and semicolons" do
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7}] == Forwarded.parse([~S'for=1.2.3.4, for="[::2:3:4:5]";proto=http;host=example.com, proto=http;for=3.4.5.6;by=127.0.0.1, proto=http;host=example.com;for="[::4:5:6:7]"'])
+ end
+
+ test "multiple headers" do
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7}] == Forwarded.parse([
+ ~S'for=1.2.3.4',
+ ~S'for="[::2:3:4:5]";proto=http;host=example.com',
+ ~S'proto=http;for=3.4.5.6;by=127.0.0.1',
+ ~S'proto=http;host=example.com;for="[::4:5:6:7]"',
+ ])
+
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7}] == Forwarded.parse([
+ ~S'for=1.2.3.4, for="[::2:3:4:5]";proto=http;host=example.com',
+ ~S'proto=http;for=3.4.5.6;by=127.0.0.1',
+ ~S'proto=http;host=example.com;for="[::4:5:6:7]"',
+ ])
+
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7}] == Forwarded.parse([
+ ~S'for=1.2.3.4, for="[::2:3:4:5]";proto=http;host=example.com, proto=http;for=3.4.5.6;by=127.0.0.1',
+ ~S'proto=http;host=example.com;for="[::4:5:6:7]"',
+ ])
+
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7}] == Forwarded.parse([
+ ~S'for=1.2.3.4',
+ ~S'for="[::2:3:4:5]";proto=http;host=example.com',
+ ~S'proto=http;for=3.4.5.6;by=127.0.0.1, proto=http;host=example.com;for="[::4:5:6:7]"',
+ ])
+
+ assert [{1, 2, 3, 4}, {0, 0, 0, 0, 2, 3, 4, 5}, {3, 4, 5, 6}, {0, 0, 0, 0, 4, 5, 6, 7}] == Forwarded.parse([
+ ~S'for=1.2.3.4',
+ ~S'for="[::2:3:4:5]";proto=http;host=example.com, proto=http;for=3.4.5.6;by=127.0.0.1, proto=http;host=example.com;for="[::4:5:6:7]"',
+ ])
+ end
+ end
+end
diff --git a/test/remote_ip/headers/generic_test.exs b/test/remote_ip/headers/generic_test.exs
new file mode 100644
index 0000000..391fb0c
--- /dev/null
+++ b/test/remote_ip/headers/generic_test.exs
@@ -0,0 +1,175 @@
+defmodule RemoteIp.Headers.GenericTest do
+ use ExUnit.Case, async: true
+ alias RemoteIp.Headers.Generic
+
+ describe "parsing" do
+ test "an empty list of headers" do
+ assert [] == Generic.parse([])
+ end
+
+ test "bad IPs" do
+ assert [] == Generic.parse([""])
+ assert [] == Generic.parse([" "])
+ assert [] == Generic.parse(["not_an_ip"])
+ assert [] == Generic.parse(["unknown"])
+ end
+
+ test "bad IPv4" do
+ assert [] == Generic.parse(["1"])
+ assert [] == Generic.parse(["1.2"])
+ assert [] == Generic.parse(["1.2.3"])
+ assert [] == Generic.parse(["1000.2.3.4"])
+ assert [] == Generic.parse(["1.2000.3.4"])
+ assert [] == Generic.parse(["1.2.3000.4"])
+ assert [] == Generic.parse(["1.2.3.4000"])
+ assert [] == Generic.parse(["1abc.2.3.4"])
+ assert [] == Generic.parse(["1.2abc.3.4"])
+ assert [] == Generic.parse(["1.2.3.4abc"])
+ assert [] == Generic.parse(["1.2.3abc.4"])
+ assert [] == Generic.parse(["1.2.3.4abc"])
+ assert [] == Generic.parse(["1.2.3.4.5"])
+ end
+
+ test "bad IPv6" do
+ assert [] == Generic.parse(["1:"])
+ assert [] == Generic.parse(["1:2"])
+ assert [] == Generic.parse(["1:2:3"])
+ assert [] == Generic.parse(["1:2:3:4"])
+ assert [] == Generic.parse(["1:2:3:4:5"])
+ assert [] == Generic.parse(["1:2:3:4:5:6"])
+ assert [] == Generic.parse(["1:2:3:4:5:6:7"])
+ assert [] == Generic.parse(["1:2:3:4:5:6:7:8:"])
+ assert [] == Generic.parse(["1:2:3:4:5:6:7:8:9"])
+ assert [] == Generic.parse(["1:::2:3:4:5:6:7:8"])
+ assert [] == Generic.parse(["a:b:c:d:e:f::g"])
+ end
+
+ test "IPv4" do
+ assert [{1, 2, 3, 4}] == Generic.parse(["1.2.3.4"])
+ assert [{1, 2, 3, 4}] == Generic.parse([" 1.2.3.4 "])
+ end
+
+ test "IPv6 without ::" do
+ assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1:23:456:7890:a:bc:def:d34d"])
+ assert [{0x0001, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1:23:456:7890:a:bc:1.2.3.4"])
+ end
+
+ test "IPv6 with :: at position 0" do
+ assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["::23:456:7890:a:bc:def:d34d"])
+ assert [{0x0000, 0x0023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["::23:456:7890:a:bc:1.2.3.4"])
+ assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["::456:7890:a:bc:def:d34d"])
+ assert [{0x0000, 0x0000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["::456:7890:a:bc:1.2.3.4"])
+ assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["::7890:a:bc:def:d34d"])
+ assert [{0x0000, 0x0000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["::7890:a:bc:1.2.3.4"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["::a:bc:def:d34d"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["::a:bc:1.2.3.4"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["::bc:def:d34d"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["::bc:1.2.3.4"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Generic.parse(["::def:d34d"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse(["::1.2.3.4"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Generic.parse(["::d34d"])
+ assert [{0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["::"])
+ end
+
+ test "IPv6 with :: at position 1" do
+ assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1::456:7890:a:bc:def:d34d"])
+ assert [{0x0001, 0x000, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1::456:7890:a:bc:1.2.3.4"])
+ assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1::7890:a:bc:def:d34d"])
+ assert [{0x0001, 0x000, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1::7890:a:bc:1.2.3.4"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1::a:bc:def:d34d"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1::a:bc:1.2.3.4"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1::bc:def:d34d"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1::bc:1.2.3.4"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Generic.parse(["1::def:d34d"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse(["1::1.2.3.4"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Generic.parse(["1::d34d"])
+ assert [{0x0001, 0x000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["1::"])
+ end
+
+ test "IPv6 with :: at position 2" do
+ assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1:23::7890:a:bc:def:d34d"])
+ assert [{0x0001, 0x023, 0x0000, 0x7890, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1:23::7890:a:bc:1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1:23::a:bc:def:d34d"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1:23::a:bc:1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1:23::bc:def:d34d"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1:23::bc:1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Generic.parse(["1:23::def:d34d"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse(["1:23::1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Generic.parse(["1:23::d34d"])
+ assert [{0x0001, 0x023, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["1:23::"])
+ end
+
+ test "IPv6 with :: at position 3" do
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000a, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1:23:456::a:bc:def:d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x000a, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1:23:456::a:bc:1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1:23:456::bc:def:d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1:23:456::bc:1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0def, 0xd34d}] == Generic.parse(["1:23:456::def:d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse(["1:23:456::1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0xd34d}] == Generic.parse(["1:23:456::d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["1:23:456::"])
+ end
+
+ test "IPv6 with :: at position 4" do
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00bc, 0x0def, 0xd34d}] == Generic.parse(["1:23:456:7890::bc:def:d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x00bc, 0x0102, 0x0304}] == Generic.parse(["1:23:456:7890::bc:1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0def, 0xd34d}] == Generic.parse(["1:23:456:7890::def:d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0102, 0x0304}] == Generic.parse(["1:23:456:7890::1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0xd34d}] == Generic.parse(["1:23:456:7890::d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["1:23:456:7890::"])
+ end
+
+ test "IPv6 with :: at position 5" do
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0def, 0xd34d}] == Generic.parse(["1:23:456:7890:a::def:d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0102, 0x0304}] == Generic.parse(["1:23:456:7890:a::1.2.3.4"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0000, 0xd34d}] == Generic.parse(["1:23:456:7890:a::d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x0000, 0x0000, 0x0000}] == Generic.parse(["1:23:456:7890:a::"])
+ end
+
+ test "IPv6 with :: at position 6" do
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0000, 0xd34d}] == Generic.parse(["1:23:456:7890:a:bc::d34d"])
+ assert [{0x0001, 0x023, 0x0456, 0x7890, 0x000a, 0x00bc, 0x0000, 0x0000}] == Generic.parse(["1:23:456:7890:a:bc::"])
+ end
+
+ test "IPv6 with leading zeroes" do
+ assert [{0x0000, 0x0001, 0x0002, 0x0003, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["0:01:002:0003:0000::"])
+ assert [{0x000a, 0x001a, 0x002a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["0a:01a:002a::"])
+ assert [{0x00ab, 0x01ab, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["0ab:01ab::"])
+ assert [{0x0abc, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}] == Generic.parse(["0abc::"])
+ end
+
+ test "IPv6 with mixed case" do
+ assert [{0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd}] == Generic.parse(["abcd:abcD:abCd:abCD:aBcd:aBcD:aBCd:aBCD"])
+ assert [{0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd, 0xabcd}] == Generic.parse(["Abcd:AbcD:AbCd:AbCD:ABcd:ABcD:ABCd:ABCD"])
+ end
+
+ test "commas with optional whitespace" do
+ assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse(["127.0.0.1,::1"])
+ assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse(["127.0.0.1,\s::1"])
+ assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse(["127.0.0.1\s,::1"])
+ assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse(["127.0.0.1\s,\s::1"])
+ assert [{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 1}] == Generic.parse(["\s\t\s\t127.0.0.1\t\t\s\s,\s\t\t\s::1\t"])
+ end
+
+ test "multiple headers" do
+ headers = [
+ "1.1.1.1, unknown, 2.2.2.2",
+ " 3.3.3.3 , 4.4.4.4,not_an_ip",
+ "5.5.5.5,::6:6:6:6",
+ "unknown,5,7.7.7.7",
+ ]
+
+ ips = [
+ {1, 1, 1, 1},
+ {2, 2, 2, 2},
+ {3, 3, 3, 3},
+ {4, 4, 4, 4},
+ {5, 5, 5, 5},
+ {0, 0, 0, 0, 6, 6, 6, 6},
+ {7, 7, 7, 7},
+ ]
+
+ assert ips == Generic.parse(headers)
+ end
+ end
+end
diff --git a/test/remote_ip_test.exs b/test/remote_ip_test.exs
new file mode 100644
index 0000000..92d444f
--- /dev/null
+++ b/test/remote_ip_test.exs
@@ -0,0 +1,301 @@
+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}]}
+ 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 custom(conn, ip), do: add_req_header(conn, "custom", ip)
+
+ def remote_ip(conn, opts \\ []) do
+ RemoteIp.call(conn, RemoteIp.init(opts)).remote_ip
+ 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])
+ end
+
+ describe "one hop" 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])
+ end
+
+ test "from a loopback IP" do
+ assert :peer == @conn |> forwarded("for=127.0.0.1") |> remote_ip
+ assert :peer == @conn |> x_forwarded_for("::1") |> remote_ip
+ assert :peer == @conn |> custom("127.0.0.2") |> remote_ip(headers: ~w[custom])
+ end
+
+ test "from a private IP" do
+ assert :peer == @conn |> forwarded("for=10.0.0.1") |> remote_ip
+ assert :peer == @conn |> x_forwarded_for("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])
+ 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_forwarded_for("::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])
+ 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_forwarded_for("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])
+ end
+ end
+
+ describe "two hops" 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])
+ end
+
+ test "from unknown to loopback" do
+ assert :peer == @conn |> forwarded("for=_obf,for=127.0.0.1") |> remote_ip
+ assert :peer == @conn |> x_forwarded_for("unknown,::1") |> remote_ip
+ assert :peer == @conn |> custom("not_an_ip, 127.0.0.2") |> remote_ip(headers: ~w[custom])
+ end
+
+ test "from unknown to private" do
+ assert :peer == @conn |> forwarded("for=unknown,for=10.10.10.10") |> remote_ip
+ assert :peer == @conn |> x_forwarded_for("_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])
+ 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_forwarded_for("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])
+ 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_forwarded_for("_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])
+ 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])
+ 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_forwarded_for("::1, ::1") |> remote_ip
+ assert :peer == @conn |> custom("::1, 127.0.0.1") |> remote_ip(headers: ~w[custom])
+ 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_forwarded_for("::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])
+ 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])
+ 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_forwarded_for("::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])
+ 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_forwarded_for("192.168.0.4, not_an_ip") |> remote_ip
+ assert :peer == @conn |> custom("172.16.72.1, unknown") |> remote_ip(headers: ~w[custom])
+ 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_forwarded_for("192.168.1.2, ::1") |> remote_ip
+ assert :peer == @conn |> custom("172.16.0.1, 127.0.0.1") |> remote_ip(headers: ~w[custom])
+ 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_forwarded_for("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])
+ 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])
+ 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_forwarded_for("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])
+ 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_forwarded_for("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])
+ 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])
+ 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_forwarded_for("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])
+ 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_forwarded_for("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])
+ 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])
+ 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_forwarded_for("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])
+ 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_forwarded_for("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])
+ 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])
+ 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_forwarded_for("::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])
+ 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_forwarded_for("::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])
+ 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_forwarded_for("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 "header priority" 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 {1, 2, 3, 4} == conn |> remote_ip(headers: ~w[a b])
+ assert {1, 2, 3, 4} == conn |> remote_ip(headers: ~w[a c])
+ assert {1, 2, 3, 4} == conn |> remote_ip(headers: ~w[a b c])
+ assert {1, 2, 3, 4} == 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 {2, 3, 4, 5} == conn |> remote_ip(headers: ~w[b c])
+ assert {2, 3, 4, 5} == conn |> remote_ip(headers: ~w[b a c])
+ assert {2, 3, 4, 5} == 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])
+ end
+
+ test "header fallback" 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)
+
+ assert {1, 2, 3, 4} == @conn |> a.() |> remote_ip(headers: headers)
+ assert {1, 2, 3, 4} == @conn |> a.() |> b.() |> remote_ip(headers: headers)
+ assert {1, 2, 3, 4} == @conn |> a.() |> c.() |> remote_ip(headers: headers)
+ assert {1, 2, 3, 4} == @conn |> a.() |> b.() |> c.() |> remote_ip(headers: headers)
+ assert {1, 2, 3, 4} == @conn |> a.() |> c.() |> b.() |> remote_ip(headers: headers)
+
+ assert {2, 3, 4, 5} == @conn |> b.() |> remote_ip(headers: headers)
+ assert {1, 2, 3, 4} == @conn |> b.() |> a.() |> remote_ip(headers: headers)
+ assert {2, 3, 4, 5} == @conn |> b.() |> c.() |> remote_ip(headers: headers)
+ assert {1, 2, 3, 4} == @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} == @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 {1, 2, 3, 4} == @conn |> c.() |> a.() |> b.() |> remote_ip(headers: headers)
+ assert {1, 2, 3, 4} == @conn |> c.() |> b.() |> a.() |> remote_ip(headers: headers)
+ end
+end
diff --git a/test/test_helper.exs b/test/test_helper.exs
new file mode 100644
index 0000000..4b8b246
--- /dev/null
+++ b/test/test_helper.exs
@@ -0,0 +1 @@
+ExUnit.start

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 30, 5:48 AM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41376
Default Alt Text
(64 KB)

Event Timeline