expose individual header parsing functions

This commit is contained in:
Robin Appelman 2024-11-29 19:22:23 +01:00
commit adb46b46f4
2 changed files with 115 additions and 64 deletions

108
src/headers.rs Normal file
View file

@ -0,0 +1,108 @@
use comma_separated::CommaSeparatedIterator;
use rfc7239::{parse, Forwarded, NodeIdentifier, NodeName};
use std::borrow::Cow;
use std::iter::{ IntoIterator};
use std::net::IpAddr;
use std::str::FromStr;
/// Get the list of ip addresses from an `forwarded` header
///
/// # Example
///
/// ```rust
/// # use std::net::IpAddr;
/// # use real_ip::headers::*;
/// assert_eq!(
/// vec![IpAddr::from([10, 10, 10, 10]), IpAddr::from([10, 10, 10, 20])],
/// extract_forwarded_header("for=10.10.10.10, for=10.10.10.20;proto=https").collect::<Vec<_>>()
/// );
/// ```
///
/// Note: if you need the other data provided by the `forwarded` header, have a look at the [`rfc7239`](https://docs.rs/rfc7239) crate.
pub fn extract_forwarded_header(header_value: &str) -> impl DoubleEndedIterator<Item = IpAddr> + '_ {
parse(header_value).filter_map(|forward| match forward {
Ok(Forwarded {
forwarded_for:
Some(NodeIdentifier {
name: NodeName::Ip(ip),
..
}),
..
}) => Some(ip),
_ => None,
})
}
/// Get the list of ip addresses from an `x-forwarded-for` header
///
/// # Example
///
/// ```rust
/// # use std::net::IpAddr;
/// # use real_ip::headers::*;
/// assert_eq!(
/// vec![IpAddr::from([10, 10, 10, 10]), IpAddr::from([10, 10, 10, 20])],
/// extract_x_forwarded_for_header("10.10.10.10,10.10.10.20").collect::<Vec<_>>()
/// );
/// ```
pub fn extract_x_forwarded_for_header(header_value: &str) -> impl DoubleEndedIterator<Item = IpAddr> + '_ {
CommaSeparatedIterator::new(header_value)
.map(str::trim)
.flat_map(|x| IpAddr::from_str(maybe_bracketed(&maybe_quoted(x))))
}
/// Get the list of ip addresses from an `x-real-ip` header
///
/// # Example
///
/// ```rust
/// # use std::net::IpAddr;
/// # use real_ip::headers::*;
/// assert_eq!(
/// vec![IpAddr::from([10, 10, 10, 10])],
/// extract_x_forwarded_for_header("10.10.10.10").collect::<Vec<_>>()
/// );
/// ```
pub fn extract_real_ip_header(header_value: &str) -> impl DoubleEndedIterator<Item = IpAddr> + '_ {
IpAddr::from_str(maybe_bracketed(&maybe_quoted(header_value))).into_iter()
}
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
}
}

View file

@ -55,15 +55,14 @@
//! assert_eq!(Some(IpAddr::from([203, 0, 113, 10])), client_ip);
//! ```
use comma_separated::CommaSeparatedIterator;
pub mod headers;
use http::HeaderMap;
use ipnetwork::IpNetwork;
use itertools::Either;
use rfc7239::{parse, Forwarded, NodeIdentifier, NodeName};
use std::borrow::Cow;
use std::iter::{empty, once, IntoIterator};
use std::iter::{empty, once};
use std::net::IpAddr;
use std::str::FromStr;
use crate::headers::{extract_forwarded_header, extract_real_ip_header, extract_x_forwarded_for_header};
/// Get the "real-ip" of an incoming request.
///
@ -96,78 +95,22 @@ pub fn real_ip(
pub fn get_forwarded_for(headers: &HeaderMap) -> impl DoubleEndedIterator<Item = IpAddr> + '_ {
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));
return Either::Left(Either::Left(extract_forwarded_header(header)));
}
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));
return Either::Left(Either::Right(extract_x_forwarded_for_header(header)));
}
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(),
));
return Either::Right(Either::Left(extract_real_ip_header(header)));
}
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
}
}
#[allow(dead_code)]
#[doc = include_str!("../README.md")]
fn test_readme_examples() {}