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
[](https://github.com/ajvondrak/remote_ip/actions?query=workflow%3Abuild)
[](https://coveralls.io/github/ajvondrak/remote_ip?branch=main)
[](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.

```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.

```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
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
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[](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.