src/utils.rs (104 lines of code) (raw):
use crate::{endpoints::EndpointState, errors::ClassifyError};
use actix_web::{web::Data, HttpRequest};
use std::net::IpAddr;
pub trait RequestClientIp<S> {
/// Determine the IP address of the client making a request, based on network
/// information and headers.
///
/// Actix has a method to do this, but it returns a string, and doesn't strip
/// off ports if present, so it is difficult to use.
fn client_ip(&self) -> Result<IpAddr, ClassifyError>;
}
pub trait RequestTraceIps<'a> {
/// Iterate all known proxy and client IPs, starting with the IPs closest to
/// the server, and ending with the alleged client.
fn trace_ips(&'a self) -> Vec<IpAddr>;
}
impl RequestClientIp<EndpointState> for HttpRequest {
fn client_ip(&self) -> Result<IpAddr, ClassifyError> {
let trusted_proxy_list = &self
.app_data::<Data<EndpointState>>()
.expect("Expected app state")
.trusted_proxies;
let is_trusted_ip =
|ip: &&IpAddr| trusted_proxy_list.iter().any(|range| range.contains(*ip));
self.trace_ips()
.iter()
.find(|ip| !is_trusted_ip(ip))
.ok_or_else(|| ClassifyError::new("Could not determine IP"))
.copied()
}
}
impl<'a> RequestTraceIps<'a> for HttpRequest {
fn trace_ips(&'a self) -> Vec<IpAddr> {
let mut trace: Vec<IpAddr> = Vec::new();
if let Some(peer_addr) = self.peer_addr() {
trace.push(peer_addr.ip());
}
if let Some(x_forwarded_for) = self.headers().get("X-Forwarded-For") {
if let Ok(header) = x_forwarded_for.to_str() {
let mut header_ips: Vec<IpAddr> =
header.split(',').flat_map(|ip| ip.trim().parse()).collect();
header_ips.reverse();
trace.append(&mut header_ips);
}
}
trace
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::test::TestRequest;
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn trace_ip_works() {
let req = TestRequest::get()
.insert_header(("x-forwarded-for", "1.2.3.4, 5.6.7.8, 9.10.11.12"))
.to_http_request();
assert_eq!(
req.trace_ips(),
vec![
IpAddr::V4(Ipv4Addr::new(9, 10, 11, 12)),
IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8)),
IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),
],
"IPs in x-forwarded-for should be iterated in reverse order",
);
}
// Note that in all of the below tests, there aren't any networks involved,
// so the requests don't have a peer address. As such, the X-Forwarded-For
// header is the only thing considered to determine the client IP. Actix
// doesn't seem to provide a way to create a request with a mocked peer
// address.
#[test]
fn get_client_ip_no_proxies() -> Result<(), Box<dyn std::error::Error + 'static>> {
let _sys = actix::System::new();
let state = EndpointState::default();
assert_eq!(
state.trusted_proxies.len(),
0,
"Precondition: no trusted proxies by default"
);
let req = TestRequest::get()
.insert_header(("x-forwarded-for", "1.2.3.4, 5.6.7.8"))
.app_data(Data::new(state))
.to_http_request();
assert_eq!(
req.client_ip()?,
IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8)),
"With no proxies, the right-most ip should be used"
);
Ok(())
}
#[test]
fn get_client_ip_one_proxies() -> Result<(), Box<dyn std::error::Error + 'static>> {
let _sys = actix::System::new();
let state = EndpointState {
trusted_proxies: vec!["5.6.7.8/32".parse()?],
..EndpointState::default()
};
let req = TestRequest::get()
.insert_header(("x-forwarded-for", "1.2.3.4, 5.6.7.8"))
.app_data(Data::new(state))
.to_http_request();
assert_eq!(
req.client_ip()?,
IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),
"With one proxy, the second-from-the-right ip should be used"
);
Ok(())
}
#[test]
fn get_client_ip_too_many_proxies() -> Result<(), Box<dyn std::error::Error + 'static>> {
let _sys = actix::System::new();
let state = EndpointState {
trusted_proxies: vec!["5.6.7.8/32".parse()?, "1.2.3.4/32".parse()?],
..EndpointState::default()
};
let req = TestRequest::get()
.insert_header(("x-forwarded-for", "1.2.3.4, 5.6.7.8"))
.app_data(Data::new(state))
.to_http_request();
assert!(
req.client_ip().is_err(),
"With too many proxies configured, no ip is given"
);
Ok(())
}
}