Full Code of ajvondrak/remote_ip for AI

main da061f05feea cached
71 files
195.7 KB
71.2k tokens
176 symbols
1 requests
Download .txt
Showing preview only (214K chars total). Download the full file or copy to clipboard to get everything.
Repository: ajvondrak/remote_ip
Branch: main
Commit: da061f05feea
Files: 71
Total size: 195.7 KB

Directory structure:
gitextract_gtwens9r/

├── .formatter.exs
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .tool-versions
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bench/
│   ├── .formatter.exs
│   ├── .gitignore
│   ├── README.md
│   ├── check.exs
│   ├── lib/
│   │   └── bench/
│   │       └── inputs.ex
│   ├── mix.exs
│   └── parse.exs
├── coveralls.json
├── extras/
│   └── algorithm.md
├── integration/
│   ├── tests/
│   │   ├── basic/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── lib/
│   │   │   │   └── basic.ex
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── basic_test.exs
│   │   │       └── test_helper.exs
│   │   ├── custom/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── config/
│   │   │   │   └── config.exs
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── custom_test.exs
│   │   │       └── test_helper.exs
│   │   ├── debug/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── config/
│   │   │   │   └── config.exs
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── debug_test.exs
│   │   │       └── test_helper.exs
│   │   ├── parsers/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── config/
│   │   │   │   └── config.exs
│   │   │   ├── lib/
│   │   │   │   ├── parsers/
│   │   │   │   │   └── forwarding.ex
│   │   │   │   └── parsers.ex
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── parsers_test.exs
│   │   │       └── test_helper.exs
│   │   └── purge/
│   │       ├── .formatter.exs
│   │       ├── .gitignore
│   │       ├── README.md
│   │       ├── config/
│   │       │   └── config.exs
│   │       ├── mix.exs
│   │       └── test/
│   │           ├── purge_test.exs
│   │           └── test_helper.exs
│   └── tests.exs
├── lib/
│   ├── remote_ip/
│   │   ├── block.ex
│   │   ├── debugger.ex
│   │   ├── headers.ex
│   │   ├── options.ex
│   │   ├── parser.ex
│   │   └── parsers/
│   │       ├── forwarded.ex
│   │       └── generic.ex
│   └── remote_ip.ex
├── mix.exs
└── test/
    ├── .formatter.exs
    ├── remote_ip/
    │   ├── block_test.exs
    │   ├── headers_test.exs
    │   ├── options_test.exs
    │   └── parsers/
    │       ├── forwarded_test.exs
    │       └── generic_test.exs
    ├── remote_ip_test.exs
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .formatter.exs
================================================
[
  inputs: ["{mix,.formatter}.exs", "lib/**/*.ex", "integration/tests.exs"],
  line_length: 80,
  subdirectories: ["test", "integration/tests/*"]
]


================================================
FILE: .github/workflows/build.yml
================================================
name: build

on:
  workflow_dispatch:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

env:
  MIX_ENV: ci

# The approach to the CI matrix:
#
# * In general, remote_ip only maintains support for the minimum supported
#   version of Elixir, so we only need to test the versions listed in
#   https://hexdocs.pm/elixir/compatibility-and-deprecations.html
#
# * To avoid combinatorial explosion of Elixir/OTP pairs, and to err on the
#   side of caution, each Elixir version is paired with its lowest supported
#   OTP version.
#
# * Additionally, the highest Elixir version is also paired with its highest
#   supported OTP version, to cover the latest & greatest case.

jobs:
  ci:
    strategy:
      fail-fast: false
      matrix:
        include:
          - elixir: '1.12'
            otp: '23'
          - elixir: '1.13'
            otp: '23'
          - elixir: '1.14'
            otp: '23'
          - elixir: '1.15'
            otp: '24'
          - elixir: '1.16'
            otp: '24'
          - elixir: '1.16'
            otp: '26'

    name: Elixir ${{ matrix.elixir }} (OTP ${{ matrix.otp }})

    runs-on: ubuntu-20.04


    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - id: install
        name: Install Elixir
        uses: erlef/setup-beam@v1
        with:
          otp-version: ${{ matrix.otp }}
          elixir-version: ${{ matrix.elixir }}

      - name: Restore cached build
        uses: actions/cache@v3
        with:
          key: builds@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix-${{ hashFiles('mix.lock') }}
          path: |
            deps
            _build

      - name: Install dependencies
        run: mix do deps.get, deps.compile

      - name: Check formatting
        run: mix format --check-formatted

      - name: Compile
        run: mix compile --warnings-as-errors

      - name: Run unit tests
        run: mix coveralls.github
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Restore cached integrations
        uses: actions/cache@v3
        with:
          key: integrations@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix-${{ hashFiles('integration/tests/*/mix.lock') }}
          path: |
            integration/tests/*/deps
            integration/tests/*/_build

      - name: Run integration tests
        run: mix integrate

      - name: Restore cached PLTs
        uses: actions/cache@v3
        with:
          key: plts@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix-${{ hashFiles('mix.lock') }}
          path: |
            priv/plts
          restore-keys: |
            plts@elixir-${{ steps.install.outputs.elixir-version }}-otp-${{ steps.install.outputs.otp-version }}-mix-

      - name: Run dialyzer
        run: mix dialyzer


================================================
FILE: .gitignore
================================================
# 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

# Dialyzer's Persistent Lookup Tables (PLTs) cache expensive analyses, but they
# change depending on the version of Elixir/OTP, so we don't want to commit the
# outputs directly to version control. E.g., having one version's PLTs could
# mess up continuous integration on a different version.
/priv/plts/*.plt
/priv/plts/*.plt.hash


================================================
FILE: .tool-versions
================================================
elixir 1.16-otp-26
erlang 26.2.5


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

**Table Of Contents**
* [Issues](#issues)
  * [Getting the wrong IP](#getting-the-wrong-ip)
  * [Other issues](#other-issues)
* [Pull Requests](#pull-requests)
  * [General guidelines](#general-guidelines)

## Issues

### Getting the wrong IP

There are many reasons you might not be getting the `remote_ip` value you expect. Before opening an issue, enable `RemoteIp.Debugger` and reproduce your problematic request.

```elixir
config :remote_ip, debug: true
```

```console
$ mix deps.compile --force remote_ip
```

Then you should see logs like these:

```
[debug] Processing remote IP
  headers: ["x-forwarded-for"]
  parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded}
  proxies: ["1.2.0.0/16", "2.3.4.5/32"]
  clients: ["1.2.3.4/32"]
[debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
[debug] Parsing IPs from forwarding headers: [{"x-forwarded-for", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
[debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}]
[debug] {2, 3, 4, 5} is a known proxy IP
[debug] {10, 0, 0, 1} is a reserved IP
[debug] {1, 2, 3, 4} is a known client IP
[debug] Processed remote IP, found client {1, 2, 3, 4} to replace {127, 0, 0, 1}
```

This can help you narrow down the issue:

* Do you see these logs at all? If not, `RemoteIp` might not even be called by your pipeline. Try debugging your code.
* Are you getting the request headers you expect? Your particular proxies might not be sending the forwarding headers they should be.
* Did you configure `:headers` right? `RemoteIp` only pays attention to the forwarding headers you specify.
* Did you configure `:proxies` right? If you don't configure an IP as a known proxy, `RemoteIp` assumes it's a legitimate client.
* Did you configure `:clients` right? Loopback or private IPs are automatically identified as proxies. If you need to carve out exceptions, you should add the relevant IP ranges to the list of known clients.
* Are all the IPs being parsed correctly? `RemoteIp` will ignore values that it cannot parse. Either this is a bug in `RemoteIp` or a bad header.
* Are the forwarding headers in the right order? IPs are processed last-to-first to prevent spoofing. Make sure you understand [the algorithm](extras/algorithm.md).
* Are there multiple "competing" forwarding headers? The order we see the `req_headers` in the `Plug.Conn` matters for the last-to-first processing. Unfortunately, servers like [cowboy](https://github.com/ninenines/cowboy) can distort the order of incoming headers, since [Erlang maps](http://erlang.org/doc/man/maps.html) do not [preserve ordering](https://medium.com/@jlouis666/breaking-erlang-maps-1-31952b8729e6) (cf. [[1]](https://github.com/elixir-plug/plug_cowboy/blob/7bf68cd757c1a052e227112b681b77066fd84d2b/lib/plug/cowboy/conn.ex#L125-L127), [[2]](https://github.com/erlang/otp/blob/2c882ec2d504019f07104b3240a989148dfc1fa3/lib/stdlib/doc/src/maps.xml#L409)). You *might* be able to configure `RemoteIp` to avoid your particular problematic situation.

If none of the above apply, you may have found a bug in `RemoteIp`, so please go ahead and open an issue.

### Other issues

All manner of issues are welcome. However, I don't often have much time to work on open source things, so my turnaround is usually pretty slow. You can help by giving as much context as possible:

* :bug: Bugs
  * How can it be reproduced?
  * Do the logs help?
  * What was the expected behavior?
  * What was the actual behavior?
* :sparkles: Feature requests
  * What problem would it solve?
  * How would it work?
  * Why does it belong in this library?
* :question: Questions
  * Before asking why you're getting the wrong IP, do your [due diligence](#getting-the-wrong-ip).
  * The more details you can provide, the better!

## Pull Requests

If there's some bug you've fixed or feature you've implemented, contribute your changes through the usual means:

1. Fork this project.
2. Commit your changes.
3. Open a pull request.

### General guidelines

A few notes about getting your pull request accepted:

* [Write good commit messages.](https://chris.beams.io/posts/git-commit/)
> **The seven rules of a great Git commit message**
>
> > Keep in mind: [This](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [has](https://www.git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines) [all](https://github.com/torvalds/subsurface-for-dirk/blob/master/README#L92-L120) [been](http://who-t.blogspot.co.at/2009/12/on-commit-messages.html) [said](https://github.com/erlang/otp/wiki/writing-good-commit-messages) [before](https://github.com/spring-projects/spring-framework/blob/30bce7/CONTRIBUTING.md#format-commit-messages).
>
> 1. Separate subject from body with a blank line
> 2. Limit the subject line to 50 characters
> 3. Capitalize the subject line
> 4. Do not end the subject line with a period
> 5. Use the imperative mood in the subject line
> 6. Wrap the body at 72 characters
> 7. Use the body to explain *what* and *why* vs. *how*
* Keep the scope of your PR tight.
  * **Do** make sure your PR accomplishes one specific thing.
  * **Don't** make unnecessary or unrelated changes.
* Keep your history clean.
  * **Do** make sure each commit pertains conceptually to a single change.
  * **Don't** leave your commits disorganized with various works-in-progress. [Rewrite](https://git-scm.com/book/id/v2/Git-Tools-Rewriting-History) [history](https://git-rebase.io/) [if](https://programmerfriend.com/git-best-practices/) [necessary](http://justinhileman.info/article/changing-history/).
* Write a good PR description.
  * What problem are you trying to solve?
  * Who does the problem affect?
  * When did this problem happen? Is it tied to a specific version?
  * Where is the source of the issue? Is it an external project? Can you link to a relevant discussion?
  * How did you solve it?
  * Why is this the proper solution?
* Write tests, if appropriate.
* Proper documentation is appreciated.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2016 Alex Vondrak

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# RemoteIp

[![build status](https://github.com/ajvondrak/remote_ip/workflows/build/badge.svg)](https://github.com/ajvondrak/remote_ip/actions?query=workflow%3Abuild)
[![coverage status](https://coveralls.io/repos/github/ajvondrak/remote_ip/badge.svg?branch=main)](https://coveralls.io/github/ajvondrak/remote_ip?branch=main)
[![hex.pm version](https://img.shields.io/hexpm/v/remote_ip)](https://hex.pm/packages/remote_ip)

A [plug](https://github.com/elixir-lang/plug) to rewrite the [`Plug.Conn`](https://hexdocs.pm/plug/Plug.Conn.html)'s `remote_ip` based on forwarding headers.

Generic comma-separated headers like `X-Forwarded-For`, `X-Real-Ip`, and `X-Client-Ip` are all recognized, as well as the [RFC 7239](https://tools.ietf.org/html/rfc7239) `Forwarded` header. You can specify any number of forwarding headers to recognize and even configure your own parsers.

IPs are processed last-to-first to prevent IP spoofing. Loopback/private IPs are ignored by default, but known proxies & clients are configurable, so you have full control over which IPs are considered legitimate.

**If your app is not behind at least one proxy, you should not use this plug.** See [the algorithm](extras/algorithm.md) for more details.

## Installation

Add `:remote_ip` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [{:remote_ip, "~> 1.2"}]
end
```

## Usage

Add the `RemoteIp` plug to your app's plug pipeline:

```elixir
defmodule MyApp do
  use Plug.Router

  plug RemoteIp

  plug :match
  plug :dispatch

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

You can also use `RemoteIp.from/2` outside of a plug pipeline to extract the remote IP from a list of headers:

```elixir
x_headers = [{"x-forwarded-for", "1.2.3.4"}]
RemoteIp.from(x_headers)
```

See the [documentation](https://hexdocs.pm/remote_ip) for full details on usage, configuration options, and troubleshooting.

## Motivation

### Problem: Your app is behind a proxy and you want to know the original client's IP address.

[Proxies](https://en.wikipedia.org/wiki/Proxy_server) are pervasive for some purpose or another in modern HTTP infrastructure: encryption, load balancing, caching, compression, and more can be done via proxies. But a proxy makes HTTP requests appear to your app as if they came from the proxy's IP address. How is your app to know the "actual" requesting IP address (e.g., so you can geolocate a user)?

**Solution:** Many proxies prevent this information loss by adding HTTP headers to communicate the requesting client's IP address. There is no single, universal header. Though [`X-Forwarded-For`](https://en.wikipedia.org/wiki/X-Forwarded-For) is common, options include [`X-Real-IP`](http://nginx.org/en/docs/http/ngx_http_realip_module.html), [`X-Client-IP`](http://httpd.apache.org/docs/trunk/mod/mod_remoteip.html), and [others](http://stackoverflow.com/a/916157). Due to this lack of standardization, [RFC 7239](https://tools.ietf.org/html/rfc7239) defines the `Forwarded` header, fulfilling a [relevant XKCD truism](https://xkcd.com/927).

### Problem: Plug does not derive `remote_ip` from headers such as `X-Forwarded-For`.

Per the [`Plug.Conn` docs](https://hexdocs.pm/plug/Plug.Conn.html#module-request-fields):

> * `remote_ip` - the IP of the client, example: `{151, 236, 219, 228}`. This
>   field is meant to be overwritten by plugs that understand e.g. the
>   `X-Forwarded-For` header or HAProxy's PROXY protocol. It defaults to peer's
>   IP.

Note that the field is _meant_ to be overwritten. Plug does not actually do any overwriting itself. The [Cowboy changelog](https://github.com/ninenines/cowboy/blob/master/CHANGELOG.md#084) espouses a similar platform of non-involvement:

> Because each server's proxy situation differs, it is better that this function is implemented by the application directly.

**Solution:** As definitively verified in [elixir-lang/plug#319](https://github.com/elixir-lang/plug/issues/319), users are intended to hand-roll their own header parsers.

### Problem: Ain't nobody got time for that.

**Solution:** There are a handful of plugs available on [Hex](https://hex.pm). There are also the comments left in the [elixir-lang/plug#319](https://github.com/elixir-lang/plug/issues/319) thread that may give you some ideas, but I consider them to be non-starters - copying/pasting code from github comments isn't much better than hand-rolling an implementation.

### Problem: Existing solutions are incomplete and have subtle bugs.

None of the available solutions I have seen are ideal. In this sort of plug, you want:

* **Configurable headers and parsers:** With so many different headers being used, you should be able to configure the ones you need and how to parse them.
* **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`.
* **Security and correctness:** Parsing forwarding headers can be surprisingly subtle, and it's easy to open yourself up to [IP spoofing](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) vulnerabilities.

Existing packages all fail on one or more of these fronts:

* [plug\_cloudflare](https://hex.pm/packages/plug_cloudflare) - not a general purpose library, but is more secure & correct if you're specifically parsing the `CF-Connecting-IP`
* [plug\_forwarded\_peer](https://hex.pm/packages/plug_forwarded_peer) - only parses `Forwarded` and `X-Forwarded-For` (`X-Forwarded-For` takes precedence over `Forwarded`), does not parse all of RFC 7239's supported syntax correctly, vulnerable to IP spoofing
* [plug\_x\_forwarded\_for](https://hex.pm/packages/plug_x_forwarded_for) - only configurable for a single header, all headers parsed the same as `X-Forwarded-For`, vulnerable to IP spoofing
* [remote\_ip\_rewriter](https://hex.pm/packages/remote_ip_rewriter) - can only configure one header, all headers parsed the same as `X-Forwarded-For`, cannot configure loopback/private IPs as known clients
* [trusted\_proxy\_rewriter](https://hex.pm/packages/trusted_proxy_rewriter) - outdated fork of remote\_ip\_rewriter, has even more limited functionality

**Solution:** These are the sorts of things application developers should not have to worry about. `RemoteIp` aims to be the proper solution to all of these problems.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for instructions on how to open issues or pull requests.

## Prior Art

While `RemoteIp` has morphed into something distinct from the Rails middleware of the same name, the Rails code was definitely where I started. So I'd like to explicitly acknowledge the inspiration: this plug would not have been possible without poring over the existing implementation, discussions, documentation, and commits that went into the Rails code. :heart:

Required reading for anyone who wants to think way too much about forwarding headers:

* [@gingerlime](https://github.com/gingerlime)'s [blog post](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/) explaining IP spoofing
* Rails' [`RemoteIp` middleware](https://github.com/rails/rails/blob/v4.1.4/actionpack/lib/action_dispatch/middleware/remote_ip.rb)
* Rails' [tests](https://github.com/rails/rails/blob/92703a9ea5d8b96f30e0b706b801c9185ef14f0e/actionpack/test/dispatch/request_test.rb#L62)
* The extensive discussion on [rails/rails#7980](https://github.com/rails/rails/pull/7980)


================================================
FILE: bench/.formatter.exs
================================================
# Used by "mix format"
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]


================================================
FILE: bench/.gitignore
================================================
# 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 third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# 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

# Ignore package tarball (built via "mix hex.build").
bench-*.tar


# Temporary files for e.g. tests
/tmp


================================================
FILE: bench/README.md
================================================
# Benchmarks

For the purposes of remote\_ip, we need a library to (1) parse strings from CIDR notation into a usable representation for (2) checking if an IP falls within a certain block. At the time of this writing, there are a couple [CIDR libraries](https://hex.pm/packages?search=cidr) available on Hex.pm:

* [inet\_cidr](https://hex.pm/packages/inet_cidr)
* [erl\_cidr](https://hex.pm/packages/erl_cidr) (an Erlang wrapper around inet\_cidr)
* [cidr](https://hex.pm/packages/cidr)
* [cider](https://hex.pm/packages/cider)

Due to the shortcomings of these libraries, remote\_ip rolls its own `RemoteIp.Block` module. This app serves as a testing ground for comparing remote\_ip's implementation against the others to validate whether it's actually an improvement.

## Results

Using [benchee](https://github.com/bencheeorg/benchee), we run a block of code repeatedly for a static amount of time (after a warmup) and count how many iterations we got through. Thus, in the _iterations per second_ results charted below, bigger is better: it means we performed more operations in a given amount of time.

### Parsing CIDRs

This benchmark generates 1,000 random CIDR strings and measures the time it takes to parse them all with each different library.

![A data plot for the CIDR parsing benchmarks](img/parse.png)

```console
$ mix run parse.exs
Randomizing with seed 294598

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
Number of Available Cores: 8
Available memory: 16 GB
Elixir 1.11.4
Erlang 23.2.7

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 28 s

Benchmarking cider...
Benchmarking cidr...
Benchmarking inet_cidr...
Benchmarking remote_ip...
Generated tmp/parse.html
Generated tmp/parse_cider.html
Generated tmp/parse_cidr.html
Generated tmp/parse_comparison.html
Generated tmp/parse_inet_cidr.html
Generated tmp/parse_remote_ip.html

Name                ips        average  deviation         median         99th %
cider            269.94        3.70 ms     ±7.72%        3.61 ms        4.74 ms
remote_ip        264.49        3.78 ms     ±9.78%        3.70 ms        5.22 ms
inet_cidr        243.27        4.11 ms     ±9.25%        4.08 ms        5.51 ms
cidr             169.52        5.90 ms    ±11.24%        5.77 ms        7.79 ms

Comparison:
cider            269.94
remote_ip        264.49 - 1.02x slower +0.0763 ms
inet_cidr        243.27 - 1.11x slower +0.41 ms
cidr             169.52 - 1.59x slower +2.19 ms
```

### Checking IPs

This benchmark generates 1,000 random CIDRs and 1,000 randoms IPs, then uses each library to check every combination (1,000 CIDRs x 1,000 IPs = 1,000,000 checks per iteration).

To avoid conflating the performance results, we perform the CIDR parsing ahead of time and don't measure it in the actual benchmark. Similarly, both remote\_ip and cider must encodes the incoming IPs as integers, so we perform that step outside of the measurement as well.

![A data plot for the IP checking benchmarks](img/check.png)

```console
$ mix run check.exs
Randomizing with seed 570890

Operating System: macOS
CPU Information: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
Number of Available Cores: 8
Available memory: 16 GB
Elixir 1.11.4
Erlang 23.2.7

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 28 s

Benchmarking cider...
Benchmarking cidr...
Benchmarking inet_cidr...
Benchmarking remote_ip...
Generated tmp/check.html
Generated tmp/check_cider.html
Generated tmp/check_cidr.html
Generated tmp/check_comparison.html
Generated tmp/check_inet_cidr.html
Generated tmp/check_remote_ip.html

Name                ips        average  deviation         median         99th %
remote_ip         14.60       68.50 ms     ±1.43%       68.65 ms       70.67 ms
cider             10.68       93.66 ms     ±2.41%       93.86 ms       97.84 ms
inet_cidr          6.37      156.88 ms     ±2.89%      157.15 ms      165.46 ms
cidr               1.53      652.73 ms     ±0.91%      652.71 ms      661.14 ms

Comparison:
remote_ip         14.60
cider             10.68 - 1.37x slower +25.16 ms
inet_cidr          6.37 - 2.29x slower +88.38 ms
cidr               1.53 - 9.53x slower +584.23 ms
```

## Other considerations

Performance is a nice, objective nail in the coffin for these other libraries. But they also have other issues that make them impractical to use:

* inet\_cidr doesn't parse individual IP addresses, so instead of `"1.2.3.4"` you have to say `"1.2.3.4/32"`
* cidr's `CIDR.match/2` interface is awkward compared to the boolean `contains?/2` functions provided by everyone else
* cider fails to distinguish between IPv4 and IPv6 addresses, so it produces false positives like
    ```elixir
    iex> Cider.contains?({0, 0, 0, 1}, Cider.parse("::1"))
    true
    ```

remote\_ip's implementation neatly handles all of these limitations while also performing significantly better.


================================================
FILE: bench/check.exs
================================================
Bench.Inputs.seed

ips = Bench.Inputs.ips(1_000)

parsed_ips = %{
  remote_ip: Enum.map(ips, &RemoteIp.Block.encode/1),
  inet_cidr: ips,
  cider: Enum.map(ips, &Cider.ip!/1),
  cidr: ips,
}

cidrs = Bench.Inputs.cidrs(1_000)

parsed_cidrs = %{
  remote_ip: Enum.map(cidrs, &RemoteIp.Block.parse!/1),
  inet_cidr: Enum.map(cidrs, &InetCidr.parse_cidr!(&1, true)),
  cider: Enum.map(cidrs, &Cider.parse/1),
  cidr: Enum.map(cidrs, &CIDR.parse/1),
}

suite = %{
  remote_ip: fn ->
    Enum.each(parsed_ips[:remote_ip], fn ip ->
      Enum.each(parsed_cidrs[:remote_ip], &RemoteIp.Block.contains?(&1, ip))
    end)
  end,
  inet_cidr: fn ->
    Enum.each(parsed_ips[:inet_cidr], fn ip ->
      Enum.each(parsed_cidrs[:inet_cidr], &InetCidr.contains?(&1, ip))
    end)
  end,
  cider: fn ->
    Enum.each(parsed_ips[:cider], fn ip ->
      Enum.each(parsed_cidrs[:cider], &Cider.contains?(ip, &1))
    end)
  end,
  cidr: fn ->
    Enum.each(parsed_ips[:cidr], fn ip ->
      Enum.each(parsed_cidrs[:cidr], &CIDR.match(&1, ip))
    end)
  end,
}

formatters = [
  {Benchee.Formatters.HTML, file: "tmp/check.html", auto_open: false},
  Benchee.Formatters.Console,
]

Benchee.run(suite, formatters: formatters)


================================================
FILE: bench/lib/bench/inputs.ex
================================================
defmodule Bench.Inputs do
  def seed do
    seed =
      case System.fetch_env("SEED") do
        {:ok, var} -> String.to_integer(var)
        :error -> System.system_time(:microsecond) |> rem(1_000_000)
      end

    IO.puts("Randomizing with seed #{seed}\n")
    :rand.seed(:exs1024, {seed, seed, seed})
  end

  def cidrs(n) do
    Stream.repeatedly(fn -> cidr() end) |> Enum.take(n)
  end

  def cidr do
    case Enum.random([:ipv4, :ipv6]) do
      :ipv4 -> cidr(ipv4())
      :ipv6 -> cidr(ipv6())
    end
  end

  def cidr({a, b, c, d}) do
    cidr({a, b, c, d}, Enum.random(0..32))
  end

  def cidr({a, b, c, d, e, f, g, h}) do
    cidr({a, b, c, d, e, f, g, h}, Enum.random(0..128))
  end

  def cidr(ip, prefix) do
    "#{:inet.ntoa(ip)}/#{prefix}"
  end

  def ips(n) do
    Stream.repeatedly(fn -> ip() end) |> Enum.take(n)
  end

  def ip do
    case Enum.random([:ipv4, :ipv6]) do
      :ipv4 -> ipv4()
      :ipv6 -> ipv6()
    end
  end

  def ipv4 do
    Stream.repeatedly(fn -> Enum.random(0..255) end)
    |> Enum.take(4)
    |> List.to_tuple()
  end

  def ipv6 do
    Stream.repeatedly(fn -> Enum.random(0..0xFFFF) end)
    |> Enum.take(8)
    |> List.to_tuple()
  end
end


================================================
FILE: bench/mix.exs
================================================
defmodule Bench.MixProject do
  use Mix.Project

  def project do
    [
      app: :bench,
      version: "0.0.0",
      elixir: "~> 1.12",
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp deps do
    [
      {:benchee, "~> 1.3"},
      {:benchee_html, "~> 1.0"},
      {:inet_cidr, "~> 1.0"},
      {:cidr, "~> 1.0"},
      {:cider, "~> 0.3"},
      {:remote_ip, path: ".."}
    ]
  end
end


================================================
FILE: bench/parse.exs
================================================
Bench.Inputs.seed

cidrs = Bench.Inputs.cidrs(1_000)

suite = %{
  remote_ip: fn -> Enum.each(cidrs, &RemoteIp.Block.parse!/1) end,
  inet_cidr: fn -> Enum.each(cidrs, &InetCidr.parse_cidr!(&1, true)) end,
  cider: fn -> Enum.each(cidrs, &Cider.parse/1) end,
  cidr: fn -> Enum.each(cidrs, &CIDR.parse/1) end,
}

formatters = [
  {Benchee.Formatters.HTML, file: "tmp/parse.html", auto_open: false},
  Benchee.Formatters.Console,
]

Benchee.run(suite, formatters: formatters)


================================================
FILE: coveralls.json
================================================
{
  "skip_files": [
    "lib/remote_ip/debugger.ex"
  ],
  "coverage_options": {
    "minimum_coverage": 100
  }
}


================================================
FILE: extras/algorithm.md
================================================
# Algorithm

To avoid IP spoofing vulnerabilities, `RemoteIp` employs a very particular algorithm. Its work is divided into two main phases:

1. Parse the right `req_headers`.
2. Compute the right `remote_ip`.

We will analyze these steps in detail to understand the benefits and caveats of the algorithm. Much of it relies on configuration values given by `RemoteIp.Options`. You may also log the steps of this algorithm using the `RemoteIp.Debugger`.

As a running example, consider the following request route:

* Client at IP `1.2.3.4` sends an HTTP request to Proxy 1 (no forwarding headers)
* Proxy 1 at IP `1.1.1.1` adds an `X-Forwarded-For: 1.2.3.4` header and forwards to Proxy 2
* Proxy 2 at IP `2.2.2.2` adds Proxy 1 to the header with `X-Forwarded-For: 1.2.3.4, 1.1.1.1` and forwards to Proxy 3
* Proxy 3 at IP `3.3.3.3` adds a `Forwarded: for=2.2.2.2` header and forwards to the application
* Application receives the request from IP `3.3.3.3` with forwarding headers `X-Forwarded-For: 1.2.3.4, 1.1.1.1` and `Forwarded: for=2.2.2.2`

## Parsing headers

There are many different forwarding headers in the wild, including `Forwarded`, `X-Forwarded-For`, `X-Client-Ip`, and `X-Real-Ip`. The header that gets used depends on the configuration of the proxy your app sits behind. If there are multiple proxies in play, it's conceivable for you to have more than one such header.

The `:headers` option tells `RemoteIp` which specific headers to parse for IP addresses. The default value casts a wide net, but you should ideally specify only those headers which you're certain you require. Otherwise, it would be trivial for a malicious client to add an extra header that could interfere with finding the correct IP.

To start off the algorithm, all of the configured headers are taken from the `Plug.Conn`'s `req_headers`. Their relative ordering is maintained, because order matters when there are multiple hops between proxies. In our running example, assuming both `Forwarded` and `X-Forwarded-For` were in the `:headers` option (which they are by default), we want to process the list

```elixir
# This is what we want
[{"x-forwarded-for", "1.2.3.4, 1.1.1.1"}, {"forwarded", "for=2.2.2.2"}]
```

and *not* the reverse

```elixir
# This is NOT what we want
[{"forwarded", "for=2.2.2.2"}, {"x-forwarded-for", "1.2.3.4, 1.1.1.1"}]
```

Let's assume we get the former. In reality, however, we usually can't rely on the stable ordering of headers in an HTTP request. For example, the [Cowboy](https://github.com/ninenines/cowboy/) server presently [uses maps](https://github.com/elixir-plug/plug_cowboy/blob/f82f2ff982f04fb4faa3a12fd2b08a7cc56ebe15/lib/plug/cowboy/conn.ex#L125-L127) to represent headers, which don't preserve key order, so everything could get jumbled up.

Configuring multiple headers might still be useful if, for example, you expect some requests to only have header A and other requests to only have header B, but never both at the same time. So `RemoteIp` doesn't limit you to just the one choice.

After selecting the allowed headers, each string is parsed for its IP addresses. In the common case, we parse comma-separated IPs with `RemoteIp.Parsers.Generic`. This works for headers like `X-Forwarded-For`, `X-Client-Ip` and `X-Real-Ip`. But you can also configure custom parsers using the `:parsers` option. For instance, by default we include `RemoteIp.Parsers.Forwarded` to parse the format specified by RFC 7239.

Each parser returns a list of IPs, each of the [`:inet.ip_address/0` type](http://erlang.org/doc/man/inet.html#type-ip_address). If there were any errors (e.g., a malformed header), this should be an empty list. But any one header may also specify multiple IPs, so once again it's important that the relative order is maintained. Thus, in our running example, the `X-Forwarded-For` header should parse as

```elixir
# This is what we want
[{1, 2, 3, 4}, {1, 1, 1, 1}]
```

and *not* another order like

```elixir
# This is NOT what we want
[{1, 1, 1, 1}, {1, 2, 3, 4}]
```

The lists returned by each parser are then concatenated together to form one chain of IPs. In our running example, the resulting addresses are

```elixir
[{1, 2, 3, 4}, {1, 1, 1, 1}, {2, 2, 2, 2}]
```

## Finding the client

With the list of IPs parsed, `RemoteIp` must then calculate the proper `remote_ip`. To [prevent IP spoofing](http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/), IPs are processed right-to-left. You can think of it as going backwards through the chain of hops:

1. Application is receiving a request from Proxy 3
2. The Proxy 3 to Application hop set the header `Forwarded: for=2.2.2.2`
3. The Proxy 2 to Proxy 3 hop added `1.1.1.1` to the `X-Forwarded-For` header
4. The Proxy 1 to Proxy 2 hop set the `X-Forwarded-For: 1.2.3.4` header
5. Client is sending a request to Proxy 1

We work backwards until we find something that looks like a client IP. This is dictated by the `:proxies` option, which configures the list of known proxy IPs. Any IP that is *not* a known proxy is assumed to be a client. In our running example:

1. The peer address `{3, 3, 3, 3}` is automatically assumed to be a proxy IP, so go through the headers
2. `{2, 2, 2, 2}` is a known proxy IP, so go one hop back
3. `{1, 1, 1, 1}` is a known proxy IP, so go one hop back
4. `{1, 2, 3, 4}` is not a known proxy IP, so we assume it's the client

Notice that the peer address is *always* assumed to be "wrong". Therefore, you should not use `RemoteIp` unless your app is behind at least one proxy. Otherwise, it would be trivial for a malicious client to spoof their IP address: if they just set a header themselves, we'll automatically use it to rewrite the `Plug.Conn`'s original (correct) peer address.

It's also important to go *backwards* through the chain, or else the client could similarly spoof their IP. For instance, consider if the client in the running example had initially sent the header `Forwarded: for=6.7.8.9`. Then the headers would have come in as

```elixir
[{"forwarded", "for=6.7.8.9"}, {"x-forwarded-for", "1.2.3.4, 1.1.1.1"}, {"forwarded", "for=2.2.2.2"}]
```

which would parse out as the IPs

```elixir
[{6, 7, 8, 9}, {1, 2, 3, 4}, {1, 1, 1, 1}, {2, 2, 2, 2}]
```

If we were to go *forwards* through this list, we'd immediately return `{6, 7, 8, 9}` as the client IP, even though it was being spoofed by our malicious user. Instead, going backwards still gives us `{1, 2, 3, 4}` even though the client is attempting to spoof the IP with their own headers. This works no matter how many extra headers the client sends.

This logic generalizes to any bad actors in the middle of the chain, too. If we add an IP to the `:proxies` list, we're trusting the forwarding headers that it sets. As such, we're implicitly trusting the incoming peer address, even without configuring it. So in our running example, it's impossible not to trust Proxy 3. We believe it when it says the request came from Proxy 2. But if we didn't trust Proxy 2, that's where we stop: we say the client is `{2, 2, 2, 2}` and won't dig further because we don't trust the `X-Forwarded-For` header that came from Proxy 2.

Not only are known proxies' headers trusted, but also requests forwarded for [loopback](https://en.wikipedia.org/wiki/Loopback) and [private](https://en.wikipedia.org/wiki/Private_network) IPs:

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

These IPs are skipped automatically 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. This goes for anything you have listed in `:proxies` as well. For example, you might say that a whole CIDR block belongs to proxies, but then carve out an exception for a single client in that block.


================================================
FILE: integration/tests/basic/.formatter.exs
================================================
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
  import_deps: [:plug]
]


================================================
FILE: integration/tests/basic/.gitignore
================================================
# 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 third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# 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

# Ignore package tarball (built via "mix hex.build").
basic-*.tar


# Temporary files for e.g. tests
/tmp


================================================
FILE: integration/tests/basic/README.md
================================================
# Basic integration test

This app plugs `RemoteIp` into a simple `Plug.Router` pipeline. Sans configuration, we expect remote\_ip to have been compiled without any debug logs. An app this straightforward serves as a basic smoke test for standard usage of the plug.


================================================
FILE: integration/tests/basic/lib/basic.ex
================================================
defmodule Basic do
  use Plug.Router

  plug RemoteIp
  plug :match
  plug :dispatch

  get "/ip" do
    send_resp(conn, 200, :inet.ntoa(conn.remote_ip))
  end
end


================================================
FILE: integration/tests/basic/mix.exs
================================================
defmodule Basic.MixProject do
  use Mix.Project

  def project do
    [
      app: :basic,
      version: "0.0.0",
      elixir: "~> 1.12",
      deps: [remote_ip: [path: "../../.."]]
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end
end


================================================
FILE: integration/tests/basic/test/basic_test.exs
================================================
defmodule BasicTest do
  use ExUnit.Case
  use Plug.Test

  import ExUnit.CaptureLog

  def xff(conn, header) do
    put_req_header(conn, "x-forwarded-for", header)
  end

  def call(conn, opts \\ []) do
    Basic.call(conn, Basic.init(opts))
  end

  test "GET /ip" do
    conn = conn(:get, "/ip") |> xff("1.2.3.4,2.3.4.5,127.0.0.1")
    assert "" == capture_log(fn -> assert call(conn).resp_body == "2.3.4.5" end)
  end
end


================================================
FILE: integration/tests/basic/test/test_helper.exs
================================================
ExUnit.start()


================================================
FILE: integration/tests/custom/.formatter.exs
================================================
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]


================================================
FILE: integration/tests/custom/.gitignore
================================================
# 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 third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# 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

# Ignore package tarball (built via "mix hex.build").
custom-*.tar


# Temporary files for e.g. tests
/tmp


================================================
FILE: integration/tests/custom/README.md
================================================
# Custom integration test

This app customizes the subset of debug messages it wants remote\_ip to actually log. All the others should be removed at compile time. We test this by directly calling `RemoteIp.call/2` & `RemoteIp.from/2` to inspect their log output.

This doesn't enumerate all the possible customizations or anything, but it does provide a smoke test. Here, we log the parsed IPs and the resulting remote IP.

```elixir
config :remote_ip, debug: [:ips, :ip]
```


================================================
FILE: integration/tests/custom/config/config.exs
================================================
import Config

config :logger, :console,
  colors: [enabled: false],
  format: "[$level] $message\n"

config :remote_ip, debug: [:ips, :ip]


================================================
FILE: integration/tests/custom/mix.exs
================================================
defmodule Custom.MixProject do
  use Mix.Project

  def project do
    [
      app: :custom,
      version: "0.0.0",
      elixir: "~> 1.12",
      deps: [remote_ip: [path: "../../.."]]
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end
end


================================================
FILE: integration/tests/custom/test/custom_test.exs
================================================
defmodule CustomTest do
  use ExUnit.Case
  import ExUnit.CaptureLog

  @head [
    {"user-agent", "test"},
    {"x-forwarded-for", "3.14.15.9, 26.53.58.97, 93.238.46.26"}
  ]

  @conn %Plug.Conn{
    remote_ip: {127, 0, 0, 1},
    req_headers: @head
  }

  def call(conn, opts \\ []) do
    RemoteIp.call(conn, RemoteIp.init(opts))
  end

  def from(head, opts \\ []) do
    RemoteIp.from(head, opts)
  end

  test "hit with RemoteIp.call/2" do
    assert capture_log(fn -> call(@conn) end) == """
           [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}, {26, 53, 58, 97}, {93, 238, 46, 26}]
           [debug] Processed remote IP, found client {93, 238, 46, 26} to replace {127, 0, 0, 1}
           """
  end

  test "miss with RemoteIp.call/2" do
    assert capture_log(fn -> call(@conn, headers: []) end) == """
           [debug] Parsed IPs from forwarding headers: []
           [debug] Processed remote IP, no client found to replace {127, 0, 0, 1}
           """
  end

  test "hit with RemoteIp.from/2" do
    assert capture_log(fn -> from(@head) end) == """
           [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}, {26, 53, 58, 97}, {93, 238, 46, 26}]
           [debug] Processed remote IP, found client {93, 238, 46, 26}
           """
  end

  test "miss with RemoteIp.from/2" do
    assert capture_log(fn -> from(@head, headers: []) end) == """
           [debug] Parsed IPs from forwarding headers: []
           [debug] Processed remote IP, no client found
           """
  end
end


================================================
FILE: integration/tests/custom/test/test_helper.exs
================================================
ExUnit.start()


================================================
FILE: integration/tests/debug/.formatter.exs
================================================
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]


================================================
FILE: integration/tests/debug/.gitignore
================================================
# 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 third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# 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

# Ignore package tarball (built via "mix hex.build").
debug-*.tar


# Temporary files for e.g. tests
/tmp


================================================
FILE: integration/tests/debug/README.md
================================================
# Debug integration test

This app compiles remote\_ip with the configuration

```elixir
config :remote_ip, debug: true
```

and calls `RemoteIp.call/2` & `RemoteIp.from/2` directly to inspect their debug logs on some basic examples. This gives us coverage on all the possible types of debug messages across the remote\_ip codebase.


================================================
FILE: integration/tests/debug/config/config.exs
================================================
import Config

config :logger, :console,
  colors: [enabled: false],
  format: "[$level] $message\n"

config :remote_ip, debug: true


================================================
FILE: integration/tests/debug/mix.exs
================================================
defmodule Debug.MixProject do
  use Mix.Project

  def project do
    [
      app: :debug,
      version: "0.0.0",
      elixir: "~> 1.12",
      deps: [remote_ip: [path: "../../.."]]
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end
end


================================================
FILE: integration/tests/debug/test/debug_test.exs
================================================
defmodule DebugTest do
  use ExUnit.Case

  import ExUnit.CaptureLog

  @head [
    {"accept", "*/*"},
    {"x-forwarded-for", "3.14.15.9"},
    {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}
  ]

  @conn %Plug.Conn{
    remote_ip: {127, 0, 0, 1},
    req_headers: @head
  }

  def call(opts) do
    RemoteIp.call(@conn, RemoteIp.init(opts))
  end

  def from(opts) do
    RemoteIp.from(@head, opts)
  end

  describe "RemoteIp.call/2" do
    test "no client" do
      opts = [
        headers: ~w[xff],
        proxies: ~w[1.2.0.0/16 2.3.4.5/32],
        clients: ~w[]
      ]

      assert capture_log(fn -> call(opts) end) == """
             [debug] Processing remote IP
               headers: ["xff"]
               parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded}
               proxies: ["1.2.0.0/16", "2.3.4.5/32"]
               clients: []
             [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsing IPs from forwarding headers: [{"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}]
             [debug] {2, 3, 4, 5} is a known proxy IP
             [debug] {10, 0, 0, 1} is a reserved IP
             [debug] {1, 2, 3, 4} is a known proxy IP
             [debug] Processed remote IP, no client found to replace {127, 0, 0, 1}
             """
    end

    test "known client" do
      opts = [
        headers: ~w[xff],
        proxies: ~w[1.2.0.0/16 2.3.4.5/32],
        clients: ~w[1.2.3.4/32]
      ]

      assert capture_log(fn -> call(opts) end) == """
             [debug] Processing remote IP
               headers: ["xff"]
               parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded}
               proxies: ["1.2.0.0/16", "2.3.4.5/32"]
               clients: ["1.2.3.4/32"]
             [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsing IPs from forwarding headers: [{"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}]
             [debug] {2, 3, 4, 5} is a known proxy IP
             [debug] {10, 0, 0, 1} is a reserved IP
             [debug] {1, 2, 3, 4} is a known client IP
             [debug] Processed remote IP, found client {1, 2, 3, 4} to replace {127, 0, 0, 1}
             """
    end

    test "assumed client" do
      opts = [
        headers: ~w[xff],
        proxies: ~w[2.3.4.5/32],
        clients: ~w[]
      ]

      assert capture_log(fn -> call(opts) end) == """
             [debug] Processing remote IP
               headers: ["xff"]
               parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded}
               proxies: ["2.3.4.5/32"]
               clients: []
             [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsing IPs from forwarding headers: [{"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}]
             [debug] {2, 3, 4, 5} is a known proxy IP
             [debug] {10, 0, 0, 1} is a reserved IP
             [debug] {1, 2, 3, 4} is an unknown IP, assuming it's the client
             [debug] Processed remote IP, found client {1, 2, 3, 4} to replace {127, 0, 0, 1}
             """
    end
  end

  describe "RemoteIp.from/2" do
    test "no client" do
      opts = [
        headers: ~w[],
        proxies: ~w[1.2.0.0/16 2.3.4.5/32],
        clients: ~w[1.0.0.0/8 2.0.0.0/8 3.0.0.0/8]
      ]

      assert capture_log(fn -> from(opts) end) == """
             [debug] Processing remote IP
               headers: []
               parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded}
               proxies: ["1.2.0.0/16", "2.3.4.5/32"]
               clients: ["1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8"]
             [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsing IPs from forwarding headers: []
             [debug] Parsed IPs from forwarding headers: []
             [debug] Processed remote IP, no client found
             """
    end

    test "known client" do
      opts = [
        headers: ~w[x-forwarded-for],
        proxies: ~w[1.2.0.0/16 2.3.4.5/32],
        clients: ~w[3.0.0.0/8]
      ]

      assert capture_log(fn -> from(opts) end) == """
             [debug] Processing remote IP
               headers: ["x-forwarded-for"]
               parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded}
               proxies: ["1.2.0.0/16", "2.3.4.5/32"]
               clients: ["3.0.0.0/8"]
             [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsing IPs from forwarding headers: [{"x-forwarded-for", "3.14.15.9"}]
             [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}]
             [debug] {3, 14, 15, 9} is a known client IP
             [debug] Processed remote IP, found client {3, 14, 15, 9}
             """
    end

    test "assumed client" do
      opts = [
        headers: ~w[x-forwarded-for xff],
        proxies: ~w[2.3.4.5/32 3.0.0.0/8],
        clients: ~w[]
      ]

      assert capture_log(fn -> from(opts) end) == """
             [debug] Processing remote IP
               headers: ["x-forwarded-for", "xff"]
               parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded}
               proxies: ["2.3.4.5/32", "3.0.0.0/8"]
               clients: []
             [debug] Taking forwarding headers from [{"accept", "*/*"}, {"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsing IPs from forwarding headers: [{"x-forwarded-for", "3.14.15.9"}, {"xff", "1.2.3.4, 10.0.0.1, 2.3.4.5"}]
             [debug] Parsed IPs from forwarding headers: [{3, 14, 15, 9}, {1, 2, 3, 4}, {10, 0, 0, 1}, {2, 3, 4, 5}]
             [debug] {2, 3, 4, 5} is a known proxy IP
             [debug] {10, 0, 0, 1} is a reserved IP
             [debug] {1, 2, 3, 4} is an unknown IP, assuming it's the client
             [debug] Processed remote IP, found client {1, 2, 3, 4}
             """
    end
  end
end


================================================
FILE: integration/tests/debug/test/test_helper.exs
================================================
ExUnit.start()


================================================
FILE: integration/tests/parsers/.formatter.exs
================================================
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
  import_deps: [:plug]
]


================================================
FILE: integration/tests/parsers/.gitignore
================================================
# 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 third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# 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

# Ignore package tarball (built via "mix hex.build").
parsers-*.tar


# Temporary files for e.g. tests
/tmp


================================================
FILE: integration/tests/parsers/README.md
================================================
# Parsers integration test

This app recognizes a custom header named `"forwarding"` which is parsed with the app's own custom implementation of the `RemoteIp.Parser` behaviour.

The header is completely made up. Its format is

```
type=ip
```

where

* `type` is either `proxy` or `client`
* `ip` is a valid IP address

Of course, you wouldn't expect to rely on the header _telling_ you if an IP was a proxy. Bad actors could easily spoof the header (at least if it's plaintext like this). In the real world, you'd configure the `RemoteIp` plug. But this format makes for more interesting tests.

This is an integration test so that we can compile remote\_ip with debugging enabled, then make sure that the custom `:parsers` option gets logged.


================================================
FILE: integration/tests/parsers/config/config.exs
================================================
import Config

config :logger, :console,
  colors: [enabled: false],
  format: "[$level] $message\n"

config :remote_ip, debug: [:options, :ips]


================================================
FILE: integration/tests/parsers/lib/parsers/forwarding.ex
================================================
defmodule Parsers.Forwarding do
  @behaviour RemoteIp.Parser

  @impl RemoteIp.Parser

  def parse(value) do
    [type, address] = String.split(value, "=")

    case type do
      "proxy" ->
        []

      "client" ->
        {:ok, ip} = :inet.parse_strict_address(address |> to_charlist())
        [ip]
    end
  end
end


================================================
FILE: integration/tests/parsers/lib/parsers.ex
================================================
defmodule Parsers do
  use Plug.Router

  plug RemoteIp,
    headers: ~w[forwarding],
    parsers: %{"forwarding" => Parsers.Forwarding}

  plug :match
  plug :dispatch

  get "/ip" do
    send_resp(conn, 200, :inet.ntoa(conn.remote_ip))
  end
end


================================================
FILE: integration/tests/parsers/mix.exs
================================================
defmodule Parsers.MixProject do
  use Mix.Project

  def project do
    [
      app: :parsers,
      version: "0.0.0",
      elixir: "~> 1.12",
      deps: [remote_ip: [path: "../../.."]]
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end
end


================================================
FILE: integration/tests/parsers/test/parsers_test.exs
================================================
defmodule ParsersTest do
  use ExUnit.Case
  use Plug.Test

  import ExUnit.CaptureLog

  def call(conn, opts \\ []) do
    Parsers.call(conn, Parsers.init(opts))
  end

  test "GET /ip" do
    head = [
      {"forwarding", "client=1.2.3.4"},
      {"forwarding", "proxy=10.20.30.40"},
      {"forwarding", "client=2.3.4.5"},
      {"forwarding", "proxy=20.30.40.50"}
    ]

    conn = %{conn(:get, "/ip") | req_headers: head}

    logs = capture_log(fn -> assert call(conn).resp_body == "2.3.4.5" end)

    assert logs == """
           [debug] Processing remote IP
             headers: ["forwarding"]
             parsers: %{"forwarded" => RemoteIp.Parsers.Forwarded, "forwarding" => Parsers.Forwarding}
             proxies: []
             clients: []
           [debug] Parsed IPs from forwarding headers: [{1, 2, 3, 4}, {2, 3, 4, 5}]
           """
  end
end


================================================
FILE: integration/tests/parsers/test/test_helper.exs
================================================
ExUnit.start()


================================================
FILE: integration/tests/purge/.formatter.exs
================================================
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]


================================================
FILE: integration/tests/purge/.gitignore
================================================
# 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 third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# 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

# Ignore package tarball (built via "mix hex.build").
purge-*.tar


# Temporary files for e.g. tests
/tmp


================================================
FILE: integration/tests/purge/README.md
================================================
# Purge integration test

This app enables remote\_ip debugging, just like the [debug](../debug) integration test. However, it also adjusts the `Logger` configuration to make sure that the [`:compile_time_purge_matching` option](https://hexdocs.pm/logger/1.11.3/Logger.html#module-application-configuration) still works when remote\_ip gets recompiled.

Specifically, we purge messages with a level lower than `:info`, which includes the `:debug` messages that `RemoteIp.Debugger` generates. This means that when we compile remote\_ip, none of the debug statements should survive. Even when we set the log level to `:debug` at runtime in the tests, the logs should have been purged at compile-time.

This is an important regression to test because older versions of remote\_ip instructed people to disable debug logs using `:compile_time_purge_matching` (cf. [`4512fe5`](https://github.com/ajvondrak/remote_ip/commit/4512fe53cd2b9c2e03924b12961e48a1ff5b0299)), so we should make an effort to ensure their configurations keep working. Of course, it's still possible they used a `:module`/`:function` matcher that is no longer relevant due to the changing internals of the remote\_ip code. But that's on them for matching against private implementation details. 🙃


================================================
FILE: integration/tests/purge/config/config.exs
================================================
import Config

config :logger, :console,
  format: "[$level] $message\n",
  colors: [enabled: false]

config :logger, compile_time_purge_matching: [[level_lower_than: :info]]

config :remote_ip, debug: true


================================================
FILE: integration/tests/purge/mix.exs
================================================
defmodule Purge.MixProject do
  use Mix.Project

  def project do
    [
      app: :purge,
      version: "0.0.0",
      elixir: "~> 1.12",
      deps: [remote_ip: [path: "../../.."]]
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end
end


================================================
FILE: integration/tests/purge/test/purge_test.exs
================================================
defmodule PurgeTest do
  use ExUnit.Case

  import ExUnit.CaptureLog

  @head [{"x-forwarded-for", "3.14.15.9"}]

  @conn %Plug.Conn{
    remote_ip: {127, 0, 0, 1},
    req_headers: @head
  }

  def call(conn, opts \\ []) do
    RemoteIp.call(conn, RemoteIp.init(opts))
  end

  def from(head, opts \\ []) do
    RemoteIp.from(head, opts)
  end

  test "logs get purged at compile time" do
    Logger.configure(level: :debug)
    assert capture_log(fn -> call(@conn) end) == ""
    assert capture_log(fn -> from(@head) end) == ""
  end
end


================================================
FILE: integration/tests/purge/test/test_helper.exs
================================================
ExUnit.start()


================================================
FILE: integration/tests.exs
================================================
defmodule Integration.Tests do
  @path Path.join(__DIR__, "tests")

  if IO.ANSI.enabled?() do
    @color "--color"
  else
    @color "--no-color"
  end

  def run do
    File.ls!(@path) |> Enum.map(&run/1) |> summarize()
  end

  def run(app) do
    IO.puts("---> Running integration tests on #{app} app")

    with 0 <- mix(app, "deps.clean", ["--build", "remote_ip"]),
         0 <- mix(app, "deps.get"),
         0 <- mix(app, "test", [@color]) do
      {app, :pass}
    else
      _ -> {app, :fail}
    end
  end

  def mix(app, task, args \\ []) do
    cmd = [task | args]
    dir = Path.expand(app, @path)
    out = IO.binstream(:stdio, :line)

    IO.puts(["-->", "mix" | cmd] |> Enum.join(" "))
    {_, status} = System.cmd("mix", cmd, cd: dir, into: out)
    status
  end

  def summarize(results) do
    count(results)

    Enum.each(results, fn
      {app, :pass} -> passed(app)
      {app, :fail} -> failed(app)
    end)
  end

  def count(results) do
    tests = length(results)
    fails = Enum.count(results, fn {_, flag} -> flag == :fail end)
    msg = [plural(tests, "integration test"), ", ", plural(fails, "failure")]

    IO.puts("")

    if fails > 0 do
      IO.ANSI.format([:red | msg]) |> IO.puts()
    else
      IO.ANSI.format([:green | msg]) |> IO.puts()
    end
  end

  def plural(1, string), do: "1 #{string}"
  def plural(n, string), do: "#{n} #{string}s"

  def passed(app) do
    IO.ANSI.format([:green, "  ✓ #{app}"]) |> IO.puts()
  end

  def failed(app) do
    IO.ANSI.format([:red, "  ✗ #{app}"]) |> IO.puts()
    System.at_exit(fn _ -> exit({:shutdown, 1}) end)
  end
end

Integration.Tests.run()


================================================
FILE: lib/remote_ip/block.ex
================================================
defmodule RemoteIp.Block do
  import Bitwise
  alias __MODULE__

  @moduledoc false

  defstruct [:proto, :net, :mask]

  def encode({a, b, c, d}) do
    <<ip::32>> = <<a::8, b::8, c::8, d::8>>
    {:v4, ip}
  end

  def encode({a, b, c, d, e, f, g, h}) do
    <<ip::128>> = <<a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>>
    {:v6, ip}
  end

  def contains?(%Block{proto: proto, net: net, mask: mask}, {proto, ip}) do
    (ip &&& mask) == net
  end

  def contains?(%Block{}, {_, _}) do
    false
  end

  def parse!(cidr) do
    case parse(cidr) do
      {:ok, block} -> block
      {:error, message} -> raise ArgumentError, message
    end
  end

  def parse(cidr) do
    case process(:parts, String.split(cidr, "/", parts: 2)) do
      {:error, e} -> {:error, "#{e} in CIDR #{inspect(cidr)}"}
      ok -> ok
    end
  end

  defp process(:parts, [ip, prefix]) do
    with {:ok, ip} <- process(:ip, ip),
         {:ok, prefix} <- process(:prefix, prefix) do
      process(:block, ip, prefix)
    end
  end

  defp process(:parts, [ip]) do
    with {:ok, ip} <- process(:ip, ip) do
      process(:block, ip)
    end
  end

  defp process(:ip, address) do
    case :inet.parse_strict_address(address |> to_charlist()) do
      {:ok, ip} -> {:ok, encode(ip)}
      {:error, _} -> {:error, "Invalid address #{inspect(address)}"}
    end
  end

  defp process(:prefix, prefix) do
    try do
      {:ok, String.to_integer(prefix)}
    rescue
      ArgumentError -> {:error, "Invalid prefix #{inspect(prefix)}"}
    end
  end

  defp process(:block, {:v4, ip}) do
    process(:block, {:v4, ip}, 32)
  end

  defp process(:block, {:v6, ip}) do
    process(:block, {:v6, ip}, 128)
  end

  defp process(:block, {:v4, ip}, prefix) when prefix in 0..32 do
    ones = 0xFFFFFFFF
    <<mask::32>> = <<bnot(ones >>> prefix)::32>>
    {:ok, %Block{proto: :v4, net: ip &&& mask, mask: mask}}
  end

  defp process(:block, {:v6, ip}, prefix) when prefix in 0..128 do
    ones = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
    <<mask::128>> = <<bnot(ones >>> prefix)::128>>
    {:ok, %Block{proto: :v6, net: ip &&& mask, mask: mask}}
  end

  defp process(:block, _, prefix) do
    {:error, "Invalid prefix #{inspect(prefix)}"}
  end

  defimpl String.Chars, for: Block do
    def to_string(%Block{proto: :v4, net: net, mask: mask}) do
      <<a::8, b::8, c::8, d::8>> = <<net::32>>
      "#{:inet.ntoa({a, b, c, d})}/#{bits(mask)}"
    end

    def to_string(%Block{proto: :v6, net: net, mask: mask}) do
      <<a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>> = <<net::128>>
      "#{:inet.ntoa({a, b, c, d, e, f, g, h})}/#{bits(mask)}"
    end

    defp bits(mask) do
      ones = for <<1::1 <- :binary.encode_unsigned(mask)>>, do: 1
      length(ones)
    end
  end
end


================================================
FILE: lib/remote_ip/debugger.ex
================================================
defmodule RemoteIp.Debugger do
  require Logger

  @moduledoc """
  Compile-time debugging facilities.

  `RemoteIp` uses the `debug/3` macro to instrument its implementation with
  *debug events* at compile time. If an event is enabled, the macro will expand
  into a `Logger.debug/2` call with a specific message. If an event is
  disabled, the logging will be purged, thus generating no extra code and
  having no impact on run time.

  ## Basic usage

  Events are fired on every call to `RemoteIp.call/2` or `RemoteIp.from/2`. To
  enable or disable all debug events at once, you can set a boolean in your
  `Config` file:

  ```elixir
  config :remote_ip, debug: true
  ```

  By default, the debugger is turned off (i.e., `debug: false`).

  Because `RemoteIp.Debugger` works at compile time, you must make sure to
  recompile the `:remote_ip` dependency whenever you change the configuration:

  ```console
  $ mix deps.clean --build remote_ip
  ```

  ## Advanced usage

  You may also pass a list of atoms into the `:debug` configuration naming
  which events to log.

  These are all the possible events:

  * `:options` - the keyword options *after* any runtime configuration has been
    evaluated (see `RemoteIp.Options`)

  * `:headers` - all incoming headers, either from the `Plug.Conn`'s
    `req_headers` or the list passed directly into `RemoteIp.from/2`; useful
    for seeing if you're even getting the forwarding headers you expect in the
    first place

  * `:forwarding` - the subset of headers (as configured by `RemoteIp.Options`)
    that contain forwarding information

  * `:ips` - the entire sequence of IP addresses parsed from the forwarding
    headers, in order

  * `:type` - for each IP (until we find the client), classifies the address
    either as a known client, a known proxy, a reserved address, or none of the
    above (and thus presumably a client)

  * `:ip` - the final result of the remote IP processing; when rewriting the
    `Plug.Conn`'s `remote_ip`, the message will tell you the original IP that
    is being replaced

  Therefore, `debug: true` is equivalent to passing in all of the above:

  ```elixir
  config :remote_ip, debug: [:options, :headers, :forwarding, :ips, :type, :ip]
  ```

  But you could disable certain events by removing them from the list. For
  example, to log only the incoming headers and resulting IP:

  ```elixir
  config :remote_ip, debug: [:headers, :ip]
  ```

  ## Interactions with `Logger`

  Since they both work at compile time, your configuration of `:logger` will
  also affect the operation of `RemoteIp.Debugger`. For example, it's possible
  to enable debugging but still purge all the resulting logs:

  ```elixir
  # All events *would* be logged...
  config :remote_ip, debug: true

  # ...But :debug logs will actually get purged at compile time
  config :logger, compile_time_purge_matching: [[level_lower_than: :info]]
  ```
  """

  @doc """
  An internal macro for generating debug logs.

  There is no reason for you to call this directly. It's used to instrument the
  `RemoteIp` module at compilation time.
  """

  @spec debug(atom(), [any()], do: any()) :: any()

  defmacro debug(id, inputs \\ [], do: output) do
    if debug?(id) do
      quote do
        inputs = unquote(inputs)
        output = unquote(output)
        unquote(__MODULE__).__log__(unquote(id), inputs, output)
        output
      end
    else
      output
    end
  end

  @debug Application.compile_env(:remote_ip, :debug, false)

  cond do
    is_list(@debug) ->
      defp debug?(id), do: Enum.member?(@debug, id)

    is_boolean(@debug) ->
      defp debug?(_), do: @debug
  end

  def __log__(id, inputs, output) do
    Logger.debug(__message__(id, inputs, output))
  end

  def __message__(:options, [], options) do
    headers = inspect(options[:headers])
    parsers = inspect(options[:parsers])
    proxies = inspect(options[:proxies] |> Enum.map(&to_string/1))
    clients = inspect(options[:clients] |> Enum.map(&to_string/1))

    [
      "Processing remote IP\n",
      "  headers: #{headers}\n",
      "  parsers: #{parsers}\n",
      "  proxies: #{proxies}\n",
      "  clients: #{clients}"
    ]
  end

  def __message__(:headers, [], headers) do
    "Taking forwarding headers from #{inspect(headers)}"
  end

  def __message__(:forwarding, [], headers) do
    "Parsing IPs from forwarding headers: #{inspect(headers)}"
  end

  def __message__(:ips, [], ips) do
    "Parsed IPs from forwarding headers: #{inspect(ips)}"
  end

  def __message__(:type, [ip], type) do
    case type do
      :client -> "#{inspect(ip)} is a known client IP"
      :proxy -> "#{inspect(ip)} is a known proxy IP"
      :reserved -> "#{inspect(ip)} is a reserved IP"
      :unknown -> "#{inspect(ip)} is an unknown IP, assuming it's the client"
    end
  end

  def __message__(:ip, [old_conn], new_conn) do
    origin = inspect(old_conn.remote_ip)
    client = inspect(new_conn.remote_ip)

    if client != origin do
      "Processed remote IP, found client #{client} to replace #{origin}"
    else
      "Processed remote IP, no client found to replace #{origin}"
    end
  end

  def __message__(:ip, [], ip) do
    if ip == nil do
      "Processed remote IP, no client found"
    else
      "Processed remote IP, found client #{inspect(ip)}"
    end
  end
end


================================================
FILE: lib/remote_ip/headers.ex
================================================
defmodule RemoteIp.Headers do
  @moduledoc """
  Functions for parsing IPs from multiple types of forwarding headers.
  """

  @doc """
  Extracts all headers with the given names.

  Note that `Plug.Conn` headers are assumed to have been normalized to
  lowercase, so the names you give should be in lowercase as well.

  ## Examples

      iex> [{"x-foo", "foo"}, {"x-bar", "bar"}, {"x-baz", "baz"}]
      ...> |> RemoteIp.Headers.take(["x-foo", "x-baz", "x-qux"])
      [{"x-foo", "foo"}, {"x-baz", "baz"}]

      iex> [{"x-dup", "foo"}, {"x-dup", "bar"}, {"x-dup", "baz"}]
      ...> |> RemoteIp.Headers.take(["x-dup"])
      [{"x-dup", "foo"}, {"x-dup", "bar"}, {"x-dup", "baz"}]
  """

  @spec take(Plug.Conn.headers(), [binary()]) :: Plug.Conn.headers()

  def take(headers, names) do
    Enum.filter(headers, fn {name, _} -> name in names end)
  end

  @doc """
  Parses IP addresses out of the given headers.

  For each header name/value pair, the value is parsed for zero or more IP
  addresses by the parser corresponding to the name. If no such parser exists
  in the given map, we fall back to `RemoteIp.Parsers.Generic`.

  The IPs are concatenated together into a single flat list. Note that the
  relative order is preserved. That is, each header produce multiple IPs that
  are kept in the order given by that specific header. Then, in the case of
  multiple headers, the concatenated list maintains the same order as the
  headers appeared in the original name/value list.

  Due to the error-safe nature of the `RemoteIp.Parser` behaviour, headers that
  do not actually contain valid IP addresses should be safely ignored.

  ## Examples

      iex> [{"x-one", "1.2.3.4, 2.3.4.5"}, {"x-two", "3.4.5.6, 4.5.6.7"}]
      ...> |> RemoteIp.Headers.parse()
      [{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}, {4, 5, 6, 7}]

      iex> [{"forwarded", "for=1.2.3.4"}, {"x-forwarded-for", "2.3.4.5"}]
      ...> |> RemoteIp.Headers.parse()
      [{1, 2, 3, 4}, {2, 3, 4, 5}]

      iex> [{"accept", "*/*"}, {"user-agent", "ua"}, {"x-real-ip", "1.2.3.4"}]
      ...> |> RemoteIp.Headers.parse()
      [{1, 2, 3, 4}]
  """

  @spec parse(Plug.Conn.headers(), %{binary() => RemoteIp.Parser.t()}) :: [
          :inet.ip_address()
        ]

  def parse(headers, parsers \\ RemoteIp.Options.default(:parsers)) do
    Enum.flat_map(headers, fn {name, value} ->
      parser = Map.get(parsers, name, RemoteIp.Parsers.Generic)
      parser.parse(value)
    end)
  end
end


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

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

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

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

  ## `:headers`

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

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

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

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

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

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

  We still allow multiple headers because:

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

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

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

  ## `:parsers`

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


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

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

  * add a parser for your own custom header

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

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

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

  ## `:proxies`

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


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

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

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

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

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

  ## `:clients`

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


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

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

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

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

  ## Runtime options

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

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

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

  ## Examples

  ### Basic usage

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

  Then you could say:

  ```elixir
  defmodule MyApp do
    use Plug.Router

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

    plug :match
    plug :dispatch

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

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

  ```elixir
  defmodule MySocket do
    use Phoenix.Socket

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

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

  ### Custom parser

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

  First, you'd implement a custom parser:

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

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

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

  ```elixir
  defmodule SpecialApp do
    use Plug.Router

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

    plug :match
    plug :dispatch

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

  ### Using MFAs

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

  ```elixir
  defmodule CompiledApp do
    use Plug.Router

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

    plug :match
    plug :dispatch

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

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

  ```elixir
  defmodule RuntimeApp do
    use Plug.Router

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

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

    plug :match
    plug :dispatch

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

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

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

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

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

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

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

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

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

  defp evaluate(:headers, headers) do
    headers
  end

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

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

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


================================================
FILE: lib/remote_ip/parser.ex
================================================
defmodule RemoteIp.Parser do
  @moduledoc """
  Defines the interface for parsing headers into IP addresses.

  `RemoteIp.Headers.parse/1` dynamically dispatches to different parser modules
  depending on the name of the header. For example, the `"forwarded"` header is
  parsed by `RemoteIp.Parsers.Forwarded`, which implements this behaviour.
  """

  @typedoc """
  Any module that implements the `RemoteIp.Parser` behaviour.
  """

  @type t() :: module()

  @doc """
  Parses the specific header's value into a list of IP addresses.

  This callback should be error-safe. For instance, if the header's value is
  invalid, it should return an empty list.

  The actual work of converting an individual IP address string into the tuple
  type should typically be done using `:inet` functions such as
  `:inet.parse_strict_address/1`.

  Note that a header may also contain more than one IP address. The order of
  the list is important because it's interpreted as routing information.
  Conceptually, the leftmost IP is the source of the request (the client), the
  rightmost IP is the destination (your server), and anything in the middle
  lists the proxy hops in order. However, in reality, there may be bad actors
  or strange routing that makes this more complicated. It's the job of
  `RemoteIp` to sort that out. This callback should *only* be concerned with
  faithfully parsing the literal order given by the header.
  """

  @callback parse(header :: binary()) :: [:inet.ip_address()]
end


================================================
FILE: lib/remote_ip/parsers/forwarded.ex
================================================
defmodule RemoteIp.Parsers.Forwarded do
  use Combine

  @behaviour RemoteIp.Parser

  @moduledoc """
  [RFC 7239](https://tools.ietf.org/html/rfc7239) compliant parser for
  `Forwarded` headers.

  This module implements the `RemoteIp.Parser` behaviour. IPs are parsed out of
  the `for=` pairs across each forwarded element.

  ## Examples

      iex> RemoteIp.Parsers.Forwarded.parse("for=1.2.3.4;by=2.3.4.5")
      [{1, 2, 3, 4}]

      iex> RemoteIp.Parsers.Forwarded.parse("for=\\"[::1]\\", for=\\"[::2]\\"")
      [{0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 2}]

      iex> RemoteIp.Parsers.Forwarded.parse("invalid")
      []
  """

  @impl RemoteIp.Parser

  def parse(header) do
    case Combine.parse(header, forwarded()) do
      [elements] -> Enum.flat_map(elements, &parse_forwarded_for/1)
      _ -> []
    end
  end

  defp parse_forwarded_for(pairs) do
    case fors_from(pairs) do
      [string] -> parse_ip(string)
      _ambiguous -> []
    end
  end

  defp fors_from(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//-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_charlist()) 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_charlist()) do
        {:ok, ip} -> ip
        {:error, :einval} -> {:error, "Invalid IPv6 address"}
      end
    end)
  end
end


================================================
FILE: lib/remote_ip/parsers/generic.ex
================================================
defmodule RemoteIp.Parsers.Generic do
  @behaviour RemoteIp.Parser

  @moduledoc """
  Generic parser for forwarding headers.

  This module implements the `RemoteIp.Parser` behaviour. When there is not a
  more specific parser, `RemoteIp.Headers.parse/1` falls back to using this
  one.

  The value is parsed simply as a comma-separated list of IPs. This is suitable
  for a wide range of headers, such as `X-Forwarded-For`, `X-Real-IP`, and
  `X-Client-IP`.

  Any amount of whitespace is allowed before and after the commas, as well as
  at the beginning & end of the input.

  ## Examples

      iex> RemoteIp.Parsers.Generic.parse("1.2.3.4, 5.6.7.8")
      [{1, 2, 3, 4}, {5, 6, 7, 8}]

      iex> RemoteIp.Parsers.Generic.parse("  ::1  ")
      [{0, 0, 0, 0, 0, 0, 0, 1}]

      iex> RemoteIp.Parsers.Generic.parse("invalid")
      []
  """

  @impl RemoteIp.Parser

  def parse(header) do
    header |> split_commas() |> parse_ips()
  end

  defp split_commas(header) do
    header |> String.trim() |> String.split(~r/\s*,\s*/)
  end

  defp parse_ips(strings) do
    List.foldr(strings, [], fn string, ips ->
      case parse_ip(string) do
        {:ok, ip} -> [ip | ips]
        {:error, _} -> ips
      end
    end)
  end

  defp parse_ip(string) do
    try do
      :inet.parse_strict_address(string |> to_charlist())
    rescue
      UnicodeConversionError -> {:error, :invalid_unicode}
    end
  end
end


================================================
FILE: lib/remote_ip.ex
================================================
defmodule RemoteIp do
  import RemoteIp.Debugger

  @behaviour Plug

  @moduledoc """
  A plug to rewrite the `Plug.Conn`'s `remote_ip` based on forwarding headers.

  Generic comma-separated headers like `X-Forwarded-For`, `X-Real-Ip`, and
  `X-Client-Ip` are all recognized, as well as the [RFC
  7239](https://tools.ietf.org/html/rfc7239) `Forwarded` header. IPs are
  processed last-to-first to prevent IP spoofing. Read more in the
  documentation for [the algorithm](algorithm.md).

  This plug is highly configurable, giving you the power to adapt it to your
  particular networking infrastructure:

  * IPs can come from any header(s) you want. You can even implement your own
    custom parser if you're using a special format.

  * You can configure the IPs of known proxies & clients so that you never get
    the wrong results.

  * All options are configurable at runtime, so you can deploy a single release
    but still customize it using environment variables, the `Application`
    environment, or any other arbitrary mechanism.

  * Still not getting the right IP? You can recompile the plug with debugging
    enabled to generate logs, and even fine-tune the verbosity by selecting
    which events to track.

  ## Usage

  This plug should be early in your pipeline, or else the `remote_ip` might not
  get rewritten before your route's logic executes.

  In [Phoenix](https://hexdocs.pm/phoenix), this might mean plugging `RemoteIp`
  into your endpoint before the router:

  ```elixir
  defmodule MyApp.Endpoint do
    use Phoenix.Endpoint, otp_app: :my_app

    plug RemoteIp
    # plug ...
    # plug ...
    plug MyApp.Router
  end
  ```

  But if you only want to rewrite IPs in a narrower part of your app, you could
  of course put it in an individual pipeline of your router.

  In an ordinary `Plug.Router`, you should make sure `RemoteIp` comes before
  the `:match`/`:dispatch` plugs:

  ```elixir
  defmodule MyApp do
    use Plug.Router

    plug RemoteIp
    plug :match
    plug :dispatch

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

  You can also use `RemoteIp.from/2` to determine an IP from a list of headers.
  This is useful outside of the plug pipeline, where you may not have access to
  the `Plug.Conn`. For example, you might only be getting the `x_headers` from
  [`Phoenix.Socket`](https://hexdocs.pm/phoenix/Phoenix.Socket.html):

  ```elixir
  defmodule MySocket do
    use Phoenix.Socket

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

  ## Configuration

  Options may be passed as a keyword list via `RemoteIp.init/1` or directly
  into `RemoteIp.from/2`. At a high level, the following options are available:

  * `:headers` - a list of header names to consider
  * `:parsers` - a map from header names to custom parser modules
  * `:clients` - a list of known client IPs, either plain or in CIDR notation
  * `:proxies` - a list of known proxy IPs, either plain or in CIDR notation

  You can specify any option using a tuple of `{module, function_name,
  arguments}`, which will be called dynamically at runtime to get the
  equivalent value.

  For more details about these options, see `RemoteIp.Options`.

  ## Troubleshooting

  Getting the right configuration can be tricky. Requests might come in with
  unexpected headers, or maybe you didn't account for certain proxies, or any
  number of other issues.

  Luckily, you can debug `RemoteIp.call/2` and `RemoteIp.from/2` by updating
  your `Config` file:

  ```elixir
  config :remote_ip, debug: true
  ```

  and recompiling the `:remote_ip` dependency:

  ```console
  $ mix deps.clean --build remote_ip
  $ mix deps.compile
  ```

  Then it will generate log messages showing how the IP gets computed. For more
  details about these messages, as well advanced usage, see
  `RemoteIp.Debugger`.

  ## Metadata

  When you use this plug, `RemoteIp.call/2` will populate the `Logger` metadata
  under the key `:remote_ip`. This will be the string representation of the
  final value of the `Plug.Conn`'s `remote_ip`. Even if no client was found in
  the headers, we still set the metadata to the original IP.

  You can use this in your logs by updating your `Config` file:

  ```elixir
  config :logger,
    message: "$metadata[$level] $message\\n",
    metadata: [:remote_ip]
  ```

  Then your logs will look something like this:

  ```log
  [info] Running ExampleWeb.Endpoint with cowboy 2.8.0 at 0.0.0.0:4000 (http)
  [info] Access ExampleWeb.Endpoint at http://localhost:4000
  remote_ip=1.2.3.4 [info] GET /
  remote_ip=1.2.3.4 [debug] Processing with ExampleWeb.PageController.index/2
    Parameters: %{}
    Pipelines: [:browser]
  remote_ip=1.2.3.4 [info] Sent 200 in 21ms
  ```

  Note that metadata will *not* be set by `RemoteIp.from/2`.
  """

  @impl Plug

  @doc """
  The `c:Plug.init/1` callback.

  This accepts the keyword options described by `RemoteIp.Options`. Because
  plug initialization typically happens at compile time, we make sure not to
  evaluate runtime options until `call/2`.
  """

  def init(opts) do
    RemoteIp.Options.pack(opts)
  end

  @impl Plug

  @doc """
  The `c:Plug.call/2` callback.

  Rewrites the `Plug.Conn`'s `remote_ip` based on its forwarding headers. Each
  call will re-evaluate all runtime options. See `RemoteIp.Options` for
  details.
  """

  def call(conn, opts) do
    debug :ip, [conn] do
      ip = ip_from(conn.req_headers, opts) || conn.remote_ip
      add_metadata(ip)
      %{conn | remote_ip: ip}
    end
  end

  @doc """
  Extracts the remote IP from a list of headers.

  In cases where you don't have access to a full `Plug.Conn` struct, you can
  use this function to process the remote IP from a list of key-value pairs
  representing the headers.

  You may specify the same options as if you were using the plug. Runtime
  options are evaluated each time you call this function. See
  `RemoteIp.Options` for details.

  If no client IP can be found in the given headers, this function will return
  `nil`.

  ## Examples

      iex> RemoteIp.from([{"x-forwarded-for", "1.2.3.4"}])
      {1, 2, 3, 4}

      iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}]
      ...> |> RemoteIp.from(headers: ~w[x-foo])
      {1, 2, 3, 4}

      iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}]
      ...> |> RemoteIp.from(headers: ~w[x-bar])
      {2, 3, 4, 5}

      iex> [{"x-foo", "1.2.3.4"}, {"x-bar", "2.3.4.5"}]
      ...> |> RemoteIp.from(headers: ~w[x-baz])
      nil
  """

  @spec from(Plug.Conn.headers(), keyword()) :: :inet.ip_address() | nil

  def from(headers, opts \\ []) do
    debug :ip do
      ip_from(headers, init(opts))
    end
  end

  defp ip_from(headers, opts) do
    opts = options_from(opts)
    client_from(ips_from(headers, opts), opts)
  end

  defp options_from(opts) do
    debug :options do
      RemoteIp.Options.unpack(opts)
    end
  end

  defp ips_from(headers, opts) do
    debug :ips do
      headers = forwarding_from(headers, opts)
      RemoteIp.Headers.parse(headers, opts[:parsers])
    end
  end

  defp forwarding_from(headers, opts) do
    debug :forwarding do
      debug(:headers, do: headers) |> RemoteIp.Headers.take(opts[:headers])
    end
  end

  defp client_from(ips, opts) do
    Enum.reverse(ips) |> Enum.find(&client?(&1, opts))
  end

  defp client?(ip, opts) do
    type(ip, opts) in [:client, :unknown]
  end

  # https://en.wikipedia.org/wiki/Loopback
  # https://en.wikipedia.org/wiki/Private_network
  # https://en.wikipedia.org/wiki/Reserved_IP_addresses
  @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
  ] |> Enum.map(&RemoteIp.Block.parse!/1)

  defp type(ip, opts) do
    debug :type, [ip] do
      ip = RemoteIp.Block.encode(ip)

      cond do
        opts[:clients] |> contains?(ip) -> :client
        opts[:proxies] |> contains?(ip) -> :proxy
        @reserved |> contains?(ip) -> :reserved
        true -> :unknown
      end
    end
  end

  defp contains?(blocks, ip) do
    Enum.any?(blocks, &RemoteIp.Block.contains?(&1, ip))
  end

  defp add_metadata(remote_ip) do
    case :inet.ntoa(remote_ip) do
      {:error, _} -> :ok
      ip -> Logger.metadata(remote_ip: to_string(ip))
    end
  end
end


================================================
FILE: mix.exs
================================================
defmodule RemoteIp.Mixfile do
  use Mix.Project

  def project do
    [
      app: :remote_ip,
      version: "1.2.0",
      elixir: "~> 1.12",
      description: description(),
      package: package(),
      deps: deps(),
      aliases: aliases(),
      dialyzer: dialyzer(),
      docs: docs(),
      test_coverage: test_coverage()
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end

  defp description do
    "A plug to rewrite the Plug.Conn's remote_ip based on request headers" <>
      " such as Forwarded, X-Forwarded-For, X-Client-Ip, and X-Real-Ip"
  end

  defp package do
    %{
      files: ~w[lib mix.exs README.md LICENSE],
      licenses: ["MIT"],
      links: %{"GitHub" => "https://github.com/ajvondrak/remote_ip"}
    }
  end

  defp deps do
    [
      {:combine, "~> 0.10"},
      {:plug, "~> 1.14"},
      {:ex_doc, "~> 0.34", only: :dev, runtime: false},
      {:dialyxir, "~> 1.4", only: [:ci, :dev], runtime: false},
      {:excoveralls, "~> 0.18", only: [:ci, :test], runtime: false},
      {:castore, "~> 1.0", only: [:ci, :test], runtime: false}
    ]
  end

  defp aliases do
    [integrate: "run integration/tests.exs"]
  end

  defp dialyzer do
    [plt_file: {:no_warn, "priv/plts/dialyzer.plt"}]
  end

  defp docs do
    [
      source_url: "https://github.com/ajvondrak/remote_ip",
      main: "RemoteIp",
      extras: ["extras/algorithm.md"]
    ]
  end

  defp test_coverage() do
    [tool: ExCoveralls]
  end
end


================================================
FILE: test/.formatter.exs
================================================
[
  inputs: ["**/*.exs"],
  import_deps: [:plug],

  # This is an arbitrarily long line length. While most of the code conforms to
  # an 80-character limit, many of the parsing tests involve gnarly strings &
  # IP tuples that are just nicer to have on a single line. There's no good way
  # of expressing this at a finer granularity (e.g., flagging specific sections
  # of code), so we just let the tests get away with murder in general.
  line_length: 800
]


================================================
FILE: test/remote_ip/block_test.exs
================================================
defmodule RemoteIp.BlockTest do
  use ExUnit.Case, async: true
  import Bitwise

  alias RemoteIp.Block

  def octets(n) do
    Stream.repeatedly(fn -> Enum.random(0..255) end) |> Enum.take(n)
  end

  def ipv4(octets) do
    octets |> Enum.join(".")
  end

  def hextets(n) do
    Stream.repeatedly(fn -> Enum.random(0..65_535) end) |> Enum.take(n)
  end

  def ipv6(hextets) do
    hextets |> Enum.map(&Integer.to_string(&1, 16)) |> Enum.join(":")
  end

  test "parse vs parse!" do
    {:ok, success} = Block.parse("127.0.0.1")
    assert Block.parse!("127.0.0.1") == success

    {:error, error} = Block.parse("127001")
    assert_raise ArgumentError, error, fn -> Block.parse!("127001") end
  end

  test "parsing invalid CIDR" do
    assert_raise ArgumentError, ~S'Invalid address "invalid" in CIDR "invalid"', fn ->
      Block.parse!("invalid")
    end
  end

  test "IPv4 block to string" do
    assert "3.14.15.92/32" == Block.parse!("3.14.15.92/32") |> to_string()
    assert "3.14.15.0/24" == Block.parse!("3.14.15.92/24") |> to_string()
    assert "3.14.0.0/16" == Block.parse!("3.14.15.92/16") |> to_string()
    assert "3.0.0.0/8" == Block.parse!("3.14.15.92/8") |> to_string()
    assert "0.0.0.0/0" == Block.parse!("3.14.15.92/0") |> to_string()
  end

  test "IPv6 block to string" do
    assert "123::456/128" == Block.parse!("123::456/128") |> to_string()
    assert "123::/64" == Block.parse!("123::456/64") |> to_string()
    assert "::/0" == Block.parse!("123::456/0") |> to_string()
  end

  describe "parsing IPv4" do
    test "invalid address and prefix" do
      assert_raise ArgumentError, ~S'Invalid address "3.14" in CIDR "3.14/159"', fn ->
        Block.parse!("3.14/159")
      end
    end

    test "invalid address" do
      assert_raise ArgumentError, ~S'Invalid address "3.141.592.6" in CIDR "3.141.592.6/5"', fn ->
        Block.parse!("3.141.592.6/5")
      end
    end

    test "invalid prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix "invalid" in CIDR "0.0.0.0/invalid"', fn ->
        Block.parse!("0.0.0.0/invalid")
      end
    end

    test "negative prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix -1 in CIDR "0.0.0.0/-1"', fn ->
        Block.parse!("0.0.0.0/-1")
      end
    end

    test "oversized prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix 33 in CIDR "0.0.0.0/33"', fn ->
        Block.parse!("0.0.0.0/33")
      end
    end

    test "address sans prefix" do
      ip = ipv4(octets(4))
      assert Block.parse!(ip) == Block.parse!("#{ip}/32")
    end

    test "address with zero prefix" do
      ip = ipv4(octets(4))
      assert %Block{proto: :v4, net: 0} = Block.parse!("#{ip}/0")
    end

    test "addresses with valid prefixes" do
      [a, b, c, d] = octets(4)
      ip = ipv4([a, b, c, d])

      {:v4, net_a} = Block.encode({a, 0, 0, 0})
      {:v4, net_b} = Block.encode({a, b, 0, 0})
      {:v4, net_c} = Block.encode({a, b, c, 0})
      {:v4, net_x} = Block.encode({a, b &&& 0b11110000, 0, 0})

      assert %Block{proto: :v4, net: ^net_a} = Block.parse!("#{ip}/8")
      assert %Block{proto: :v4, net: ^net_b} = Block.parse!("#{ip}/16")
      assert %Block{proto: :v4, net: ^net_c} = Block.parse!("#{ip}/24")
      assert %Block{proto: :v4, net: ^net_x} = Block.parse!("#{ip}/12")
    end

    test "masks" do
      assert %Block{mask: 0b00000000000000000000000000000000} = Block.parse!("0.0.0.0/0")
      assert %Block{mask: 0b10000000000000000000000000000000} = Block.parse!("0.0.0.0/1")
      assert %Block{mask: 0b11000000000000000000000000000000} = Block.parse!("0.0.0.0/2")
      assert %Block{mask: 0b11100000000000000000000000000000} = Block.parse!("0.0.0.0/3")
      assert %Block{mask: 0b11110000000000000000000000000000} = Block.parse!("0.0.0.0/4")
      assert %Block{mask: 0b11111000000000000000000000000000} = Block.parse!("0.0.0.0/5")
      assert %Block{mask: 0b11111100000000000000000000000000} = Block.parse!("0.0.0.0/6")
      assert %Block{mask: 0b11111110000000000000000000000000} = Block.parse!("0.0.0.0/7")
      assert %Block{mask: 0b11111111000000000000000000000000} = Block.parse!("0.0.0.0/8")
      assert %Block{mask: 0b11111111100000000000000000000000} = Block.parse!("0.0.0.0/9")
      assert %Block{mask: 0b11111111110000000000000000000000} = Block.parse!("0.0.0.0/10")
      assert %Block{mask: 0b11111111111000000000000000000000} = Block.parse!("0.0.0.0/11")
      assert %Block{mask: 0b11111111111100000000000000000000} = Block.parse!("0.0.0.0/12")
      assert %Block{mask: 0b11111111111110000000000000000000} = Block.parse!("0.0.0.0/13")
      assert %Block{mask: 0b11111111111111000000000000000000} = Block.parse!("0.0.0.0/14")
      assert %Block{mask: 0b11111111111111100000000000000000} = Block.parse!("0.0.0.0/15")
      assert %Block{mask: 0b11111111111111110000000000000000} = Block.parse!("0.0.0.0/16")
      assert %Block{mask: 0b11111111111111111000000000000000} = Block.parse!("0.0.0.0/17")
      assert %Block{mask: 0b11111111111111111100000000000000} = Block.parse!("0.0.0.0/18")
      assert %Block{mask: 0b11111111111111111110000000000000} = Block.parse!("0.0.0.0/19")
      assert %Block{mask: 0b11111111111111111111000000000000} = Block.parse!("0.0.0.0/20")
      assert %Block{mask: 0b11111111111111111111100000000000} = Block.parse!("0.0.0.0/21")
      assert %Block{mask: 0b11111111111111111111110000000000} = Block.parse!("0.0.0.0/22")
      assert %Block{mask: 0b11111111111111111111111000000000} = Block.parse!("0.0.0.0/23")
      assert %Block{mask: 0b11111111111111111111111100000000} = Block.parse!("0.0.0.0/24")
      assert %Block{mask: 0b11111111111111111111111110000000} = Block.parse!("0.0.0.0/25")
      assert %Block{mask: 0b11111111111111111111111111000000} = Block.parse!("0.0.0.0/26")
      assert %Block{mask: 0b11111111111111111111111111100000} = Block.parse!("0.0.0.0/27")
      assert %Block{mask: 0b11111111111111111111111111110000} = Block.parse!("0.0.0.0/28")
      assert %Block{mask: 0b11111111111111111111111111111000} = Block.parse!("0.0.0.0/29")
      assert %Block{mask: 0b11111111111111111111111111111100} = Block.parse!("0.0.0.0/30")
      assert %Block{mask: 0b11111111111111111111111111111110} = Block.parse!("0.0.0.0/31")
      assert %Block{mask: 0b11111111111111111111111111111111} = Block.parse!("0.0.0.0/32")
    end

    test "reserved ranges" do
      assert Block.parse!("127.0.0.0/8") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<127, 0, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 0, 0, 0>>)
             }

      assert Block.parse!("10.0.0.0/8") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<10, 0, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 0, 0, 0>>)
             }

      assert Block.parse!("172.16.0.0/12") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<172, 16, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 240, 0, 0>>)
             }

      assert Block.parse!("192.168.0.0/16") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<192, 168, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 255, 0, 0>>)
             }
    end
  end

  describe "parsing IPv6" do
    test "invalid address and prefix" do
      assert_raise ArgumentError, ~S'Invalid address "a:b:c" in CIDR "a:b:c/1/2/3"', fn ->
        Block.parse!("a:b:c/1/2/3")
      end
    end

    test "invalid address" do
      assert_raise ArgumentError, ~S'Invalid address "f7::u" in CIDR "f7::u/12"', fn ->
        Block.parse!("f7::u/12")
      end
    end

    test "invalid prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix "1/2/3" in CIDR "::a:b:c/1/2/3"', fn ->
        Block.parse!("::a:b:c/1/2/3")
      end
    end

    test "negative prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix -1 in CIDR "::/-1"', fn ->
        Block.parse!("::/-1")
      end
    end

    test "oversized prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix 129 in CIDR "::/129"', fn ->
        Block.parse!("::/129")
      end
    end

    test "address sans prefix" do
      ip = ipv6(hextets(8))
      assert Block.parse!(ip) == Block.parse!("#{ip}/128")
    end

    test "address with zero prefix" do
      ip = ipv6(hextets(8))
      assert %Block{proto: :v6, net: 0} = Block.parse!("#{ip}/0")
    end

    test "addresses with valid prefixes" do
      [a, b, c, d, e, f, g, h] = hextets(8)
      ip = ipv6([a, b, c, d, e, f, g, h])

      {:v6, net_a} = Block.encode({a, 0, 0, 0, 0, 0, 0, 0})
      {:v6, net_b} = Block.encode({a, b, 0, 0, 0, 0, 0, 0})
      {:v6, net_c} = Block.encode({a, b, c, 0, 0, 0, 0, 0})
      {:v6, net_d} = Block.encode({a, b, c, d, 0, 0, 0, 0})
      {:v6, net_e} = Block.encode({a, b, c, d, e, 0, 0, 0})
      {:v6, net_f} = Block.encode({a, b, c, d, e, f, 0, 0})
      {:v6, net_g} = Block.encode({a, b, c, d, e, f, g, 0})
      {:v6, net_x} = Block.encode({a, b, c, d, e &&& 0b1000000000000000, 0, 0, 0})

      assert %Block{proto: :v6, net: ^net_a} = Block.parse!("#{ip}/16")
      assert %Block{proto: :v6, net: ^net_b} = Block.parse!("#{ip}/32")
      assert %Block{proto: :v6, net: ^net_c} = Block.parse!("#{ip}/48")
      assert %Block{proto: :v6, net: ^net_d} = Block.parse!("#{ip}/64")
      assert %Block{proto: :v6, net: ^net_e} = Block.parse!("#{ip}/80")
      assert %Block{proto: :v6, net: ^net_f} = Block.parse!("#{ip}/96")
      assert %Block{proto: :v6, net: ^net_g} = Block.parse!("#{ip}/112")
      assert %Block{proto: :v6, net: ^net_x} = Block.parse!("#{ip}/65")
    end

    test "masks" do
      assert %Block{mask: 0b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/0")
      assert %Block{mask: 0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/1")
      assert %Block{mask: 0b11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/2")
      assert %Block{mask: 0b11100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/3")
      assert %Block{mask: 0b11110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/4")
      assert %Block{mask: 0b11111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/5")
      assert %Block{mask: 0b11111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/6")
      assert %Block{mask: 0b11111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/7")
      assert %Block{mask: 0b11111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/8")
      assert %Block{mask: 0b11111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/9")
      assert %Block{mask: 0b11111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/10")
      assert %Block{mask: 0b11111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/11")
      assert %Block{mask: 0b11111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/12")
      assert %Block{mask: 0b11111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/13")
      assert %Block{mask: 0b11111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/14")
      assert %Block{mask: 0b11111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/15")
      assert %Block{mask: 0b11111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/16")
      assert %Block{mask: 0b11111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/17")
      assert %Block{mask: 0b11111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/18")
      assert %Block{mask: 0b11111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/19")
      assert %Block{mask: 0b11111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/20")
      assert %Block{mask: 0b11111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/21")
      assert %Block{mask: 0b11111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/22")
      assert %Block{mask: 0b11111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/23")
      assert %Block{mask: 0b11111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/24")
      assert %Block{mask: 0b11111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/25")
      assert %Block{mask: 0b11111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/26")
      assert %Block{mask: 0b11111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/27")
      assert %Block{mask: 0b11111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/28")
      assert %Block{mask: 0b11111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/29")
      assert %Block{mask: 0b11111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/30")
      assert %Block{mask: 0b11111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/31")
      assert %Block{mask: 0b11111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/32")
      assert %Block{mask: 0b11111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/33")
      assert %Block{mask: 0b11111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/34")
      assert %Block{mask: 0b11111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/35")
      assert %Block{mask: 0b11111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/36")
      assert %Block{mask: 0b11111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/37")
      assert %Block{mask: 0b11111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/38")
      assert %Block{mask: 0b11111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/39")
      assert %Block{mask: 0b11111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/40")
      assert %Block{mask: 0b11111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/41")
      assert %Block{mask: 0b11111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/42")
      assert %Block{mask: 0b11111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/43")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/44")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/45")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/46")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/47")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/48")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/49")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/50")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/51")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/52")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/53")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/54")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/55")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/56")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/57")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/58")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/59")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/60")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/61")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/62")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/63")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/64")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/65")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/66")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/67")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/68")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/69")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/70")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/71")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000} = Block.parse!("::/72")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000} = Block.parse!("::/73")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000} = Block.parse!("::/74")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000} = Block.parse!("::/75")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000} = Block.parse!("::/76")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000} = Block.parse!("::/77")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000} = Block.parse!("::/78")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000} = Block.parse!("::/79")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000} = Block.parse!("::/80")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000} = Block.parse!("::/81")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000} = Block.parse!("::/82")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000} = Block.parse!("::/83")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000} = Block.parse!("::/84")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000} = Block.parse!("::/85")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000} = Block.parse!("::/86")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000} = Block.parse!("::/87")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000} = Block.parse!("::/88")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000} = Block.parse!("::/89")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000} = Block.parse!("::/90")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000} = Block.parse!("::/91")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000} = Block.parse!("::/92")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000} = Block.parse!("::/93")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000} = Block.parse!("::/94")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000} = Block.parse!("::/95")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000} = Block.parse!("::/96")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000} = Block.parse!("::/97")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000} = Block.parse!("::/98")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000} = Block.parse!("::/99")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000} = Block.parse!("::/100")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000} = Block.parse!("::/101")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000} = Block.parse!("::/102")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000} = Block.parse!("::/103")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000} = Block.parse!("::/104")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000} = Block.parse!("::/105")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000} = Block.parse!("::/106")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000} = Block.parse!("::/107")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000} = Block.parse!("::/108")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000} = Block.parse!("::/109")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000} = Block.parse!("::/110")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000} = Block.parse!("::/111")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000} = Block.parse!("::/112")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000} = Block.parse!("::/113")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000} = Block.parse!("::/114")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000} = Block.parse!("::/115")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000} = Block.parse!("::/116")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000} = Block.parse!("::/117")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000} = Block.parse!("::/118")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000} = Block.parse!("::/119")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000} = Block.parse!("::/120")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000} = Block.parse!("::/121")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000} = Block.parse!("::/122")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000} = Block.parse!("::/123")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000} = Block.parse!("::/124")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000} = Block.parse!("::/125")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100} = Block.parse!("::/126")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110} = Block.parse!("::/127")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111} = Block.parse!("::/128")
    end

    test "reserved ranges" do
      assert Block.parse!("::1/128") == %Block{
               proto: :v6,
               net: 0x00000000000000000000000000000001,
               mask: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
             }

      assert Block.parse!("fc00::/7") == %Block{
               proto: :v6,
               net: 0xFC000000000000000000000000000000,
               mask: 0xFE000000000000000000000000000000
             }
    end
  end

  describe "IPv4 membership" do
    test "inside block" do
      block = Block.parse!("192.168.0.0/16")

      for a <- 0..255, b <- 0..255 do
        assert Block.contains?(block, Block.encode({192, 168, a, b}))
      end
    end

    test "outside block" do
      block = Block.parse!("192.168.0.0/16")
      refute Block.contains?(block, Block.encode({192, 167, 255, 255}))
      refute Block.contains?(block, Block.encode({192, 169, 0, 0}))
      refute Block.contains?(block, Block.encode({191, 168, 0, 0}))
      refute Block.contains?(block, Block.encode({191, 255, 255, 255}))
      refute Block.contains?(block, Block.encode({194, 0, 0, 0}))
      refute Block.contains?(block, Block.encode({31, 41, 59, 27}))
    end

    test "with exact match" do
      block = Block.parse!("127.0.0.1/32")
      refute Block.contains?(block, Block.encode({127, 0, 0, 0}))
      assert Block.contains?(block, Block.encode({127, 0, 0, 1}))
      refute Block.contains?(block, Block.encode({127, 0, 0, 2}))
    end

    test "with zero-length prefix" do
      block = Block.parse!("0.0.0.0/0")
      [a, b, c, d] = octets(4)
      assert Block.contains?(block, Block.encode({a, b, c, d}))
    end

    test "with lower bits that are masked off" do
      block = Block.parse!("192.168.100.14/24")
      assert block.net == :binary.decode_unsigned(<<192, 168, 100, 0>>)

      for member <- 0..255 do
        assert Block.contains?(block, Block.encode({192, 168, 100, member}))
      end
    end

    test "against IPv6" do
      block = Block.parse!("0.0.0.0/0")
      [a, b, c, d, e, f, g, h] = hextets(8)
      refute Block.contains?(block, Block.encode({a, b, c, d, e, f, g, h}))
    end
  end

  describe "IPv6 membership" do
    test "inside block" do
      block = Block.parse!("1111:2222:3333:4444:5555:6666:7777:8800/120")

      for member <- 0x8800..0x88FF do
        assert Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, member}))
      end
    end

    test "outside block" do
      block = Block.parse!("1111:2222:3333:4444:5555:6666:7777:8800/120")
      refute Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x87FF}))
      refute Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x8900}))
    end

    test "with exact match" do
      block = Block.parse!("::1/128")
      refute Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 0}))
      assert Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 1}))
      refute Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 2}))
    end

    test "with zero-length prefix" do
      block = Block.parse!("::/0")
      [a, b, c, d, e, f, g, h] = hextets(8)
      assert Block.contains?(block, Block.encode({a, b, c, d, e, f, g, h}))
    end

    test "with lower bits that are masked off" do
      block = Block.parse!("a:b:c:d:e:f::/48")
      assert block.net == :binary.decode_unsigned(<<0x000A::16, 0x000B::16, 0x00C::16, 0::16, 0::16, 0::16, 0::16, 0::16>>)
      [d, e, f, g, h] = hextets(5)
      assert Block.contains?(block, Block.encode({0x000A, 0x000B, 0x000C, d, e, f, g, h}))
    end

    test "against IPv4" do
      block = Block.parse!("::/0")
      [a, b, c, d] = octets(4)
      refute Block.contains?(block, Block.encode({a, b, c, d}))
    end
  end
end


================================================
FILE: test/remote_ip/headers_test.exs
================================================
defmodule RemoteIp.HeadersTest do
  use ExUnit.Case, async: true

  doctest RemoteIp.Headers

  test "taking from an empty list of headers" do
    headers = []
    allowed = ["a", "b", "c"]
    assert RemoteIp.Headers.take(headers, allowed) == []
  end

  test "taking no headers" do
    headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}]
    allowed = []
    assert RemoteIp.Headers.take(headers, allowed) == []
  end

  test "taking all headers" do
    headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}]
    allowed = ["a", "b", "c"]
    assert RemoteIp.Headers.take(headers, allowed) == headers
  end

  test "taking a subset of headers" do
    headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}]
    allowed = ["a", "c"]
    assert RemoteIp.Headers.take(headers, allowed) == [{"a", "1"}, {"c", "3"}]
  end

  test "taking a superset of headers" do
    headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}]
    allowed = ["a", "z"]
    assert RemoteIp.Headers.take(headers, allowed) == [{"a", "1"}]
  end

  test "taking a disjoint set of headers" do
    headers = [{"a", "1"}, {"b", "2"}, {"c", "3"}]
    allowed = ["x", "y", "z"]
    assert RemoteIp.Headers.take(headers, allowed) == []
  end

  test "taking duplicate headers" do
    headers = [{"a", "1"}, {"a", "2"}, {"b", "3"}]
    allowed = ["a"]
    assert RemoteIp.Headers.take(headers, allowed) == [{"a", "1"}, {"a", "2"}]
  end

  test "parsing Forwarded headers" do
    ips = [
      {1, 2, 3, 4},
      {0, 0, 0, 0, 2, 3, 4, 5},
      {3, 4, 5, 6},
      {0, 0, 0, 0, 4, 5, 6, 7}
    ]

    headers = [
      {"forwarded", ~S'for=1.2.3.4'},
      {"forwarded", ~S'for="[::2:3:4:5]";proto=http;host=example.com'},
      {"forwarded", ~S'proto=http;for=3.4.5.6;by=127.0.0.1'},
      {"forwarded", ~S'proto=http;host=example.com;for="[::4:5:6:7]"'}
    ]

    assert RemoteIp.Headers.parse(headers) == ips

    headers = [
      {"forwarded", ~S'for=1.2.3.4, for="[::2:3:4:5]";proto=http;host=example.com'},
      {"forwarded", ~S'proto=http;for=3.4.5.6;by=127.0.0.1'},
      {"forwarded", ~S'proto=http;host=example.com;for="[::4:5:6:7]"'}
    ]

    assert RemoteIp.Headers.parse(headers) == ips

    headers = [
      {"forwarded", ~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'},
      {"forwarded", ~S'proto=http;host=example.com;for="[::4:5:6:7]"'}
    ]

    assert RemoteIp.Headers.parse(headers) == ips

    headers = [
      {"forwarded", ~S'for=1.2.3.4'},
      {"forwarded", ~S'for="[::2:3:4:5]";proto=http;host=example.com'},
      {"forwarded", ~S'proto=http;for=3.4.5.6;by=127.0.0.1, proto=http;host=example.com;for="[::4:5:6:7]"'}
    ]

    assert RemoteIp.Headers.parse(headers) == ips

    headers = [
      {"forwarded", ~S'for=1.2.3.4'},
      {"forwarded", ~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]"'}
    ]

    assert RemoteIp.Headers.parse(headers) == ips
  end

  test "parsing generic headers" do
    headers = [
      {"generic", "1.1.1.1, unknown, 2.2.2.2"},
      {"generic", "   3.3.3.3 ,  4.4.4.4,not_an_ip"},
      {"generic", "5.5.5.5,::6:6:6:6"},
      {"generic", "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 RemoteIp.Headers.parse(headers) == ips
  end

  test "parsing an unrecognized header falls back to generic parsing" do
    headers = [
      {"x-forwarded-for", "1.1.1.1,2.2.2.2"},
      {"x-real-ip", "3.3.3.3, 4.4.4.4"},
      {"x-client-ip", "5.5.5.5"}
    ]

    ips = [
      {1, 1, 1, 1},
      {2, 2, 2, 2},
      {3, 3, 3, 3},
      {4, 4, 4, 4},
      {5, 5, 5, 5}
    ]

    assert RemoteIp.Headers.parse(headers) == ips
  end

  test "parsing multiple kinds of headers" do
    headers = [
      {"forwarded", "for=1.1.1.1"},
      {"x-forwarded-for", "2.2.2.2"},
      {"forwarded", "for=3.3.3.3, for=4.4.4.4"},
      {"x-forwarded-for", "invalid"},
      {"forwarded", "for=5.5.5.5"},
      {"x-forwarded-for", "6.6.6.6, 7.7.7.7"},
      {"invalid", "header"}
    ]

    ips = [
      {1, 1, 1, 1},
      {2, 2, 2, 2},
      {3, 3, 3, 3},
      {4, 4, 4, 4},
      {5, 5, 5, 5},
      {6, 6, 6, 6},
      {7, 7, 7, 7}
    ]

    assert RemoteIp.Headers.parse(headers) == ips
  end

  defmodule Custom do
    @behaviour RemoteIp.Parser

    @impl RemoteIp.Parser

    def parse(_) do
      [{1, 2, 3, 4}]
    end
  end

  test "using custom parsers" do
    headers = [
      {"x-custom", "parser's gonna parse"},
      {"x-forwarded-for", "2.3.4.5"},
      {"forwarded", "for=3.4.5.6"}
    ]

    ips = [
      {1, 2, 3, 4},
      {2, 3, 4, 5}
    ]

    assert RemoteIp.Headers.parse(headers, %{"x-custom" => Custom}) == ips
  end
end


================================================
FILE: test/remote_ip/options_test.exs
================================================
defmodule RemoteIp.OptionsTest do
  use ExUnit.Case, async: true

  defmodule MFA do
    use Agent

    def setup do
      {:ok, _} = Agent.start_link(fn -> [] end, name: __MODULE__)
      :ok
    end

    def get(opt) do
      Agent.get(__MODULE__, fn opts -> Keyword.get(opts, opt) end)
    end

    def put(opt, val) do
      Agent.update(__MODULE__, fn opts -> Keyword.put(opts, opt, val) end)
    end
  end

  setup do
    MFA.setup()
  end

  describe "pack" do
    test "unknown option" do
      packed = RemoteIp.Options.pack(unknown: :option)
      refute Keyword.has_key?(packed, :unknown)
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :proxies)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":headers default" do
      packed = RemoteIp.Options.pack([])
      assert "forwarded" in packed[:headers]
      assert "x-forwarded-for" in packed[:headers]
      assert "x-client-ip" in packed[:headers]
      assert "x-real-ip" in packed[:headers]
    end

    test ":headers list" do
      packed = RemoteIp.Options.pack(headers: ~w[a b c])
      assert packed[:headers] == ~w[a b c]
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :proxies)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":headers mfa" do
      packed = RemoteIp.Options.pack(headers: {MFA, :get, [:headers]})
      assert packed[:headers] == {MFA, :get, [:headers]}
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :proxies)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":parsers map" do
      packed = RemoteIp.Options.pack(parsers: %{"foo" => Bar})
      assert is_map(packed[:parsers])
      assert packed[:parsers]["foo"] == Bar
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :proxies)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":parsers default" do
      packed = RemoteIp.Options.pack([])
      assert is_map(packed[:parsers])
      assert packed[:parsers]["forwarded"] == RemoteIp.Parsers.Forwarded
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :proxies)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":parsers mfa" do
      packed = RemoteIp.Options.pack(parsers: {MFA, :get, [:parsers]})
      assert packed[:parsers] == {MFA, :get, [:parsers]}
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :proxies)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":proxies default" do
      packed = RemoteIp.Options.pack([])
      assert packed[:proxies] == []
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":proxies list" do
      packed = RemoteIp.Options.pack(proxies: ~w[123.0.0.0/8])
      assert [%RemoteIp.Block{} = block] = packed[:proxies]
      assert to_string(block) == "123.0.0.0/8"
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":proxies mfa" do
      packed = RemoteIp.Options.pack(proxies: {MFA, :get, [:proxies]})
      assert packed[:proxies] == {MFA, :get, [:proxies]}
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :clients)
    end

    test ":clients default" do
      packed = RemoteIp.Options.pack([])
      assert packed[:clients] == []
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :proxies)
    end

    test ":clients list" do
      packed = RemoteIp.Options.pack(clients: ~w[234.0.0.0/8])
      assert [%RemoteIp.Block{} = block] = packed[:clients]
      assert to_string(block) == "234.0.0.0/8"
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :proxies)
    end

    test ":clients mfa" do
      packed = RemoteIp.Options.pack(clients: {MFA, :get, [:clients]})
      assert packed[:clients] == {MFA, :get, [:clients]}
      assert Keyword.has_key?(packed, :headers)
      assert Keyword.has_key?(packed, :parsers)
      assert Keyword.has_key?(packed, :proxies)
    end
  end

  describe "unpack" do
    test ":headers default" do
      packed = RemoteIp.Options.pack([])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:headers] == packed[:headers]
    end

    test ":headers list" do
      packed = RemoteIp.Options.pack(headers: ~w[a b c])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:headers] == packed[:headers]
    end

    test ":headers mfa" do
      packed = RemoteIp.Options.pack(headers: {MFA, :get, [:headers]})

      MFA.put(:headers, ~w[a b c])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:headers] == ~w[a b c]

      MFA.put(:headers, ~w[d e f])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:headers] == ~w[d e f]
    end

    test ":parsers default" do
      packed = RemoteIp.Options.pack([])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:parsers] == packed[:parsers]
    end

    test ":parsers map" do
      packed = RemoteIp.Options.pack(parsers: %{"foo" => Bar})
      unpacked = RemoteIp.Options.unpack(packed)
      parsers = %{"forwarded" => RemoteIp.Parsers.Forwarded, "foo" => Bar}
      assert unpacked[:parsers] == parsers
    end

    test ":parsers mfa" do
      packed = RemoteIp.Options.pack(parsers: {MFA, :get, [:parsers]})

      MFA.put(:parsers, %{"foo" => Bar})
      unpacked = RemoteIp.Options.unpack(packed)
      parsers = %{"forwarded" => RemoteIp.Parsers.Forwarded, "foo" => Bar}
      assert unpacked[:parsers] == parsers

      MFA.put(:parsers, %{"bar" => Baz})
      unpacked = RemoteIp.Options.unpack(packed)
      parsers = %{"forwarded" => RemoteIp.Parsers.Forwarded, "bar" => Baz}
      assert unpacked[:parsers] == parsers
    end

    test ":proxies default" do
      packed = RemoteIp.Options.pack([])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:proxies] == packed[:proxies]
    end

    test ":proxies list" do
      packed = RemoteIp.Options.pack(proxies: ~w[123.0.0.0/8 234.0.0.0/8])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:proxies] == packed[:proxies]
    end

    test ":proxies mfa" do
      packed = RemoteIp.Options.pack(proxies: {MFA, :get, [:proxies]})

      MFA.put(:proxies, ~w[123.0.0.0/8])
      unpacked = RemoteIp.Options.unpack(packed)
      assert [%RemoteIp.Block{} = block] = unpacked[:proxies]
      assert to_string(block) == "123.0.0.0/8"

      MFA.put(:proxies, ~w[234.0.0.0/8])
      unpacked = RemoteIp.Options.unpack(packed)
      assert [%RemoteIp.Block{} = block] = unpacked[:proxies]
      assert to_string(block) == "234.0.0.0/8"
    end

    test ":clients default" do
      packed = RemoteIp.Options.pack([])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:clients] == packed[:clients]
    end

    test ":clients list" do
      packed = RemoteIp.Options.pack(clients: ~w[123.0.0.0/8 234.0.0.0/8])
      unpacked = RemoteIp.Options.unpack(packed)
      assert unpacked[:clients] == packed[:clients]
    end

    test ":clients mfa" do
      packed = RemoteIp.Options.pack(clients: {MFA, :get, [:clients]})

      MFA.put(:clients, ~w[123.0.0.0/8])
      unpacked = RemoteIp.Options.unpack(packed)
      assert [%RemoteIp.Block{} = block] = unpacked[:clients]
      assert to_string(block) == "123.0.0.0/8"

      MFA.put(:clients, ~w[234.0.0.0/8])
      unpacked = RemoteIp.Options.unpack(packed)
      assert [%RemoteIp.Block{} = block] = unpacked[:clients]
      assert to_string(block) == "234.0.0.0/8"
    end
  end
end


================================================
FILE: test/remote_ip/parsers/forwarded_test.exs
================================================
defmodule RemoteIp.Parsers.ForwardedTest do
  use ExUnit.Case, async: true

  alias RemoteIp.Parsers.Forwarded

  doctest 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 "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
  end
end


================================================
FILE: test/remote_ip/parsers/generic_test.exs
================================================
defmodule RemoteIp.Parsers.GenericTest do
  use ExUnit.Case, async: true

  alias RemoteIp.Parsers.Generic

  doctest Generic

  describe "parsing" do
    test "bad IPs" do
      assert [] == Generic.parse("")
      assert [] == Generic.parse("      ")
      assert [] == Generic.parse("not_an_ip")
      assert [] == Generic.parse("unknown")
      assert [] == Generic.parse(<<240, 253, 253, 253>>)
    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
  end
end


================================================
FILE: test/remote_ip_test.exs
================================================
defmodule RemoteIpTest do
  use ExUnit.Case, async: true
  use Plug.Test

  doctest RemoteIp

  @unknown [
    {"forwarded", "for=unknown"},
    {"x-forwarded-for", "not_an_ip"},
    {"x-client-ip", "_obf"},
    {"x-real-ip", "1.2.3"},
    {"custom", "::g"}
  ]

  @loopback [
    {"forwarded", "for=127.0.0.1"},
    {"x-forwarded-for", "::1"},
    {"x-client-ip", "127.0.0.2"},
    {"x-real-ip", "::::::1"},
    {"custom", "127.127.127.127"}
  ]

  @private [
    {"forwarded", "for=10.0.0.1"},
    {"x-forwarded-for", "172.16.0.1"},
    {"x-client-ip", "fd00::"},
    {"x-real-ip", "192.168.10.10"},
    {"custom", "172.31.41.59"}
  ]

  @public_v4 [
    {"forwarded", "for=2.71.82.8"},
    {"x-forwarded-for", "2.71.82.8"},
    {"x-client-ip", "2.71.82.8"},
    {"x-real-ip", "2.71.82.8"},
    {"custom", "2.71.82.8"}
  ]

  @public_v6 [
    {"forwarded", "for=\"[::2.71.82.8]\""},
    {"x-forwarded-for", "::247:5208"},
    {"x-client-ip", "0:0:0:0:0:0:2.71.82.8"},
    {"x-real-ip", "0::0:247:5208"},
    {"custom", "0:0::2.71.82.8"}
  ]

  def call(conn, opts \\ []) do
    RemoteIp.call(conn, RemoteIp.init(opts))
  end

  describe "call/2" do
    test "no headers" do
      peer = {86, 75, 30, 9}
      head = []
      conn = %Plug.Conn{remote_ip: peer, req_headers: head}
      assert call(conn).remote_ip == peer
      assert Logger.metadata()[:remote_ip] == "86.75.30.9"
    end

    test "invalid ip address" do
      peer = "an invalid ip"
      head = []
      conn = %Plug.Conn{remote_ip: peer, req_headers: head}
      assert call(conn).remote_ip == peer
      assert Logger.metadata()[:remote_ip] == nil
    end

    for {header, value} <- @unknown do
      test "#{header} header from unknown IP" do
        peer = {1, 2, 3, 4}
        head = [{unquote(header), unquote(value)}]
        conn = %Plug.Conn{remote_ip: peer, req_headers: head}
        opts = [headers: [unquote(header)]]
        assert call(conn, opts).remote_ip == peer
        assert Logger.metadata()[:remote_ip] == "1.2.3.4"
      end
    end

    for {header, value} <- @loopback do
      test "#{header} header from loopback IP" do
        peer = {0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF}
        head = [{unquote(header), unquote(value)}]
        conn = %Plug.Conn{remote_ip: peer, req_headers: head}
        opts = [headers: [unquote(header)]]
        assert call(conn, opts).remote_ip == peer
        assert Logger.metadata()[:remote_ip] == "d:e:a:d:b:e:e:f"
      end
    end

    for {header, value} <- @private do
      test "#{header} header from private IP" do
        peer = {0xDE, 0xAD, 0, 0, 0, 0, 0xBE, 0xEF}
        head = [{unquote(header), unquote(value)}]
        conn = %Plug.Conn{remote_ip: peer, req_headers: head}
        opts = [headers: [unquote(header)]]
        assert call(conn, opts).remote_ip == peer
        assert Logger.metadata()[:remote_ip] == "de:ad::be:ef"
      end
    end

    for {header, value} <- @public_v4 do
      test "#{header} header from public IP (v4)" do
        peer = {3, 141, 59, 27}
        head = [{unquote(header), unquote(value)}]
        conn = %Plug.Conn{remote_ip: peer, req_headers: head}
        opts = [headers: [unquote(header)]]
        assert call(conn, opts).remote_ip == {2, 71, 82, 8}
        assert Logger.metadata()[:remote_ip] == "2.71.82.8"
      end
    end

    for {header, value} <- @public_v6 do
      test "#{header} header from public IP (v6)" do
        peer = {3, 141, 59, 27}
        head = [{unquote(header), unquote(value)}]
        conn = %Plug.Conn{remote_ip: peer, req_headers: head}
        opts = [headers: [unquote(header)]]
        assert call(conn, opts).remote_ip == {0, 0, 0, 0, 0, 0, 583, 21000}
        assert Logger.metadata()[:remote_ip] == "::2.71.82.8"
      end
    end
  end

  describe "from/2" do
    test "no headers" do
      head = []
      assert RemoteIp.from(head) == nil
      assert Logger.metadata()[:remote_ip] == nil
    end

    for {header, value} <- @unknown do
      test "#{header} header from unknown IP" do
        head = [{unquote(header), unquote(value)}]
        opts = [headers: [unquote(header)]]
        assert RemoteIp.from(head, opts) == nil
        assert Logger.metadata()[:remote_ip] == nil
      end
    end

    for {header, value} <- @loopback do
      test "#{header} header from loopback IP" do
        head = [{unquote(header), unquote(value)}]
        opts = [headers: [unquote(header)]]
        assert RemoteIp.from(head, opts) == nil
        assert Logger.metadata()[:remote_ip] == nil
      end
    end

    for {header, value} <- @private do
      test "#{header} header from private IP" do
        head = [{unquote(header), unquote(value)}]
        opts = [headers: [unquote(header)]]
        assert RemoteIp.from(head, opts) == nil
        assert Logger.metadata()[:remote_ip] == nil
      end
    end

    for {header, value} <- @public_v4 do
      test "#{header} header from public IP (v4)" do
        head = [{unquote(header), unquote(value)}]
        opts = [headers: [unquote(header)]]
        assert RemoteIp.from(head, opts) == {2, 71, 82, 8}
        assert Logger.metadata()[:remote_ip] == nil
      end
    end

    for {header, value} <- @public_v6 do
      test "#{header} header from public IP (v6)" do
        head = [{unquote(header), unquote(value)}]
        opts = [headers: [unquote(header)]]
        assert RemoteIp.from(head, opts) == {0, 0, 0, 0, 0, 0, 583, 21000}
        assert Logger.metadata()[:remote_ip] == nil
      end
    end
  end

  @proxies [
    {"forwarded", "for=1.2.3.4"},
    {"x-forwarded-for", "::a"},
    {"x-client-ip", "1:2:3:4:5:6:7:8"},
    {"x-real-ip", "4.4.4.4"}
  ]

  describe ":proxies option" do
    test "can block presumed clients" do
      head = @proxies
      opts = [proxies: ~w[1.2.0.0/16 ::a/128 4.0.0.0/8 1::/30]]
      assert RemoteIp.from(head, opts) == nil
    end

    test "cannot block known clients" do
      head = @proxies
      opts = [proxies: ~w[0.0.0.0/0 ::/0], clients: ~w[1.2.0.0/16]]
      assert RemoteIp.from(head, opts) == {1, 2, 3, 4}
    end

    test "always includes reserved IPs" do
      head = @proxies ++ @loopback ++ @private
      opts = [proxies: ~w[1.2.0.0/16 ::a/128 4.0.0.0/8 1::/30 8.8.8.8/32]]
      assert RemoteIp.from(head, opts) == nil
    end

    test "can be an MFA" do
      head = [{"x-forwarded-for", "1.2.3.4, 2.3.4.5"}]
      opts = [proxies: {Application, :get_env, [:remote_ip_test, :proxies]}]

      Application.put_env(:remote_ip_test, :proxies, [])
      assert RemoteIp.from(head, opts) == {2, 3, 4, 5}

      Application.put_env(:remote_ip_test, :proxies, ~w[2.0.0.0/8])
      assert RemoteIp.from(head, opts) == {1, 2, 3, 4}
    end
  end

  @clients [
    {"forwarded", "for=2.71.82.81"},
    {"x-forwarded-for", "82.84.59.0"},
    {"x-client-ip", "45.235.36.0"},
    {"x-real-ip", "28.74.71.35"}
  ]

  describe ":clients option" do
    test "can allow reserved IPs" do
      head = @loopback ++ @private
      opts = [clients: ~w[192.168.10.0/24]]
      assert RemoteIp.from(head, opts) == {192, 168, 10, 10}
    end

    test "can allow known proxies" do
      head = @clients

      opts = [
        proxies: ~w[2.0.0.0/8 82.84.0.0/16 45.235.36.0/24 28.74.71.35/32],
        clients: ~w[2.71.0.0/16]
      ]

      assert RemoteIp.from(head, opts) == {2, 71, 82, 81}
    end

    test "doesn't impact presumed clients" do
      head = @clients
      opts = [clients: ~w[2.0.0.0/8 82.84.0.0/16 45.235.36.0/24 28.74.71.35/32]]
      assert RemoteIp.from(head, opts) == {28, 74, 71, 35}
    end

    test "can be an MFA" do
      head = [{"x-forwarded-for", "1.2.3.4, 127.0.0.1"}]
      opts = [clients: {Application, :get_env, [:remote_ip_test, :clients]}]

      Application.put_env(:remote_ip_test, :clients, [])
      assert RemoteIp.from(head, opts) == {1, 2, 3, 4}

      Application.put_env(:remote_ip_test, :clients, ~w[127.0.0.0/8])
      assert RemoteIp.from(head, opts) == {127, 0, 0, 1}
    end
  end

  @headers [
    {"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"}
  ]

  describe ":headers option" do
    test "specifies which headers to use" do
      head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}]

      assert RemoteIp.from(head, headers: ~w[a b]) == {2, 3, 4, 5}
      assert RemoteIp.from(head, headers: ~w[a c]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[b a]) == {2, 3, 4, 5}
      assert RemoteIp.from(head, headers: ~w[b c]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[c a]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[c b]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[a]) == {1, 2, 3, 4}
      assert RemoteIp.from(head, headers: ~w[b]) == {2, 3, 4, 5}
      assert RemoteIp.from(head, headers: ~w[c]) == {3, 4, 5, 6}
    end

    for {header, value} <- @headers do
      test "includes #{header} by default" do
        head = [{unquote(header), unquote(value)}]
        assert RemoteIp.from(head) == {1, 2, 3, 4}
      end
    end

    test "overrides the defaults when specified" do
      head = @headers
      opts = [headers: ~w[custom]]
      fail = "default headers are still being parsed"
      refute RemoteIp.from(head, opts) == {1, 2, 3, 4}, fail
    end

    test "doesn't care about order" do
      head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}, {"c", "3.4.5.6"}]

      assert RemoteIp.from(head, headers: ~w[a b c]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[a c b]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[b a c]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[b c a]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[c a b]) == {3, 4, 5, 6}
      assert RemoteIp.from(head, headers: ~w[c b a]) == {3, 4, 5, 6}
    end

    test "can be an MFA" do
      head = [{"a", "1.2.3.4"}, {"b", "2.3.4.5"}]
      opts = [headers: {Application, :get_env, [:remote_ip_test, :headers]}]

      Application.put_env(:remote_ip_test, :headers, ~w[a])
      assert RemoteIp.from(head, opts) == {1, 2, 3, 4}

      Application.put_env(:remote_ip_test, :headers, ~w[b])
      assert RemoteIp.from(head, opts) == {2, 3, 4, 5}
    end
  end

  describe "multiple headers" do
    test "from unknown to unknown" do
      head = [{"forwarded", "for=unknown,for=_obf"}]
      opts = []
      assert RemoteIp.from(head, opts) == nil
    end

    test "from unknown to loopback" do
      head = [{"x-forwarded-for", "unknown,::1"}]
      opts = []
      assert RemoteIp.fr
Download .txt
gitextract_gtwens9r/

├── .formatter.exs
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .tool-versions
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bench/
│   ├── .formatter.exs
│   ├── .gitignore
│   ├── README.md
│   ├── check.exs
│   ├── lib/
│   │   └── bench/
│   │       └── inputs.ex
│   ├── mix.exs
│   └── parse.exs
├── coveralls.json
├── extras/
│   └── algorithm.md
├── integration/
│   ├── tests/
│   │   ├── basic/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── lib/
│   │   │   │   └── basic.ex
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── basic_test.exs
│   │   │       └── test_helper.exs
│   │   ├── custom/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── config/
│   │   │   │   └── config.exs
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── custom_test.exs
│   │   │       └── test_helper.exs
│   │   ├── debug/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── config/
│   │   │   │   └── config.exs
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── debug_test.exs
│   │   │       └── test_helper.exs
│   │   ├── parsers/
│   │   │   ├── .formatter.exs
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   ├── config/
│   │   │   │   └── config.exs
│   │   │   ├── lib/
│   │   │   │   ├── parsers/
│   │   │   │   │   └── forwarding.ex
│   │   │   │   └── parsers.ex
│   │   │   ├── mix.exs
│   │   │   └── test/
│   │   │       ├── parsers_test.exs
│   │   │       └── test_helper.exs
│   │   └── purge/
│   │       ├── .formatter.exs
│   │       ├── .gitignore
│   │       ├── README.md
│   │       ├── config/
│   │       │   └── config.exs
│   │       ├── mix.exs
│   │       └── test/
│   │           ├── purge_test.exs
│   │           └── test_helper.exs
│   └── tests.exs
├── lib/
│   ├── remote_ip/
│   │   ├── block.ex
│   │   ├── debugger.ex
│   │   ├── headers.ex
│   │   ├── options.ex
│   │   ├── parser.ex
│   │   └── parsers/
│   │       ├── forwarded.ex
│   │       └── generic.ex
│   └── remote_ip.ex
├── mix.exs
└── test/
    ├── .formatter.exs
    ├── remote_ip/
    │   ├── block_test.exs
    │   ├── headers_test.exs
    │   ├── options_test.exs
    │   └── parsers/
    │       ├── forwarded_test.exs
    │       └── generic_test.exs
    ├── remote_ip_test.exs
    └── test_helper.exs
Download .txt
SYMBOL INDEX (176 symbols across 31 files)

FILE: bench/lib/bench/inputs.ex
  class Bench.Inputs (line 1) | defmodule Bench.Inputs
    method seed (line 2) | def seed do
    method cidrs (line 13) | def cidrs(n) do
    method cidr (line 17) | def cidr do
    method cidr (line 24) | def cidr({a, b, c, d}) do
    method cidr (line 28) | def cidr({a, b, c, d, e, f, g, h}) do
    method cidr (line 32) | def cidr(ip, prefix) do
    method ips (line 36) | def ips(n) do
    method ip (line 40) | def ip do
    method ipv4 (line 47) | def ipv4 do
    method ipv6 (line 53) | def ipv6 do

FILE: bench/mix.exs
  class Bench.MixProject (line 1) | defmodule Bench.MixProject
    method project (line 4) | def project do
    method application (line 13) | def application do
    method deps (line 19) | defp deps do

FILE: integration/tests.exs
  class Integration.Tests (line 1) | defmodule Integration.Tests
    method run (line 10) | def run do
    method run (line 14) | def run(app) do
    method mix (line 26) | def mix(app, task, args \\ []) do
    method summarize (line 36) | def summarize(results) do
    method count (line 45) | def count(results) do
    method plural (line 59) | def plural(1, string), do: "1 #{string}"
    method plural (line 60) | def plural(n, string), do: "#{n} #{string}s"
    method passed (line 62) | def passed(app) do
    method failed (line 66) | def failed(app) do

FILE: integration/tests/basic/lib/basic.ex
  class Basic (line 1) | defmodule Basic

FILE: integration/tests/basic/mix.exs
  class Basic.MixProject (line 1) | defmodule Basic.MixProject
    method project (line 4) | def project do
    method application (line 13) | def application do

FILE: integration/tests/basic/test/basic_test.exs
  class BasicTest (line 1) | defmodule BasicTest
    method xff (line 7) | def xff(conn, header) do
    method call (line 11) | def call(conn, opts \\ []) do

FILE: integration/tests/custom/mix.exs
  class Custom.MixProject (line 1) | defmodule Custom.MixProject
    method project (line 4) | def project do
    method application (line 13) | def application do

FILE: integration/tests/custom/test/custom_test.exs
  class CustomTest (line 1) | defmodule CustomTest
    method call (line 15) | def call(conn, opts \\ []) do
    method from (line 19) | def from(head, opts \\ []) do

FILE: integration/tests/debug/mix.exs
  class Debug.MixProject (line 1) | defmodule Debug.MixProject
    method project (line 4) | def project do
    method application (line 13) | def application do

FILE: integration/tests/debug/test/debug_test.exs
  class DebugTest (line 1) | defmodule DebugTest
    method call (line 17) | def call(opts) do
    method from (line 21) | def from(opts) do

FILE: integration/tests/parsers/lib/parsers.ex
  class Parsers (line 1) | defmodule Parsers

FILE: integration/tests/parsers/lib/parsers/forwarding.ex
  class Parsers.Forwarding (line 1) | defmodule Parsers.Forwarding
    method parse (line 6) | def parse(value) do

FILE: integration/tests/parsers/mix.exs
  class Parsers.MixProject (line 1) | defmodule Parsers.MixProject
    method project (line 4) | def project do
    method application (line 13) | def application do

FILE: integration/tests/parsers/test/parsers_test.exs
  class ParsersTest (line 1) | defmodule ParsersTest
    method call (line 7) | def call(conn, opts \\ []) do

FILE: integration/tests/purge/mix.exs
  class Purge.MixProject (line 1) | defmodule Purge.MixProject
    method project (line 4) | def project do
    method application (line 13) | def application do

FILE: integration/tests/purge/test/purge_test.exs
  class PurgeTest (line 1) | defmodule PurgeTest
    method call (line 13) | def call(conn, opts \\ []) do
    method from (line 17) | def from(head, opts \\ []) do

FILE: lib/remote_ip.ex
  class RemoteIp (line 1) | defmodule RemoteIp
    method init (line 165) | def init(opts) do
    method call (line 179) | def call(conn, opts) do
    method from (line 221) | def from(headers, opts \\ []) do
    method ip_from (line 227) | defp ip_from(headers, opts) do
    method options_from (line 232) | defp options_from(opts) do
    method ips_from (line 238) | defp ips_from(headers, opts) do
    method forwarding_from (line 245) | defp forwarding_from(headers, opts) do
    method client_from (line 251) | defp client_from(ips, opts) do
    method client? (line 255) | defp client?(ip, opts) do
    method type (line 271) | defp type(ip, opts) do
    method contains? (line 284) | defp contains?(blocks, ip) do
    method add_metadata (line 288) | defp add_metadata(remote_ip) do

FILE: lib/remote_ip/block.ex
  class RemoteIp.Block (line 1) | defmodule RemoteIp.Block
    method encode (line 9) | def encode({a, b, c, d}) do
    method encode (line 14) | def encode({a, b, c, d, e, f, g, h}) do
    method contains? (line 19) | def contains?(%Block{proto: proto, net: net, mask: mask}, {proto, ip}) do
    method contains? (line 23) | def contains?(%Block{}, {_, _}) do
    method parse! (line 27) | def parse!(cidr) do
    method parse (line 34) | def parse(cidr) do
    method process (line 41) | defp process(:parts, [ip, prefix]) do
    method process (line 48) | defp process(:parts, [ip]) do
    method process (line 54) | defp process(:ip, address) do
    method process (line 61) | defp process(:prefix, prefix) do
    method process (line 69) | defp process(:block, {:v4, ip}) do
    method process (line 73) | defp process(:block, {:v6, ip}) do
    method process (line 89) | defp process(:block, _, prefix) do

FILE: lib/remote_ip/debugger.ex
  class RemoteIp.Debugger (line 1) | defmodule RemoteIp.Debugger
    method __log__ (line 121) | def __log__(id, inputs, output) do
    method __message__ (line 125) | def __message__(:options, [], options) do
    method __message__ (line 140) | def __message__(:headers, [], headers) do
    method __message__ (line 144) | def __message__(:forwarding, [], headers) do
    method __message__ (line 148) | def __message__(:ips, [], ips) do
    method __message__ (line 152) | def __message__(:type, [ip], type) do
    method __message__ (line 161) | def __message__(:ip, [old_conn], new_conn) do
    method __message__ (line 172) | def __message__(:ip, [], ip) do

FILE: lib/remote_ip/headers.ex
  class RemoteIp.Headers (line 1) | defmodule RemoteIp.Headers
    method take (line 25) | def take(headers, names) do
    method parse (line 64) | def parse(headers, parsers \\ RemoteIp.Options.default(:parsers)) do

FILE: lib/remote_ip/options.ex
  class RemoteIp.Options (line 1) | defmodule RemoteIp.Options
    method default (line 297) | def default(option)
    method default (line 298) | def default(:headers), do: @headers
    method default (line 299) | def default(:parsers), do: @parsers
    method default (line 300) | def default(:proxies), do: @proxies
    method default (line 301) | def default(:clients), do: @clients
    method pack (line 307) | def pack(options) do
    method pack (line 316) | defp pack(options, option) do
    method unpack (line 327) | def unpack(options) do
    method unpack (line 336) | defp unpack(options, option) do
    method evaluate (line 343) | defp evaluate(:headers, headers) do
    method evaluate (line 347) | defp evaluate(:parsers, parsers) do
    method evaluate (line 351) | defp evaluate(:proxies, proxies) do
    method evaluate (line 355) | defp evaluate(:clients, clients) do

FILE: lib/remote_ip/parser.ex
  class RemoteIp.Parser (line 1) | defmodule RemoteIp.Parser

FILE: lib/remote_ip/parsers/forwarded.ex
  class RemoteIp.Parsers.Forwarded (line 1) | defmodule RemoteIp.Parsers.Forwarded
    method parse (line 27) | def parse(header) do
    method parse_forwarded_for (line 34) | defp parse_forwarded_for(pairs) do
    method fors_from (line 41) | defp fors_from(pairs) do
    method parse_ip (line 45) | defp parse_ip(string) do
    method forwarded (line 54) | defp forwarded do
    method forwarded_element (line 58) | defp forwarded_element do
    method forwarded_pair (line 62) | defp forwarded_pair do
    method value (line 67) | defp value do
    method token (line 73) | defp token do
    method quoted_string (line 77) | defp quoted_string do
    method quoted (line 81) | defp quoted(parser) do
    method string_of (line 85) | defp string_of(parser) do
    method qdtext (line 89) | defp qdtext do
    method quoted_pair (line 96) | defp quoted_pair do
    method comma (line 102) | defp comma do
    method ip_address (line 110) | defp ip_address do
    method node_name (line 116) | defp node_name do
    method node_port (line 125) | defp node_port(previous) do
    method port (line 129) | defp port do
    method obfuscated (line 137) | defp obfuscated do
    method ipv4_address (line 145) | defp ipv4_address do
    method ipv6_address (line 154) | defp ipv6_address do

FILE: lib/remote_ip/parsers/generic.ex
  class RemoteIp.Parsers.Generic (line 1) | defmodule RemoteIp.Parsers.Generic
    method parse (line 32) | def parse(header) do
    method split_commas (line 36) | defp split_commas(header) do
    method parse_ips (line 40) | defp parse_ips(strings) do
    method parse_ip (line 49) | defp parse_ip(string) do

FILE: mix.exs
  class RemoteIp.Mixfile (line 1) | defmodule RemoteIp.Mixfile
    method project (line 4) | def project do
    method application (line 19) | def application do
    method description (line 23) | defp description do
    method package (line 28) | defp package do
    method deps (line 36) | defp deps do
    method aliases (line 47) | defp aliases do
    method dialyzer (line 51) | defp dialyzer do
    method docs (line 55) | defp docs do
    method test_coverage (line 63) | defp test_coverage() do

FILE: test/remote_ip/block_test.exs
  class RemoteIp.BlockTest (line 1) | defmodule RemoteIp.BlockTest
    method octets (line 7) | def octets(n) do
    method ipv4 (line 11) | def ipv4(octets) do
    method hextets (line 15) | def hextets(n) do
    method ipv6 (line 19) | def ipv6(hextets) do

FILE: test/remote_ip/headers_test.exs
  class RemoteIp.HeadersTest (line 1) | defmodule RemoteIp.HeadersTest
  class Custom (line 159) | defmodule Custom
    method parse (line 164) | def parse(_) do

FILE: test/remote_ip/options_test.exs
  class RemoteIp.OptionsTest (line 1) | defmodule RemoteIp.OptionsTest
  class MFA (line 4) | defmodule MFA
    method setup (line 7) | def setup do
    method get (line 12) | def get(opt) do
    method put (line 16) | def put(opt, val) do

FILE: test/remote_ip/parsers/forwarded_test.exs
  class RemoteIp.Parsers.ForwardedTest (line 1) | defmodule RemoteIp.Parsers.ForwardedTest

FILE: test/remote_ip/parsers/generic_test.exs
  class RemoteIp.Parsers.GenericTest (line 1) | defmodule RemoteIp.Parsers.GenericTest

FILE: test/remote_ip_test.exs
  class RemoteIpTest (line 1) | defmodule RemoteIpTest
    method call (line 47) | def call(conn, opts \\ []) do
  class ParserA (line 486) | defmodule ParserA
    method parse (line 489) | def parse(value) do
  class ParserB (line 495) | defmodule ParserB
    method parse (line 498) | def parse(value) do
  class ParserC (line 504) | defmodule ParserC
    method parse (line 507) | def parse(value) do
  class App (line 559) | defmodule App
    method config (line 575) | def config(var) do
    method parsers (line 579) | def parsers do
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (213K chars).
[
  {
    "path": ".formatter.exs",
    "chars": 149,
    "preview": "[\n  inputs: [\"{mix,.formatter}.exs\", \"lib/**/*.ex\", \"integration/tests.exs\"],\n  line_length: 80,\n  subdirectories: [\"tes"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 2957,
    "preview": "name: build\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nenv"
  },
  {
    "path": ".gitignore",
    "chars": 754,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build\n\n# If you run \"mix test --cover\", coverage assets end up h"
  },
  {
    "path": ".tool-versions",
    "chars": 33,
    "preview": "elixir 1.16-otp-26\nerlang 26.2.5\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 6062,
    "preview": "# Contributing\n\n**Table Of Contents**\n* [Issues](#issues)\n  * [Getting the wrong IP](#getting-the-wrong-ip)\n  * [Other i"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2016 Alex Vondrak\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 7542,
    "preview": "# RemoteIp\n\n[![build status](https://github.com/ajvondrak/remote_ip/workflows/build/badge.svg)](https://github.com/ajvon"
  },
  {
    "path": "bench/.formatter.exs",
    "chars": 97,
    "preview": "# Used by \"mix format\"\n[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "bench/.gitignore",
    "chars": 616,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "bench/README.md",
    "chars": 5096,
    "preview": "# Benchmarks\n\nFor the purposes of remote\\_ip, we need a library to (1) parse strings from CIDR notation into a usable re"
  },
  {
    "path": "bench/check.exs",
    "chars": 1205,
    "preview": "Bench.Inputs.seed\n\nips = Bench.Inputs.ips(1_000)\n\nparsed_ips = %{\n  remote_ip: Enum.map(ips, &RemoteIp.Block.encode/1),\n"
  },
  {
    "path": "bench/lib/bench/inputs.ex",
    "chars": 1196,
    "preview": "defmodule Bench.Inputs do\n  def seed do\n    seed =\n      case System.fetch_env(\"SEED\") do\n        {:ok, var} -> String.t"
  },
  {
    "path": "bench/mix.exs",
    "chars": 458,
    "preview": "defmodule Bench.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :bench,\n      version: \"0.0.0\",\n     "
  },
  {
    "path": "bench/parse.exs",
    "chars": 475,
    "preview": "Bench.Inputs.seed\n\ncidrs = Bench.Inputs.cidrs(1_000)\n\nsuite = %{\n  remote_ip: fn -> Enum.each(cidrs, &RemoteIp.Block.par"
  },
  {
    "path": "coveralls.json",
    "chars": 115,
    "preview": "{\n  \"skip_files\": [\n    \"lib/remote_ip/debugger.ex\"\n  ],\n  \"coverage_options\": {\n    \"minimum_coverage\": 100\n  }\n}\n"
  },
  {
    "path": "extras/algorithm.md",
    "chars": 8233,
    "preview": "# Algorithm\n\nTo avoid IP spoofing vulnerabilities, `RemoteIp` employs a very particular algorithm. Its work is divided i"
  },
  {
    "path": "integration/tests/basic/.formatter.exs",
    "chars": 98,
    "preview": "[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  import_deps: [:plug]\n]\n"
  },
  {
    "path": "integration/tests/basic/.gitignore",
    "chars": 616,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "integration/tests/basic/README.md",
    "chars": 266,
    "preview": "# Basic integration test\n\nThis app plugs `RemoteIp` into a simple `Plug.Router` pipeline. Sans configuration, we expect "
  },
  {
    "path": "integration/tests/basic/lib/basic.ex",
    "chars": 164,
    "preview": "defmodule Basic do\n  use Plug.Router\n\n  plug RemoteIp\n  plug :match\n  plug :dispatch\n\n  get \"/ip\" do\n    send_resp(conn,"
  },
  {
    "path": "integration/tests/basic/mix.exs",
    "chars": 264,
    "preview": "defmodule Basic.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :basic,\n      version: \"0.0.0\",\n     "
  },
  {
    "path": "integration/tests/basic/test/basic_test.exs",
    "chars": 426,
    "preview": "defmodule BasicTest do\n  use ExUnit.Case\n  use Plug.Test\n\n  import ExUnit.CaptureLog\n\n  def xff(conn, header) do\n    put"
  },
  {
    "path": "integration/tests/basic/test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  },
  {
    "path": "integration/tests/custom/.formatter.exs",
    "chars": 74,
    "preview": "[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "integration/tests/custom/.gitignore",
    "chars": 617,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "integration/tests/custom/README.md",
    "chars": 476,
    "preview": "# Custom integration test\n\nThis app customizes the subset of debug messages it wants remote\\_ip to actually log. All the"
  },
  {
    "path": "integration/tests/custom/config/config.exs",
    "chars": 140,
    "preview": "import Config\n\nconfig :logger, :console,\n  colors: [enabled: false],\n  format: \"[$level] $message\\n\"\n\nconfig :remote_ip,"
  },
  {
    "path": "integration/tests/custom/mix.exs",
    "chars": 266,
    "preview": "defmodule Custom.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :custom,\n      version: \"0.0.0\",\n   "
  },
  {
    "path": "integration/tests/custom/test/custom_test.exs",
    "chars": 1528,
    "preview": "defmodule CustomTest do\n  use ExUnit.Case\n  import ExUnit.CaptureLog\n\n  @head [\n    {\"user-agent\", \"test\"},\n    {\"x-forw"
  },
  {
    "path": "integration/tests/custom/test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  },
  {
    "path": "integration/tests/debug/.formatter.exs",
    "chars": 74,
    "preview": "[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "integration/tests/debug/.gitignore",
    "chars": 616,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "integration/tests/debug/README.md",
    "chars": 333,
    "preview": "# Debug integration test\n\nThis app compiles remote\\_ip with the configuration\n\n```elixir\nconfig :remote_ip, debug: true\n"
  },
  {
    "path": "integration/tests/debug/config/config.exs",
    "chars": 133,
    "preview": "import Config\n\nconfig :logger, :console,\n  colors: [enabled: false],\n  format: \"[$level] $message\\n\"\n\nconfig :remote_ip,"
  },
  {
    "path": "integration/tests/debug/mix.exs",
    "chars": 264,
    "preview": "defmodule Debug.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :debug,\n      version: \"0.0.0\",\n     "
  },
  {
    "path": "integration/tests/debug/test/debug_test.exs",
    "chars": 6499,
    "preview": "defmodule DebugTest do\n  use ExUnit.Case\n\n  import ExUnit.CaptureLog\n\n  @head [\n    {\"accept\", \"*/*\"},\n    {\"x-forwarded"
  },
  {
    "path": "integration/tests/debug/test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  },
  {
    "path": "integration/tests/parsers/.formatter.exs",
    "chars": 98,
    "preview": "[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  import_deps: [:plug]\n]\n"
  },
  {
    "path": "integration/tests/parsers/.gitignore",
    "chars": 618,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "integration/tests/parsers/README.md",
    "chars": 746,
    "preview": "# Parsers integration test\n\nThis app recognizes a custom header named `\"forwarding\"` which is parsed with the app's own "
  },
  {
    "path": "integration/tests/parsers/config/config.exs",
    "chars": 145,
    "preview": "import Config\n\nconfig :logger, :console,\n  colors: [enabled: false],\n  format: \"[$level] $message\\n\"\n\nconfig :remote_ip,"
  },
  {
    "path": "integration/tests/parsers/lib/parsers/forwarding.ex",
    "chars": 325,
    "preview": "defmodule Parsers.Forwarding do\n  @behaviour RemoteIp.Parser\n\n  @impl RemoteIp.Parser\n\n  def parse(value) do\n    [type, "
  },
  {
    "path": "integration/tests/parsers/lib/parsers.ex",
    "chars": 248,
    "preview": "defmodule Parsers do\n  use Plug.Router\n\n  plug RemoteIp,\n    headers: ~w[forwarding],\n    parsers: %{\"forwarding\" => Par"
  },
  {
    "path": "integration/tests/parsers/mix.exs",
    "chars": 268,
    "preview": "defmodule Parsers.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :parsers,\n      version: \"0.0.0\",\n "
  },
  {
    "path": "integration/tests/parsers/test/parsers_test.exs",
    "chars": 866,
    "preview": "defmodule ParsersTest do\n  use ExUnit.Case\n  use Plug.Test\n\n  import ExUnit.CaptureLog\n\n  def call(conn, opts \\\\ []) do\n"
  },
  {
    "path": "integration/tests/parsers/test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  },
  {
    "path": "integration/tests/purge/.formatter.exs",
    "chars": 74,
    "preview": "[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "integration/tests/purge/.gitignore",
    "chars": 616,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "integration/tests/purge/README.md",
    "chars": 1262,
    "preview": "# Purge integration test\n\nThis app enables remote\\_ip debugging, just like the [debug](../debug) integration test. Howev"
  },
  {
    "path": "integration/tests/purge/config/config.exs",
    "chars": 207,
    "preview": "import Config\n\nconfig :logger, :console,\n  format: \"[$level] $message\\n\",\n  colors: [enabled: false]\n\nconfig :logger, co"
  },
  {
    "path": "integration/tests/purge/mix.exs",
    "chars": 264,
    "preview": "defmodule Purge.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :purge,\n      version: \"0.0.0\",\n     "
  },
  {
    "path": "integration/tests/purge/test/purge_test.exs",
    "chars": 540,
    "preview": "defmodule PurgeTest do\n  use ExUnit.Case\n\n  import ExUnit.CaptureLog\n\n  @head [{\"x-forwarded-for\", \"3.14.15.9\"}]\n\n  @con"
  },
  {
    "path": "integration/tests/purge/test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  },
  {
    "path": "integration/tests.exs",
    "chars": 1636,
    "preview": "defmodule Integration.Tests do\n  @path Path.join(__DIR__, \"tests\")\n\n  if IO.ANSI.enabled?() do\n    @color \"--color\"\n  el"
  },
  {
    "path": "lib/remote_ip/block.ex",
    "chars": 2770,
    "preview": "defmodule RemoteIp.Block do\n  import Bitwise\n  alias __MODULE__\n\n  @moduledoc false\n\n  defstruct [:proto, :net, :mask]\n\n"
  },
  {
    "path": "lib/remote_ip/debugger.ex",
    "chars": 5343,
    "preview": "defmodule RemoteIp.Debugger do\n  require Logger\n\n  @moduledoc \"\"\"\n  Compile-time debugging facilities.\n\n  `RemoteIp` use"
  },
  {
    "path": "lib/remote_ip/headers.ex",
    "chars": 2475,
    "preview": "defmodule RemoteIp.Headers do\n  @moduledoc \"\"\"\n  Functions for parsing IPs from multiple types of forwarding headers.\n  "
  },
  {
    "path": "lib/remote_ip/options.ex",
    "chars": 11001,
    "preview": "defmodule RemoteIp.Options do\n  @headers ~w[forwarded x-forwarded-for x-client-ip x-real-ip]\n  @parsers %{\"forwarded\" =>"
  },
  {
    "path": "lib/remote_ip/parser.ex",
    "chars": 1502,
    "preview": "defmodule RemoteIp.Parser do\n  @moduledoc \"\"\"\n  Defines the interface for parsing headers into IP addresses.\n\n  `RemoteI"
  },
  {
    "path": "lib/remote_ip/parsers/forwarded.ex",
    "chars": 3855,
    "preview": "defmodule RemoteIp.Parsers.Forwarded do\n  use Combine\n\n  @behaviour RemoteIp.Parser\n\n  @moduledoc \"\"\"\n  [RFC 7239](https"
  },
  {
    "path": "lib/remote_ip/parsers/generic.ex",
    "chars": 1418,
    "preview": "defmodule RemoteIp.Parsers.Generic do\n  @behaviour RemoteIp.Parser\n\n  @moduledoc \"\"\"\n  Generic parser for forwarding hea"
  },
  {
    "path": "lib/remote_ip.ex",
    "chars": 8295,
    "preview": "defmodule RemoteIp do\n  import RemoteIp.Debugger\n\n  @behaviour Plug\n\n  @moduledoc \"\"\"\n  A plug to rewrite the `Plug.Conn"
  },
  {
    "path": "mix.exs",
    "chars": 1477,
    "preview": "defmodule RemoteIp.Mixfile do\n  use Mix.Project\n\n  def project do\n    [\n      app: :remote_ip,\n      version: \"1.2.0\",\n "
  },
  {
    "path": "test/.formatter.exs",
    "chars": 462,
    "preview": "[\n  inputs: [\"**/*.exs\"],\n  import_deps: [:plug],\n\n  # This is an arbitrarily long line length. While most of the code c"
  },
  {
    "path": "test/remote_ip/block_test.exs",
    "chars": 37004,
    "preview": "defmodule RemoteIp.BlockTest do\n  use ExUnit.Case, async: true\n  import Bitwise\n\n  alias RemoteIp.Block\n\n  def octets(n)"
  },
  {
    "path": "test/remote_ip/headers_test.exs",
    "chars": 4841,
    "preview": "defmodule RemoteIp.HeadersTest do\n  use ExUnit.Case, async: true\n\n  doctest RemoteIp.Headers\n\n  test \"taking from an emp"
  },
  {
    "path": "test/remote_ip/options_test.exs",
    "chars": 8027,
    "preview": "defmodule RemoteIp.OptionsTest do\n  use ExUnit.Case, async: true\n\n  defmodule MFA do\n    use Agent\n\n    def setup do\n   "
  },
  {
    "path": "test/remote_ip/parsers/forwarded_test.exs",
    "chars": 24136,
    "preview": "defmodule RemoteIp.Parsers.ForwardedTest do\n  use ExUnit.Case, async: true\n\n  alias RemoteIp.Parsers.Forwarded\n\n  doctes"
  },
  {
    "path": "test/remote_ip/parsers/generic_test.exs",
    "chars": 10325,
    "preview": "defmodule RemoteIp.Parsers.GenericTest do\n  use ExUnit.Case, async: true\n\n  alias RemoteIp.Parsers.Generic\n\n  doctest Ge"
  },
  {
    "path": "test/remote_ip_test.exs",
    "chars": 20580,
    "preview": "defmodule RemoteIpTest do\n  use ExUnit.Case, async: true\n  use Plug.Test\n\n  doctest RemoteIp\n\n  @unknown [\n    {\"forward"
  },
  {
    "path": "test/test_helper.exs",
    "chars": 32,
    "preview": "ExUnit.start(capture_log: true)\n"
  }
]

About this extraction

This page contains the full source code of the ajvondrak/remote_ip GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (195.7 KB), approximately 71.2k tokens, and a symbol index with 176 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!