mirror of
https://codeberg.org/icewind/real-ip.git
synced 2026-06-03 17:44:06 +02:00
initial version
This commit is contained in:
parent
f8f6aacd4d
commit
95b407ba29
3 changed files with 317 additions and 9 deletions
143
Cargo.lock
generated
143
Cargo.lock
generated
|
|
@ -2,6 +2,149 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
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]]
|
[[package]]
|
||||||
name = "real-ip"
|
name = "real-ip"
|
||||||
version = "0.1.0"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
||||||
|
|
|
||||||
178
src/lib.rs
178
src/lib.rs
|
|
@ -1,14 +1,174 @@
|
||||||
pub fn add(left: u64, right: u64) -> u64 {
|
//! Get the "real-ip" of an incoming request.
|
||||||
left + right
|
//!
|
||||||
|
//! 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// all hops were trusted, return the first one
|
||||||
|
first
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
/// Extracts the ip addresses from the "forwarded for" chain from a request
|
||||||
mod tests {
|
///
|
||||||
use super::*;
|
/// 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));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
if let Some(header) = headers.get("x-forwarded-for") {
|
||||||
fn it_works() {
|
let header = header.to_str().unwrap_or_default();
|
||||||
let result = add(2, 2);
|
let hops = CommaSeparatedIterator::new(header)
|
||||||
assert_eq!(result, 4);
|
.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue