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
+1
View File
@@ -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":"3d70b6b26686c18ceaa9df2e263cbd18ca0c499185eb55c225895006e4e16a15",".editorconfig":"abecf663d723e04a0281304c3374c27c644869716e56fa8ac59f4c92cf62730f",".github/workflows/ci.yml":"d626609116c713000e55e917546138ae538753200ee198acab3f1be8d9c2766f",".pre-commit.sh":"6ba1aad68e659850c02a9065983b488032bd710ebf6f4aebacde74da6d73c3a1",".rustfmt.toml":"a73dc1c6a2f3f4fafb2ebbdae52b2bdba5c17de1e80a0c7c35f736805ba2407f","Cargo.lock":"2c2fd0e00704a80a032ebaf3fe1cbb370deb5481aa2165758add62cf367e270a","Cargo.toml":"700e419c6e7c1702d5f6324d34fceaee2aea11d7886d6b163f705690b9ea1985","Cargo.toml.orig":"bd13db0ed14daaabfae1a21c7d7a19949c210557e234dea3a86b87792d3416bb","LICENSE":"88f5fad4a47802618bb50d15af59369713920dd30c931ebb5c21ecc47b8c57e3","README.md":"bd03becde4caf81c7c4e3003095c1da70a26a2a048e088a261df184c02b35274","src/lib.rs":"4743cc3e840bb81010da86bce72f82750adb49a0cdd80fc87290abad92add676"},"package":"39d2056bf065c8b4bce5a8898d40e175211ff4410add2a84d695845d3937c729"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "76fa005673bf00ba84beb1f7a6ec0b958c494b98"
},
"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
+117
View File
@@ -0,0 +1,117 @@
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 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
+40
View File
@@ -0,0 +1,40 @@
#!/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 --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
+104
View File
@@ -0,0 +1,104 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "client-ip"
version = "0.2.1"
dependencies = [
"forwarded-header-value",
"http",
]
[[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 = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "nonempty"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
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 = "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 = "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 = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+99
View File
@@ -0,0 +1,99 @@
# 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 = "client-ip"
version = "0.2.1"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "HTTP client IP address extractors"
readme = "README.md"
license = "MIT"
repository = "https://github.com/imbolc/client-ip"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = [
"--cfg",
"docsrs",
]
[features]
default = []
forwarded-header = ["dep:forwarded-header-value"]
[lib]
name = "client_ip"
path = "src/lib.rs"
[dependencies.forwarded-header-value]
version = "0.1"
optional = true
[dependencies.http]
version = "1"
[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
+38
View File
@@ -0,0 +1,38 @@
[package]
description = "HTTP client IP address extractors"
edition = "2024"
license = "MIT"
name = "client-ip"
repository = "https://github.com/imbolc/client-ip"
version = "0.2.1"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
# Enables `rightmost_forwarded` extractor
forwarded-header = ["dep:forwarded-header-value"]
[dependencies]
forwarded-header-value = { version = "0.1", optional = true }
http = "1"
[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.
+41
View File
@@ -0,0 +1,41 @@
# `client-ip`
[![License](https://img.shields.io/crates/l/client-ip.svg)](https://choosealicense.com/licenses/mit/)
[![Crates.io](https://img.shields.io/crates/v/client-ip.svg)](https://crates.io/crates/client-ip)
[![Docs.rs](https://docs.rs/client-ip/badge.svg)](https://docs.rs/client-ip)
Secure extraction of a client IP from [http::HeaderMap]. The code was initially
extracted from the [axum-client-ip] crate for non-axum use.
If you're maintaining client IP extraction in other frameworks based on [http],
consider using this crate, so we can handle security-sensitive code in one
place. Look at the [axum-client-ip] integration for reference.
## Supported extractors
Open an issue if there's an uncovered header or send a PR straight away.
| Extractor | Header Used | Typical Proxy / Service |
| ----------------------------- | --------------------------- | ------------------------------------------------------- |
| [`cf_connecting_ip`] | `CF-Connecting-IP` | Cloudflare |
| [`cloudfront_viewer_address`] | `CloudFront-Viewer-Address` | AWS CloudFront |
| [`fly_client_ip`] | `Fly-Client-IP` | Fly.io |
| [`rightmost_forwarded`] | `Forwarded` | Proxies supporting RFC 7239 (extracts rightmost `for=`) |
| [`rightmost_x_forwarded_for`] | `X-Forwarded-For` | Nginx, Apache, HAProxy, CDNs, LBs |
| [`true_client_ip`] | `True-Client-IP` | Cloudflare, Akamai |
| [`x_envoy_external_address`] | `X-Envoy-External-Address` | Envoy |
| [`x_real_ip`] | `X-Real-Ip` | Nginx |
## 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/client-ip/blob/main/.pre-commit.sh
[axum-client-ip]: https://github.com/imbolc/axum-client-ip
[http::HeaderMap]: https://docs.rs/http/1.3.1/http/header/struct.HeaderMap.html
[http]: https://github.com/rust-lang/rust
[license]: https://github.com/imbolc/client-ip/blob/main/LICENSE
+721
View File
@@ -0,0 +1,721 @@
#![doc = include_str!("../README.md")]
use std::net::IpAddr;
pub use error::Error;
use http::{HeaderMap, HeaderName};
type Result<T> = std::result::Result<T, Error>;
/// Extracts client IP from `CF-Connecting-IP` (Cloudflare) header
pub fn cf_connecting_ip(header_map: &HeaderMap) -> Result<IpAddr> {
ip_from_single_header(header_map, &HeaderName::from_static("cf-connecting-ip"))
}
/// Extracts client IP from `CloudFront-Viewer-Address` (AWS CloudFront) header
pub fn cloudfront_viewer_address(header_map: &HeaderMap) -> Result<IpAddr> {
const HEADER_NAME: HeaderName = HeaderName::from_static("cloudfront-viewer-address");
fn ip_from_header_value(header_value: &str) -> Result<IpAddr> {
// Spec: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-viewer-location
// Note: Both IPv4 and IPv6 addresses (in the specified format) do not contain
// non-ascii characters, so no need to handle percent-encoding.
//
// CloudFront does not use `[::]:12345` style notation for IPv6 (unfortunately),
// otherwise parsing via `SocketAddr` would be possible.
header_value
.rsplit_once(':')
.map(|(ip, _port)| ip)
.ok_or_else(|| Error::MalformedHeaderValue {
header_name: HEADER_NAME,
header_value: header_value.to_owned(),
})?
.trim()
.parse::<IpAddr>()
.map_err(|_| Error::MalformedHeaderValue {
header_name: HEADER_NAME,
header_value: header_value.to_owned(),
})
}
let header_value = AsciiHeaderValue::of_last_header(header_map, &HEADER_NAME)?;
ip_from_header_value(header_value.0)
}
/// Extracts client IP from `Fly-Client-IP` (Fly.io) header
///
/// When the 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)
pub fn fly_client_ip(header_map: &HeaderMap) -> Result<IpAddr> {
ip_from_single_header(header_map, &HeaderName::from_static("fly-client-ip"))
}
#[cfg(feature = "forwarded-header")]
/// Extracts the rightmost IP from `Forwarded` header
pub fn rightmost_forwarded(header_map: &HeaderMap) -> Result<IpAddr> {
const HEADER_NAME: HeaderName = HeaderName::from_static("forwarded");
fn ip_from_header_value(header_value: &str) -> Result<IpAddr> {
use forwarded_header_value::{ForwardedHeaderValue, Identifier};
let stanza = ForwardedHeaderValue::from_forwarded(header_value)
.map_err(|_| Error::MalformedHeaderValue {
header_name: HEADER_NAME,
header_value: header_value.to_owned(),
})?
.into_iter()
.last()
.ok_or_else(|| Error::MalformedHeaderValue {
header_name: HEADER_NAME,
header_value: header_value.to_owned(),
})?;
let forwarded_for = stanza.forwarded_for.ok_or_else(|| Error::ForwardedNoFor {
header_value: header_value.to_owned(),
})?;
match forwarded_for {
Identifier::SocketAddr(a) => Ok(a.ip()),
Identifier::IpAddr(ip) => Ok(ip),
Identifier::String(_) => Err(Error::ForwardedObfuscated {
header_value: header_value.to_owned(),
}),
Identifier::Unknown => Err(Error::ForwardedUnknown {
header_value: header_value.to_owned(),
}),
}
}
let header_value = AsciiHeaderValue::of_last_header(header_map, &HEADER_NAME)?;
ip_from_header_value(header_value.0)
}
/// Extracts the rightmost IP address from the comma-separated list in the value
/// of the last `X-Forwarded-For` header.
pub fn rightmost_x_forwarded_for(header_map: &HeaderMap) -> Result<IpAddr> {
const HEADER_NAME: HeaderName = HeaderName::from_static("x-forwarded-for");
fn ip_from_header_value(header_value: &str) -> Result<IpAddr> {
header_value
.split(',')
.next_back()
.ok_or_else(|| Error::MalformedHeaderValue {
header_name: HEADER_NAME,
header_value: header_value.to_owned(),
})?
.trim()
.parse::<IpAddr>()
.map_err(|_| Error::MalformedHeaderValue {
header_name: HEADER_NAME,
header_value: header_value.to_owned(),
})
}
let header_value = AsciiHeaderValue::of_last_header(header_map, &HEADER_NAME)?;
ip_from_header_value(header_value.0)
}
/// Extracts client IP from `True-Client-IP` (Akamai, Cloudflare) header
pub fn true_client_ip(header_map: &HeaderMap) -> Result<IpAddr> {
ip_from_single_header(header_map, &HeaderName::from_static("true-client-ip"))
}
/// Extracts client IP from `X-Envoy-External-Address` header
pub fn x_envoy_external_address(header_map: &HeaderMap) -> Result<IpAddr> {
ip_from_single_header(
header_map,
&HeaderName::from_static("x-envoy-external-address"),
)
}
/// Extracts client IP from `X-Real-Ip` (Nginx) header
pub fn x_real_ip(header_map: &HeaderMap) -> Result<IpAddr> {
ip_from_single_header(header_map, &HeaderName::from_static("x-real-ip"))
}
/// A [`http::HeaderValue`] converted to string and ensured to be valid ASCII
#[derive(Debug)]
struct AsciiHeaderValue<'a>(&'a str);
impl<'a> AsciiHeaderValue<'a> {
/// Returns value of a header that must occur only once. Multiple
/// occurrences of the header are considered a critical proxy configuration
/// error.
fn of_single_header(header_map: &'a HeaderMap, header_name: &HeaderName) -> Result<Self> {
let mut iter = header_map.get_all(header_name).into_iter();
let Some(header_value) = iter.next() else {
return Err(Error::AbsentHeader {
header_name: header_name.to_owned(),
});
};
if iter.next().is_some() {
return Err(Error::SingleHeaderRequired {
header_name: header_name.to_owned(),
});
}
header_value
.to_str()
.map_err(|_| Error::NonAsciiHeaderValue {
header_name: header_name.to_owned(),
})
.map(Self)
}
/// Returns a value of the last occurring header.
fn of_last_header(header_map: &'a HeaderMap, header_name: &HeaderName) -> Result<Self> {
header_map
.get_all(header_name)
.into_iter()
.next_back()
.ok_or_else(|| Error::AbsentHeader {
header_name: header_name.to_owned(),
})?
.to_str()
.map_err(|_| Error::NonAsciiHeaderValue {
header_name: header_name.to_owned(),
})
.map(Self)
}
/// Tries to parse the whole value as an IP.
fn parse_ip(&self, header_name: &HeaderName) -> Result<IpAddr> {
self.0
.trim()
.parse()
.map_err(|_| Error::MalformedHeaderValue {
header_name: header_name.to_owned(),
header_value: self.0.to_owned(),
})
}
}
/// Parses an IP from a header that occurs only once. Multiple
/// occurrences of the header are considered a proxy configuration error.
fn ip_from_single_header(header_map: &HeaderMap, header_name: &HeaderName) -> Result<IpAddr> {
AsciiHeaderValue::of_single_header(header_map, header_name)?.parse_ip(header_name)
}
mod error {
use std::fmt;
use http::HeaderName;
/// Errors that can occur during IP extraction
#[derive(Debug, PartialEq)]
pub enum Error {
/// The IP-related header is missing
AbsentHeader {
/// Header name
header_name: HeaderName,
},
/// Header value contains not only visible ASCII characters
NonAsciiHeaderValue {
/// Header name
header_name: HeaderName,
},
/// Header value has an unexpected format
MalformedHeaderValue {
/// Header name
header_name: HeaderName,
/// Header value
header_value: String,
},
/// Multiple occurrences of a header required to occur only once found
///
/// According to the HTTP/1.1 specification (RFC 7230, Section 3.2.2):
/// > A sender MUST NOT generate multiple header fields with the same
/// > field name in a message unless either the entire field value for
/// > that header field is defined as a comma-separated list ...
SingleHeaderRequired {
/// Header name
header_name: HeaderName,
},
#[cfg(feature = "forwarded-header")]
/// Forwarded header doesn't contain `for` directive
ForwardedNoFor {
/// Header value
header_value: String,
},
#[cfg(feature = "forwarded-header")]
/// RFC 7239 allows to [obfuscate IPs](https://www.rfc-editor.org/rfc/rfc7239.html#section-6.3)
ForwardedObfuscated {
/// Header value
header_value: String,
},
#[cfg(feature = "forwarded-header")]
/// RFC 7239 allows [unknown identifiers](https://www.rfc-editor.org/rfc/rfc7239.html#section-6.2)
ForwardedUnknown {
/// Header value
header_value: String,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AbsentHeader { header_name } => {
write!(f, "Missing required header: {header_name}")
}
Self::NonAsciiHeaderValue { header_name } => write!(
f,
"Header value contains non-ASCII characters: {header_name}",
),
Self::MalformedHeaderValue {
header_name,
header_value,
} => write!(
f,
"Malformed header value for `{header_name}`: {header_value}",
),
Self::SingleHeaderRequired { header_name } => write!(
f,
"Multiple occurrences of the header aren't allowed: {header_name}"
),
#[cfg(feature = "forwarded-header")]
Self::ForwardedNoFor { header_value } => write!(
f,
"`Forwarded` header missing `for` directive: {header_value}",
),
#[cfg(feature = "forwarded-header")]
Self::ForwardedObfuscated { header_value } => write!(
f,
"`Forwarded` header contains obfuscated IP: {header_value}",
),
#[cfg(feature = "forwarded-header")]
Self::ForwardedUnknown { header_value } => write!(
f,
"`Forwarded` header contains unknown identifier: {header_value}",
),
}
}
}
impl std::error::Error for Error {}
}
#[cfg(test)]
mod tests {
use super::*;
const VALID_IPV4: &str = "1.2.3.4";
const VALID_IPV6: &str = "1:23:4567:89ab:c:d:e:f";
fn headers<'a>(items: impl IntoIterator<Item = (&'a str, &'a str)>) -> HeaderMap {
HeaderMap::from_iter(
items
.into_iter()
.map(|(name, value)| (name.parse().unwrap(), value.parse().unwrap())),
)
}
#[test]
fn test_ascii_header_value_of_last_header() {
let header_name_str = "my-header";
let header_name = HeaderName::from_static(header_name_str);
assert_eq!(
AsciiHeaderValue::of_last_header(&headers([]), &header_name).unwrap_err(),
Error::AbsentHeader {
header_name: header_name.clone()
}
);
assert_eq!(
AsciiHeaderValue::of_last_header(&headers([(header_name_str, "ы")]), &header_name)
.unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: header_name.clone()
}
);
assert_eq!(
AsciiHeaderValue::of_last_header(&headers([(header_name_str, "foo")]), &header_name)
.unwrap()
.0,
"foo",
"single valid header"
);
assert_eq!(
AsciiHeaderValue::of_last_header(
&headers([(header_name_str, "foo"), (header_name_str, "bar")]),
&header_name
)
.unwrap()
.0,
"bar",
"multiple valid headers"
);
}
#[test]
fn test_ascii_header_value_of_single_header() {
let header_name_str = "my-header";
let header_name = HeaderName::from_static(header_name_str);
assert_eq!(
AsciiHeaderValue::of_single_header(&headers([]), &header_name).unwrap_err(),
Error::AbsentHeader {
header_name: header_name.clone()
}
);
assert_eq!(
AsciiHeaderValue::of_single_header(&headers([(header_name_str, "ы")]), &header_name)
.unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: header_name.clone()
}
);
assert_eq!(
AsciiHeaderValue::of_single_header(
&headers([(header_name_str, "foo"), (header_name_str, "bar")]),
&header_name
)
.unwrap_err(),
Error::SingleHeaderRequired {
header_name: header_name.clone()
}
);
assert_eq!(
AsciiHeaderValue::of_single_header(&headers([(header_name_str, "foo")]), &header_name)
.unwrap()
.0,
"foo"
);
}
#[test]
fn test_cf_connecting_ip() {
let header = "cf-connecting-ip";
assert_eq!(
cf_connecting_ip(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
cf_connecting_ip(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
cf_connecting_ip(&headers([(header, "foo")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "foo".into(),
}
);
assert_eq!(
cf_connecting_ip(&headers([(header, VALID_IPV4)])).unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
cf_connecting_ip(&headers([(header, VALID_IPV6)])).unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
#[test]
fn test_cloudfront_viewer_address() {
let header = "cloudfront-viewer-address";
assert_eq!(
cloudfront_viewer_address(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
cloudfront_viewer_address(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
cloudfront_viewer_address(&headers([(header, VALID_IPV4)])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: VALID_IPV4.into(),
}
);
assert_eq!(
cloudfront_viewer_address(&headers([(header, "foo:8000")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "foo:8000".into(),
}
);
let valid_header_value_v4 = format!("{VALID_IPV4}:8000");
let valid_header_value_v6 = format!("{VALID_IPV6}:8000");
assert_eq!(
cloudfront_viewer_address(&headers([(header, valid_header_value_v4.as_ref())]))
.unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
cloudfront_viewer_address(&headers([(header, valid_header_value_v6.as_ref())]))
.unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
#[test]
fn test_fly_client_ip() {
let header = "fly-client-ip";
assert_eq!(
fly_client_ip(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
fly_client_ip(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
fly_client_ip(&headers([(header, "foo")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "foo".into(),
}
);
assert_eq!(
fly_client_ip(&headers([(header, VALID_IPV4)])).unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
fly_client_ip(&headers([(header, VALID_IPV6)])).unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
#[cfg(feature = "forwarded-header")]
#[test]
fn test_rightmost_forwarded() {
let header = "forwarded";
assert_eq!(
rightmost_forwarded(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
rightmost_forwarded(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
rightmost_forwarded(&headers([(header, "foo")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "foo".into(),
}
);
assert_eq!(
rightmost_forwarded(&headers([
(header, format!("for={VALID_IPV4}").as_ref()),
(header, "proto=http"),
]))
.unwrap_err(),
Error::ForwardedNoFor {
header_value: "proto=http".into(),
}
);
assert_eq!(
rightmost_forwarded(&headers([(header, "for=unknown")])).unwrap_err(),
Error::ForwardedUnknown {
header_value: "for=unknown".into(),
}
);
assert_eq!(
rightmost_forwarded(&headers([(header, "for=_foo")])).unwrap_err(),
Error::ForwardedObfuscated {
header_value: "for=_foo".into(),
}
);
assert_eq!(
rightmost_forwarded(&headers([
(header, "proto=http"),
(header, format!("for={VALID_IPV4};proto=http").as_ref()),
]))
.unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
rightmost_forwarded(&headers([(
header,
format!("for={VALID_IPV4}:8000").as_ref()
),]))
.unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
rightmost_forwarded(&headers([(header, format!("for={VALID_IPV6}").as_ref()),]))
.unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
assert_eq!(
rightmost_forwarded(&headers([(
header,
format!("for=[{VALID_IPV6}]:8000").as_ref()
),]))
.unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
#[test]
fn test_rightmost_x_forwarded_for() {
let header = "x-forwarded-for";
assert_eq!(
rightmost_x_forwarded_for(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
rightmost_x_forwarded_for(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
rightmost_x_forwarded_for(&headers([(header, "1.2.3.4,foo")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "1.2.3.4,foo".into(),
}
);
assert_eq!(
rightmost_x_forwarded_for(&headers([(header, format!("foo,{VALID_IPV4}").as_ref())]))
.unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
rightmost_x_forwarded_for(&headers([(header, VALID_IPV6)])).unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
#[test]
fn test_true_client_ip() {
let header = "true-client-ip";
assert_eq!(
true_client_ip(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
true_client_ip(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
true_client_ip(&headers([(header, "foo")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "foo".into(),
}
);
assert_eq!(
true_client_ip(&headers([(header, VALID_IPV4)])).unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
true_client_ip(&headers([(header, VALID_IPV6)])).unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
#[test]
fn test_x_envoy_external_address() {
let header = "x-envoy-external-address";
assert_eq!(
x_envoy_external_address(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
x_envoy_external_address(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
x_envoy_external_address(&headers([(header, "foo")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "foo".into(),
}
);
assert_eq!(
x_envoy_external_address(&headers([(header, VALID_IPV4)])).unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
x_envoy_external_address(&headers([(header, VALID_IPV6)])).unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
#[test]
fn test_x_real_ip() {
let header = "x-real-ip";
assert_eq!(
x_real_ip(&headers([])).unwrap_err(),
Error::AbsentHeader {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
x_real_ip(&headers([(header, "ы")])).unwrap_err(),
Error::NonAsciiHeaderValue {
header_name: HeaderName::from_static(header)
}
);
assert_eq!(
x_real_ip(&headers([(header, "foo")])).unwrap_err(),
Error::MalformedHeaderValue {
header_name: HeaderName::from_static(header),
header_value: "foo".into(),
}
);
assert_eq!(
x_real_ip(&headers([(header, VALID_IPV4)])).unwrap(),
VALID_IPV4.parse::<IpAddr>().unwrap()
);
assert_eq!(
x_real_ip(&headers([(header, VALID_IPV6)])).unwrap(),
VALID_IPV6.parse::<IpAddr>().unwrap()
);
}
}