initial version

This commit is contained in:
Robin Appelman 2024-11-28 22:22:50 +01:00
commit 95b407ba29
3 changed files with 317 additions and 9 deletions

143
Cargo.lock generated
View file

@ -2,6 +2,149 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "bytes"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "comma-separated"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac26892eaac40bd0a28b3a1ea93da165ef30f8ffbc3ac6fea430daf7091de58"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "ipnetwork"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
dependencies = [
"serde",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "real-ip"
version = "0.1.0"
dependencies = [
"comma-separated",
"http",
"ipnetwork",
"itertools",
"rfc7239",
]
[[package]]
name = "rfc7239"
version = "0.1.1"
dependencies = [
"uncased",
]
[[package]]
name = "serde"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"

View file

@ -4,3 +4,8 @@ version = "0.1.0"
edition = "2021"
[dependencies]
http = "1.1.0"
rfc7239 = { version = "0.1.1", path = "../rfc7239" }
comma-separated = "0.1.0"
ipnetwork = "0.20.0"
itertools = "0.13.0"

View file

@ -1,14 +1,174 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
//! Get the "real-ip" of an incoming request.
//!
//! This uses the "forwarded", "x-forwarded-for" or "x-real-ip" headers set by reverse proxies.
//! To stop clients from abusing these headers, only headers set by trusted remotes will be accepted.
//!
//! Note that if multiple forwarded-for addresses are present, which can be the case when using nested reverse proxies,
//! all proxies in the chain have to be within the list of trusted proxies.
//!
//! ## Trusted proxies
//!
//! To stop clients from being able to spoof the remote ip, you are required to configure the trusted proxies
//! which are allowed to set the forwarded headers.
//!
//! Trusted proxies are configured as a list of [`IpNetwork`]s.
//!
//! ## Examples
//!
//! A request originating from 192.0.2.1, being proxied through 10.10.10.10 and 10.0.0.1 before reaching our program
//!
//! ```
//! # use http::Request;
//! # use std::net::IpAddr;
//! # use ipnetwork::IpNetwork;
//! # use real_ip::real_ip;
//! #
//! // in a real program this info would of course come from the http server
//! let incoming_ip = IpAddr::from([10, 0, 0, 1]);
//! let request = Request::builder().header("x-forwarded-for", "192.0.2.1, 10.10.10.10").body(()).unwrap();
//!
//! // the reverse-proxies in our network that we trust
//! let trusted_proxies = [
//! IpAddr::from([10, 0, 0, 1]).into(),
//! IpNetwork::new(IpAddr::from([10, 10, 10, 0]), 24).unwrap(), // 10.10.10.0/24
//! ];
//! let client_ip = real_ip(&request, incoming_ip, &trusted_proxies);
//! assert_eq!(Some(IpAddr::from([192, 0, 2, 1])), client_ip);
//! ```
//!
//! A request originating from 192.0.2.1, being proxied through 203.0.113.10 and 10.0.0.1 before reaching our program.
//! But 203.0.113.10 is not a trusted proxy, so we don't accept anything it added to the forwarded headers
//!
//! ```
//! # use http::Request;
//! # use std::net::IpAddr;
//! # use ipnetwork::IpNetwork;
//! # use real_ip::real_ip;
//! #
//! let incoming_ip = IpAddr::from([10, 0, 0, 1]);
//! let request = Request::builder().header("forwarded", "for=192.0.2.1, for=203.0.113.10;proto=https").body(()).unwrap();
//!
//! let trusted_proxies = [
//! IpAddr::from([10, 0, 0, 1]).into(),
//! IpNetwork::new(IpAddr::from([10, 10, 10, 0]), 24).unwrap(),
//! ];
//! let client_ip = real_ip(&request, incoming_ip, &trusted_proxies);
//! assert_eq!(Some(IpAddr::from([203, 0, 113, 10])), client_ip);
//! ```
use http::Request;
use ipnetwork::IpNetwork;
use itertools::Either;
use rfc7239::{parse, Forwarded, NodeIdentifier, NodeName};
use std::borrow::Cow;
use std::iter::{empty, once, IntoIterator};
use std::net::IpAddr;
use std::str::FromStr;
use comma_separated::CommaSeparatedIterator;
/// Get the "real-ip" of an incoming request.
///
/// See the [top level documentation](crate) for more usage details.
pub fn real_ip<B>(
request: &Request<B>,
remote: IpAddr,
trusted_proxies: &[IpNetwork],
) -> Option<IpAddr> {
let mut hops = get_forwarded_for(request).chain(once(remote));
let first = hops.next();
let hops = first.iter().copied().chain(hops);
'outer: for hop in hops.rev() {
for proxy in trusted_proxies {
if proxy.contains(hop) {
continue 'outer;
}
}
return Some(hop);
}
#[cfg(test)]
mod tests {
use super::*;
// all hops were trusted, return the first one
first
}
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
/// Extracts the ip addresses from the "forwarded for" chain from a request
///
/// Note that this doesn't perform any validation against clients forging the headers
pub fn get_forwarded_for<'a, B>(
request: &'a Request<B>,
) -> impl DoubleEndedIterator<Item = IpAddr> + 'a {
let headers = request.headers();
if let Some(header) = headers.get("forwarded") {
let header = header.to_str().unwrap_or_default();
let hops = parse(header).filter_map(|forward| match forward {
Ok(Forwarded {
forwarded_for:
Some(NodeIdentifier {
name: NodeName::Ip(ip),
..
}),
..
}) => Some(ip),
_ => None,
});
return Either::Left(Either::Left(hops));
}
if let Some(header) = headers.get("x-forwarded-for") {
let header = header.to_str().unwrap_or_default();
let hops = CommaSeparatedIterator::new(header)
.map(str::trim)
.flat_map(|x| IpAddr::from_str(maybe_bracketed(&maybe_quoted(x))));
return Either::Left(Either::Right(hops));
}
if let Some(header) = headers.get("x-real-ip") {
let header = header.to_str().unwrap_or_default();
return Either::Right(Either::Left(
IpAddr::from_str(maybe_bracketed(&maybe_quoted(&header))).into_iter(),
));
}
Either::Right(Either::Right(empty()))
}
enum EscapeState {
Normal,
Escaped,
}
fn maybe_quoted(x: &str) -> Cow<str> {
let mut i = x.chars();
if i.next() == Some('"') {
let mut s = String::with_capacity(x.len());
let mut state = EscapeState::Normal;
for c in i {
state = match state {
EscapeState::Normal => match c {
'"' => break,
'\\' => EscapeState::Escaped,
_ => {
s.push(c);
EscapeState::Normal
}
},
EscapeState::Escaped => {
s.push(c);
EscapeState::Normal
}
};
}
s.into()
} else {
x.into()
}
}
fn maybe_bracketed(x: &str) -> &str {
if x.as_bytes().first() == Some(&b'[') && x.as_bytes().last() == Some(&b']') {
&x[1..x.len() - 1]
} else {
x
}
}