Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"49a93f01c298b5043629479d76b884aea9b2e2ed9f60ff0400f34c508c45ca68",".editorconfig":"abecf663d723e04a0281304c3374c27c644869716e56fa8ac59f4c92cf62730f",".github/workflows/ci.yml":"fa24419da3124e50abfad24b4964e36abb12842f773216e2d581db2775596938",".pre-commit.sh":"c1be31d4006017f409745c6f8c9b41b60277943bda87f0224ee1183c23569639",".rustfmt.toml":"a73dc1c6a2f3f4fafb2ebbdae52b2bdba5c17de1e80a0c7c35f736805ba2407f","Cargo.lock":"37358cbc66b883d7bdeb84f940abbc470930df77b7f4b9b979f554430c6a906c","Cargo.toml":"1f53d5e51d2e520b9d97915a339943c3e508527c2a848bd71c1b2e02357be432","Cargo.toml.orig":"d100425834c2180c842bd94f517a6318f51263548c4eb8cadb406ba07f44566e","LICENSE":"88f5fad4a47802618bb50d15af59369713920dd30c931ebb5c21ecc47b8c57e3","README.md":"545cb8d533355224ef7a7018b85c2c50a7e9c4ab43d88e49cc019bd7608cb0ff","examples/configurable.rs":"3e7d492c003153c25f435cdab170a25c29aa18257fdf6aedf08dfeea854faaba","examples/tracing.rs":"1c62d4f9ceaa1102b42c286389eb6b504f458787f0675e9c485125d5546cbfb8","src/lib.rs":"b19163b87c0649927d31b93906c28a536948a06f1c4471c5101106046d0d19aa"},"package":"a8ba1af5b620232acf37f2eb6d22151ea465491e0b4c25f552d1990f64ec5a67"}
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "a79604a3299768856008d687480e5ac07de9bb04"
},
"path_in_vcs": ""
}
+11
View File
@@ -0,0 +1,11 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
[*.toml]
indent_size = 2
@@ -0,0 +1,120 @@
name: CI
on:
push:
branches:
- main
- ci
pull_request:
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Nightly Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
components: rustfmt
- name: Rustfmt
run: cargo +nightly fmt -- --check
clippy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Stable Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: clippy
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
rustdoc:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Stable Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Rustdoc
run: cargo rustdoc --all-features -- -D warnings
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Stable Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Test no default features
run: cargo test --lib --no-default-features
- name: Test default features
run: cargo test --all-targets
- name: Test forwarded-header feature
run: cargo test --all-targets --features forwarded-header
- name: Test docs
run: cargo test --doc
typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check typos
uses: crate-ci/typos@master
with:
files: .
cargo_sort:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Stable Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install cargo-sort
run: cargo install --locked cargo-sort
- name: Check `Cargo.toml` sort
run: cargo sort -c
machete:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Stable Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install `cargo-machete`
run: cargo install --locked cargo-machete
- name: Check unused Cargo dependencies
run: cargo machete
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env sh
set -eu
# Linking the script as the pre-commit hook
SCRIPT_PATH=$(realpath "$0")
HOOK_PATH=$(git rev-parse --git-dir)/hooks/pre-commit
if [ "$(realpath "$HOOK_PATH")" != "$SCRIPT_PATH" ]; then
printf "Link this script as the git pre-commit hook to avoid further manual running? (y/N): "
read -r link_hook
case "$link_hook" in
[Yy])
ln -sf "$SCRIPT_PATH" "$HOOK_PATH"
;;
esac
fi
set -x
# Install tools
cargo clippy --version >/dev/null 2>&1 || rustup component add clippy
cargo machete --version >/dev/null 2>&1 || cargo install --locked cargo-machete
cargo sort --version >/dev/null 2>&1 || cargo install --locked cargo-sort
typos --version >/dev/null 2>&1 || cargo install --locked typos-cli
rustup toolchain list | grep -q 'nightly' || rustup toolchain install nightly
cargo +nightly fmt --version >/dev/null 2>&1 || rustup component add rustfmt --toolchain nightly
# Checks
typos .
cargo machete
cargo +nightly fmt -- --check
cargo sort -c
cargo clippy --all-targets --all-features -- -D warnings
cargo rustdoc --all-features -- -D warnings
cargo test --doc
cargo test --lib --no-default-features
cargo test --all-targets
cargo test --all-targets --features forwarded-header
+3
View File
@@ -0,0 +1,3 @@
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
wrap_comments = true
+786
View File
@@ -0,0 +1,786 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "axum"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"bytes",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde_core",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-client-ip"
version = "1.3.1"
dependencies = [
"axum",
"client-ip",
"envy",
"http-body-util",
"hyper",
"serde",
"tokio",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "client-ip"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a759f2bdd2ab8c8049cfe47697603493e164936abfa60370dc8f76116779df"
dependencies = [
"forwarded-header-value",
"http",
]
[[package]]
name = "envy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "forwarded-header-value"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
dependencies = [
"nonempty",
"thiserror",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"pin-utils",
]
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "nonempty"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "tokio"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
"http",
"http-body",
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+150
View File
@@ -0,0 +1,150 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "axum-client-ip"
version = "1.3.1"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Client IP address extractors for Axum"
readme = "README.md"
license = "MIT"
repository = "https://github.com/imbolc/axum-client-ip"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = [
"--cfg",
"docsrs",
]
[features]
connect-info = ["axum/tokio"]
default = [
"serde",
"connect-info",
]
forwarded-header = ["client-ip/forwarded-header"]
serde = ["dep:serde"]
[lib]
name = "axum_client_ip"
path = "src/lib.rs"
[[example]]
name = "configurable"
path = "examples/configurable.rs"
[[example]]
name = "tracing"
path = "examples/tracing.rs"
[dependencies.axum]
version = "0.8"
default-features = false
[dependencies.client-ip]
version = "0.2"
[dependencies.serde]
version = "1"
features = ["derive"]
optional = true
[dev-dependencies.axum]
version = "0.8"
features = ["http1"]
default-features = false
[dev-dependencies.envy]
version = "0.4"
[dev-dependencies.http-body-util]
version = "0.1"
[dev-dependencies.hyper]
version = "1"
[dev-dependencies.tokio]
version = "1"
features = ["full"]
[dev-dependencies.tower]
version = "0.5"
features = ["util"]
[dev-dependencies.tower-http]
version = "0.6"
features = ["trace"]
[dev-dependencies.tracing]
version = "0.1"
[dev-dependencies.tracing-subscriber]
version = "0.3"
features = ["env-filter"]
[lints.clippy.all]
level = "warn"
priority = -1
[lints.rust]
missing_docs = "deny"
unsafe_code = "forbid"
[lints.rust.future_incompatible]
level = "deny"
priority = -2
[lints.rust.keyword_idents]
level = "deny"
priority = -2
[lints.rust.let_underscore]
level = "deny"
priority = -2
[lints.rust.nonstandard_style]
level = "deny"
priority = -2
[lints.rust.refining_impl_trait]
level = "deny"
priority = -2
[lints.rust.rust_2018_compatibility]
level = "deny"
priority = -2
[lints.rust.rust_2018_idioms]
level = "deny"
priority = -2
[lints.rust.rust_2021_compatibility]
level = "deny"
priority = -2
[lints.rust.rust_2024_compatibility]
level = "deny"
priority = -2
[lints.rust.unreachable_pub]
level = "warn"
priority = -1
[lints.rust.unused]
level = "warn"
priority = -1
+54
View File
@@ -0,0 +1,54 @@
[package]
description = "Client IP address extractors for Axum"
edition = "2024"
license = "MIT"
name = "axum-client-ip"
repository = "https://github.com/imbolc/axum-client-ip"
version = "1.3.1"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["serde", "connect-info"]
# Enables `ConnectInfo` extractor
connect-info = ["axum/tokio"]
# Enables `RightmostForwarded` extractor
forwarded-header = ["client-ip/forwarded-header"]
# Enables `ClientIpSource` serde compatibility
serde = ["dep:serde"]
[dependencies]
axum = { version = "0.8", default-features = false }
client-ip = "0.2"
serde = { version = "1", features = ["derive"], optional = true }
[dev-dependencies]
axum = { version = "0.8", default-features = false, features = ["http1"] }
envy = "0.4"
http-body-util = "0.1"
hyper = "1"
tokio = { version = "1", features = ["full"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[lints.rust]
unsafe_code = "forbid"
future_incompatible = { level = "deny", priority = -2 }
keyword_idents = { level = "deny", priority = -2 }
let_underscore = { level = "deny", priority = -2 }
missing_docs = "deny"
nonstandard_style = { level = "deny", priority = -2 }
refining_impl_trait = { level = "deny", priority = -2 }
rust_2018_compatibility = { level = "deny", priority = -2 }
rust_2018_idioms = { level = "deny", priority = -2 }
rust_2021_compatibility = { level = "deny", priority = -2 }
rust_2024_compatibility = { level = "deny", priority = -2 }
unreachable_pub = { level = "warn", priority = -1 }
unused = { level = "warn", priority = -1 }
[lints.clippy]
all = { level = "warn", priority = -1 }
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 imbolc <me@imbolc.name>
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.
+73
View File
@@ -0,0 +1,73 @@
# `axum-client-ip`
[![License](https://img.shields.io/crates/l/axum-client-ip.svg)](https://choosealicense.com/licenses/mit/)
[![Crates.io](https://img.shields.io/crates/v/axum-client-ip.svg)](https://crates.io/crates/axum-client-ip)
[![Docs.rs](https://docs.rs/axum-client-ip/badge.svg)](https://docs.rs/axum-client-ip)
Client IP address extractors for the [Axum] web framework. The crate is just a
thin wrapper around a framework-independent [client-ip] crate.
## V1 breaking changes
- Removed `InsecureClientIp` and related "leftmost" IP logic. The library now
focuses solely on secure extraction based on trusted headers.
- Renamed `SecureClientIp` to `ClientIp`.
- Renamed `SecureClientIpSource` to `ClientIpSource`.
The changes are triggered by
["rightmost" IP extraction bug](https://github.com/imbolc/axum-client-ip/issues/32).
## Configurable vs specific extractors
There's a configurable [`ClientIp`] extractor you can use to make your
application independent from a proxy it can run behind (if any) and also
separate extractors for each proxy / source header.
| Extractor / `ClientIpSource` Variant | Header Used | Typical Proxy / Service |
|--------------------------------------| --------------------------- |---------------------------------------------------------|
| [`CfConnectingIp`] | `CF-Connecting-IP` | Cloudflare |
| [`CloudFrontViewerAddress`] | `CloudFront-Viewer-Address` | AWS CloudFront |
| [`FlyClientIp`] | `Fly-Client-IP` | Fly.io |
| [`RightmostForwarded`] | `Forwarded` | Proxies supporting RFC 7239 (extracts rightmost `for=`) |
| [`RightmostXForwardedFor`] | `X-Forwarded-For` | Nginx, Apache, HAProxy, CDNs, LBs |
| [`TrueClientIp`] | `True-Client-IP` | Cloudflare, Akamai |
| [`XEnvoyExternalAddress`] | `X-Envoy-External-Address` | Envoy, Istio |
| [`XRealIp`] | `X-Real-Ip` | Nginx |
| [`ConnectInfo`] | N/A (uses socket address) | No proxy, e.g. listening directly to 80 port |
## Configurable extractor
The configurable extractor assumes initializing [`ClientIpSource`] at runtime
(e.g. with an environment variable). This makes sense when you ship a
pre-compiled binary, people meant to use in different environments. Here's an
initialization [example].
## Specific extractors
Specific extractors don't require runtime initialization, but you'd have to
recompile your binary when you change proxy server.
```rust,no_run
// With the renaming, you have to change only one line when you change proxy
use axum_client_ip::XRealIp as ClientIp;
async fn handler(ClientIp(ip): ClientIp) {
todo!()
}
```
## Contributing
- please run [.pre-commit.sh] before sending a PR, it will check everything
## License
This project is licensed under the [MIT license][license].
[.pre-commit.sh]:
https://github.com/imbolc/axum-client-ip/blob/main/.pre-commit.sh
[Axum]: https://github.com/tokio-rs/axum
[client-ip]: https://github.com/imbolc/client-ip
[example]:
https://github.com/imbolc/axum-client-ip/blob/main/examples/configurable.rs
[license]: https://github.com/imbolc/axum-client-ip/blob/main/LICENSE
@@ -0,0 +1,41 @@
//! An example of configuring `ClientIp` using an environment variable
//!
//! Don't forget to set the variable before running, e.g.:
//! ```sh
//! IP_SOURCE=ConnectInfo cargo run --example configurable
//! ```
use std::net::SocketAddr;
use axum::{Router, routing::get};
use axum_client_ip::{ClientIp, ClientIpSource};
#[derive(serde::Deserialize)]
struct Config {
ip_source: ClientIpSource,
}
async fn handler(ClientIp(ip): ClientIp) -> String {
ip.to_string()
}
#[tokio::main]
async fn main() {
let config: Config = envy::from_env().unwrap();
let app = Router::new()
.route("/", get(handler))
// The line you're probably looking for :)
.layer(config.ip_source.into_extension());
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
println!("Listening on http://localhost:3000/");
axum::serve(
listener,
// Required for `ClientIpSource::ConnectInfo`
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap()
}
+76
View File
@@ -0,0 +1,76 @@
//! An example of integration with Tracing
use std::net::SocketAddr;
use axum::{
Router,
extract::{self, FromRequestParts},
http::{self},
middleware::{self, Next},
routing::get,
};
use axum_client_ip::{ClientIp, ClientIpSource};
use tokio::net::TcpListener;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use tracing::{Span, info, info_span, level_filters::LevelFilter};
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::TRACE.into())
.from_env_lossy(),
)
.with(fmt::layer())
.init();
let app = Router::new()
.route(
"/",
get(async || {
info!("hi");
"Hello, World!"
}),
)
.layer(
ServiceBuilder::new()
// Hardcode IP source, look into `examples/configurable.rs` for runtime
// configuration
.layer(ClientIpSource::ConnectInfo.into_extension())
// Create a request span with a placeholder for IP
.layer(
TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| {
info_span!(
"request",
method = %request.method(),
uri = %request.uri(),
ip = tracing::field::Empty
)
}),
)
// Extract IP and fill the span placeholder
.layer(middleware::from_fn(
async |request: extract::Request, next: Next| {
let (mut parts, body) = request.into_parts();
if let Ok(ip) = ClientIp::from_request_parts(&mut parts, &()).await {
let span = Span::current();
span.record("ip", ip.0.to_string());
}
next.run(extract::Request::from_parts(parts, body)).await
},
)),
);
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
let listener = TcpListener::bind(&addr).await.unwrap();
println!("Listening on http://localhost:3000/");
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap()
}
+590
View File
@@ -0,0 +1,590 @@
#![doc = include_str!("../README.md")]
#[cfg(feature = "connect-info")]
use std::net::SocketAddr;
use std::{error::Error, fmt, marker::Sync, net::IpAddr, str::FromStr};
#[cfg(feature = "connect-info")]
use axum::extract::ConnectInfo;
use axum::{
extract::{Extension, FromRequestParts},
http::{StatusCode, request::Parts},
response::{IntoResponse, Response},
};
/// Defines an extractor
macro_rules! define_extractor {
(
$(#[$meta:meta])*
$newtype:ident,
$extractor:path
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy)]
pub struct $newtype(pub std::net::IpAddr);
impl $newtype {
fn ip_from_headers(headers: &axum::http::HeaderMap) -> Result<std::net::IpAddr, Rejection> {
Ok($extractor(&headers)?)
}
}
impl<S> axum::extract::FromRequestParts<S> for $newtype
where
S: Sync,
{
type Rejection = Rejection;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
Self::ip_from_headers(&parts.headers).map(Self)
}
}
};
}
define_extractor!(
/// Extracts an IP from `CF-Connecting-IP` (Cloudflare) header
CfConnectingIp,
client_ip::cf_connecting_ip
);
define_extractor!(
/// Extracts an IP from `CloudFront-Viewer-Address` (AWS CloudFront) header
CloudFrontViewerAddress,
client_ip::cloudfront_viewer_address
);
define_extractor!(
/// Extracts an IP from `Fly-Client-IP` (Fly.io) header
///
/// When [`FlyClientIp`] extractor is run for health check path,
/// provide required `Fly-Client-IP` header through
/// [`services.http_checks.headers`](https://fly.io/docs/reference/configuration/#services-http_checks)
/// or [`http_service.checks.headers`](https://fly.io/docs/reference/configuration/#services-http_checks)
FlyClientIp,
client_ip::fly_client_ip
);
#[cfg(feature = "forwarded-header")]
define_extractor!(
/// Extracts the rightmost IP from `Forwarded` header
RightmostForwarded,
client_ip::rightmost_forwarded
);
define_extractor!(
/// Extracts the rightmost IP from `X-Forwarded-For` header
RightmostXForwardedFor,
client_ip::rightmost_x_forwarded_for
);
define_extractor!(
/// Extracts an IP from `True-Client-IP` (Akamai, Cloudflare) header
TrueClientIp,
client_ip::true_client_ip
);
define_extractor!(
/// Extracts an IP from `X-Envoy-External-Address` (Envoy, Istio) header
XEnvoyExternalAddress,
client_ip::x_envoy_external_address
);
define_extractor!(
/// Extracts an IP from `X-Real-Ip` (Nginx) header
XRealIp,
client_ip::x_real_ip
);
/// Client IP extractor with configurable source
///
/// The configuration would include knowing the header the last proxy (the
/// one you own or the one your cloud server provides) is using to store
/// user connection IP. Then you'd need to pass a corresponding
/// [`ClientIpSource`] variant into the [`axum::routing::Router::layer`] as
/// an extension. Look at the [example][].
///
/// [example]: https://github.com/imbolc/axum-client-ip/blob/main/examples/integration.rs
#[derive(Debug, Clone, Copy)]
pub struct ClientIp(pub IpAddr);
/// [`ClientIp`] source configuration
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ClientIpSource {
/// IP from the `CF-Connecting-IP` header
CfConnectingIp,
/// IP from the `CloudFront-Viewer-Address` header
CloudFrontViewerAddress,
#[cfg(feature = "connect-info")]
/// IP from the [`axum::extract::ConnectInfo`]
ConnectInfo,
/// IP from the `Fly-Client-IP` header
FlyClientIp,
#[cfg(feature = "forwarded-header")]
/// Rightmost IP from the `Forwarded` header
RightmostForwarded,
/// Rightmost IP from the `X-Forwarded-For` header
RightmostXForwardedFor,
/// IP from the `True-Client-IP` header
TrueClientIp,
/// IP from the `X-Envoy-External-Address` address
XEnvoyExternalAddress,
/// IP from the `X-Real-Ip` header
XRealIp,
}
impl ClientIpSource {
/// Wraps [`ClientIpSource`] into the [`axum::extract::Extension`]
/// for passing to [`axum::routing::Router::layer`]
pub const fn into_extension(self) -> Extension<Self> {
Extension(self)
}
}
/// Invalid [`ClientIpSource`]
#[derive(Debug)]
pub struct ParseClientIpSourceError(String);
impl fmt::Display for ParseClientIpSourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Invalid ClientIpSource value {}", self.0)
}
}
impl Error for ParseClientIpSourceError {}
impl FromStr for ClientIpSource {
type Err = ParseClientIpSourceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"CfConnectingIp" => Self::CfConnectingIp,
"CloudFrontViewerAddress" => Self::CloudFrontViewerAddress,
#[cfg(feature = "connect-info")]
"ConnectInfo" => Self::ConnectInfo,
"FlyClientIp" => Self::FlyClientIp,
#[cfg(feature = "forwarded-header")]
"RightmostForwarded" => Self::RightmostForwarded,
"RightmostXForwardedFor" => Self::RightmostXForwardedFor,
"TrueClientIp" => Self::TrueClientIp,
"XEnvoyExternalAddress" => Self::XEnvoyExternalAddress,
"XRealIp" => Self::XRealIp,
_ => return Err(ParseClientIpSourceError(s.to_string())),
})
}
}
// ensure to update tests::client_ip_source_display_impl_matches_from_str_impl
impl fmt::Display for ClientIpSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
ClientIpSource::CfConnectingIp => "CfConnectingIp",
ClientIpSource::CloudFrontViewerAddress => "CloudFrontViewerAddress",
#[cfg(feature = "connect-info")]
ClientIpSource::ConnectInfo => "ConnectInfo",
ClientIpSource::FlyClientIp => "FlyClientIp",
#[cfg(feature = "forwarded-header")]
ClientIpSource::RightmostForwarded => "RightmostForwarded",
ClientIpSource::RightmostXForwardedFor => "RightmostXForwardedFor",
ClientIpSource::TrueClientIp => "TrueClientIp",
ClientIpSource::XEnvoyExternalAddress => "XEnvoyExternalAddress",
ClientIpSource::XRealIp => "XRealIp",
})
}
}
impl<S> FromRequestParts<S> for ClientIp
where
S: Sync,
{
type Rejection = Rejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let Some(ip_source) = parts.extensions.get() else {
return Err(Rejection::NoClientIpSource);
};
match ip_source {
ClientIpSource::CfConnectingIp => CfConnectingIp::ip_from_headers(&parts.headers),
ClientIpSource::CloudFrontViewerAddress => {
CloudFrontViewerAddress::ip_from_headers(&parts.headers)
}
#[cfg(feature = "connect-info")]
ClientIpSource::ConnectInfo => parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map(|ConnectInfo(addr)| addr.ip())
.ok_or_else(|| Rejection::NoConnectInfo),
ClientIpSource::FlyClientIp => FlyClientIp::ip_from_headers(&parts.headers),
#[cfg(feature = "forwarded-header")]
ClientIpSource::RightmostForwarded => {
RightmostForwarded::ip_from_headers(&parts.headers)
}
ClientIpSource::RightmostXForwardedFor => {
RightmostXForwardedFor::ip_from_headers(&parts.headers)
}
ClientIpSource::TrueClientIp => TrueClientIp::ip_from_headers(&parts.headers),
ClientIpSource::XEnvoyExternalAddress => {
XEnvoyExternalAddress::ip_from_headers(&parts.headers)
}
ClientIpSource::XRealIp => XRealIp::ip_from_headers(&parts.headers),
}
.map(Self)
}
}
/// Rejection type for IP extractors
#[non_exhaustive]
#[derive(Debug, PartialEq)]
pub enum Rejection {
#[cfg(feature = "connect-info")]
/// No [`axum::extract::ConnectInfo`] in extensions
NoConnectInfo,
/// No [`ClientIpSource`] in extensions
NoClientIpSource,
/// [`client_ip::Error`]
ClientIp(client_ip::Error),
}
impl From<client_ip::Error> for Rejection {
fn from(value: client_ip::Error) -> Self {
Self::ClientIp(value)
}
}
impl fmt::Display for Rejection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "connect-info")]
Rejection::NoConnectInfo => {
write!(f, "Add `axum::extract::ConnectInfo` to request extensions")
}
Rejection::NoClientIpSource => write!(
f,
"Add `axum_client_ip::ClientIpSource` to request extensions"
),
Rejection::ClientIp(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for Rejection {}
impl IntoResponse for Rejection {
fn into_response(self) -> Response {
let title = match self {
#[cfg(feature = "connect-info")]
Self::NoConnectInfo => "500 Axum Misconfiguration",
Self::NoClientIpSource => "500 Axum Misconfiguration",
Self::ClientIp { .. } => "500 Proxy Server Misconfiguration",
};
let footer = "(the request is rejected by axum-client-ip)";
let text = format!("{title}\n\n{self}\n\n{footer}");
(StatusCode::INTERNAL_SERVER_ERROR, text).into_response()
}
}
#[cfg(test)]
mod tests {
use axum::{
Router,
body::Body,
http::{Request, StatusCode},
routing::get,
};
use http_body_util::BodyExt;
use tower::ServiceExt;
#[cfg(feature = "forwarded-header")]
use super::RightmostForwarded;
use super::{
CfConnectingIp, ClientIpSource, FlyClientIp, RightmostXForwardedFor, TrueClientIp,
XEnvoyExternalAddress, XRealIp,
};
use crate::CloudFrontViewerAddress;
const VALID_IPV4: &str = "1.2.3.4";
const VALID_IPV6: &str = "1:23:4567:89ab:c:d:e:f";
async fn body_to_string(body: Body) -> String {
let bytes = body.collect().await.unwrap().to_bytes();
String::from_utf8_lossy(&bytes).into()
}
#[tokio::test]
async fn cf_connecting_ip() {
let header = "cf-connecting-ip";
fn app() -> Router {
Router::new().route(
"/",
get(|ip: CfConnectingIp| async move { ip.0.to_string() }),
)
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV4)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV6)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}
#[tokio::test]
async fn cloudfront_viewer_address() {
let header = "cloudfront-viewer-address";
let valid_header_value_v4 = format!("{VALID_IPV4}:8000");
let valid_header_value_v6 = format!("{VALID_IPV6}:8000");
fn app() -> Router {
Router::new().route(
"/",
get(|ip: CloudFrontViewerAddress| async move { ip.0.to_string() }),
)
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(header, &valid_header_value_v4)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
let req = Request::builder()
.uri("/")
.header(header, &valid_header_value_v6)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}
#[tokio::test]
async fn fly_client_ip() {
let header = "fly-client-ip";
fn app() -> Router {
Router::new().route("/", get(|ip: FlyClientIp| async move { ip.0.to_string() }))
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV4)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV6)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}
#[cfg(feature = "forwarded-header")]
#[tokio::test]
async fn rightmost_forwarded() {
let header = "forwarded";
fn app() -> Router {
Router::new().route(
"/",
get(|ip: RightmostForwarded| async move { ip.0.to_string() }),
)
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(header, format!("for=[{VALID_IPV6}]:8000"))
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
let req = Request::builder()
.uri("/")
.header("Forwarded", r#"for="_mdn""#)
.header("Forwarded", r#"For="[2001:db8:cafe::17]:4711""#)
.header("Forwarded", r#"for=192.0.2.60;proto=http;by=203.0.113.43"#)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, "192.0.2.60");
}
#[tokio::test]
async fn rightmost_x_forwarded_for() {
fn app() -> Router {
Router::new().route(
"/",
get(|ip: RightmostXForwardedFor| async move { ip.0.to_string() }),
)
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(
"X-Forwarded-For",
"1.1.1.1, foo, 2001:db8:85a3:8d3:1319:8a2e:370:7348",
)
.header("X-Forwarded-For", "bar")
.header("X-Forwarded-For", format!("2.2.2.2, {VALID_IPV4}"))
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
}
#[tokio::test]
async fn true_client_ip() {
let header = "true-client-ip";
fn app() -> Router {
Router::new().route("/", get(|ip: TrueClientIp| async move { ip.0.to_string() }))
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV4)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV6)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}
#[tokio::test]
async fn x_envoy_external_address() {
let header = "x-envoy-external-address";
fn app() -> Router {
Router::new().route(
"/",
get(|ip: XEnvoyExternalAddress| async move { ip.0.to_string() }),
)
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV4)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV6)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}
#[tokio::test]
async fn x_real_ip() {
let header = "x-real-ip";
fn app() -> Router {
Router::new().route("/", get(|ip: XRealIp| async move { ip.0.to_string() }))
}
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV4)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV4);
let req = Request::builder()
.uri("/")
.header(header, VALID_IPV6)
.body(Body::empty())
.unwrap();
let resp = app().oneshot(req).await.unwrap();
assert_eq!(body_to_string(resp.into_body()).await, VALID_IPV6);
}
#[test]
fn client_ip_source_display_impl_matches_from_str_impl() {
use std::str::FromStr;
#[inline]
fn assert_match(variant: ClientIpSource) {
assert_eq!(
variant,
ClientIpSource::from_str(variant.to_string().as_str()).unwrap()
);
}
assert_match(ClientIpSource::CfConnectingIp);
assert_match(ClientIpSource::CloudFrontViewerAddress);
#[cfg(feature = "connect-info")]
assert_match(ClientIpSource::ConnectInfo);
assert_match(ClientIpSource::FlyClientIp);
#[cfg(feature = "forwarded-header")]
assert_match(ClientIpSource::RightmostForwarded);
assert_match(ClientIpSource::RightmostXForwardedFor);
assert_match(ClientIpSource::TrueClientIp);
assert_match(ClientIpSource::XEnvoyExternalAddress);
assert_match(ClientIpSource::XRealIp);
}
}