1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-12-16 21:26:34 +00:00

feat(web): add trusted proxies feature

This commit is contained in:
Joel Wurtz 2024-12-10 11:57:15 +01:00
parent 002c1b5a19
commit f4476fbd0d
No known key found for this signature in database
GPG key ID: ED264D1967A51B0D
9 changed files with 457 additions and 85 deletions

View file

@ -5,6 +5,7 @@
- On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use. - On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use.
- Update `brotli` dependency to `7`. - Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75. - Minimum supported Rust version (MSRV) is now 1.75.
- Add trusted proxies features to allow for customizing the list of trusted proxies and headers for determining host, scheme and ip of the `ConnectionInfo` object.
## 4.9.0 ## 4.9.0

View file

@ -152,6 +152,7 @@ futures-core = { version = "0.3.17", default-features = false }
futures-util = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false }
itoa = "1" itoa = "1"
impl-more = "0.1.4" impl-more = "0.1.4"
ipnet = "2.10.1"
language-tags = "0.3" language-tags = "0.3"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"

View file

@ -14,6 +14,7 @@ use crate::{
AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper,
ServiceRequest, ServiceResponse, ServiceRequest, ServiceResponse,
}, },
trusted_proxies::TrustedProxies,
}; };
type Guards = Vec<Box<dyn Guard>>; type Guards = Vec<Box<dyn Guard>>;
@ -112,17 +113,28 @@ pub struct AppConfig {
secure: bool, secure: bool,
host: String, host: String,
addr: SocketAddr, addr: SocketAddr,
trusted_proxies: TrustedProxies,
} }
impl AppConfig { impl AppConfig {
pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self { pub(crate) fn new(
AppConfig { secure, host, addr } secure: bool,
host: String,
addr: SocketAddr,
trusted_proxies: TrustedProxies,
) -> Self {
AppConfig {
secure,
host,
addr,
trusted_proxies,
}
} }
/// Needed in actix-test crate. Semver exempt. /// Needed in actix-test crate. Semver exempt.
#[doc(hidden)] #[doc(hidden)]
pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self {
AppConfig::new(secure, host, addr) AppConfig::new(secure, host, addr, TrustedProxies::default())
} }
/// Server host name. /// Server host name.
@ -146,6 +158,17 @@ impl AppConfig {
self.addr self.addr
} }
/// Returns the trusted proxies list.
pub fn trusted_proxies(&self) -> &TrustedProxies {
&self.trusted_proxies
}
/// Set the trusted proxies
#[cfg(test)]
pub fn set_trusted_proxies(&mut self, proxies: TrustedProxies) {
self.trusted_proxies = proxies;
}
#[cfg(test)] #[cfg(test)]
pub(crate) fn set_host(&mut self, host: &str) { pub(crate) fn set_host(&mut self, host: &str) {
host.clone_into(&mut self.host); host.clone_into(&mut self.host);
@ -168,6 +191,7 @@ impl Default for AppConfig {
false, false,
"localhost:8080".to_owned(), "localhost:8080".to_owned(),
"127.0.0.1:8080".parse().unwrap(), "127.0.0.1:8080".parse().unwrap(),
TrustedProxies::default(),
) )
} }
} }

View file

@ -1,4 +1,7 @@
use std::{convert::Infallible, net::SocketAddr}; use std::{
convert::Infallible,
net::{IpAddr, SocketAddr},
};
use actix_utils::future::{err, ok, Ready}; use actix_utils::future::{err, ok, Ready};
use derive_more::derive::{Display, Error}; use derive_more::derive::{Display, Error};
@ -6,16 +9,12 @@ use derive_more::derive::{Display, Error};
use crate::{ use crate::{
dev::{AppConfig, Payload, RequestHead}, dev::{AppConfig, Payload, RequestHead},
http::{ http::{
header::{self, HeaderName}, header,
uri::{Authority, Scheme}, uri::{Authority, Scheme},
}, },
FromRequest, HttpRequest, ResponseError, FromRequest, HttpRequest, ResponseError,
}; };
static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
/// Trim whitespace then any quote marks. /// Trim whitespace then any quote marks.
fn unquote(val: &str) -> &str { fn unquote(val: &str) -> &str {
val.trim().trim_start_matches('"').trim_end_matches('"') val.trim().trim_start_matches('"').trim_end_matches('"')
@ -35,11 +34,25 @@ fn bare_address(val: &str) -> &str {
} }
} }
/// Extracts and trims first value for given header name. /// Extract default host from request or server configuration.
fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> { fn default_host<'a>(req: &'a RequestHead, cfg: &'a AppConfig) -> &'a str {
let hdr = req.headers.get(name)?.to_str().ok()?; req.headers
let val = hdr.split(',').next()?.trim(); .get(&header::HOST)
Some(val) .and_then(|v| v.to_str().ok())
// skip host header if HTTP/2, we should use :authority instead
.filter(|_| req.version < actix_http::Version::HTTP_2)
.or_else(|| req.uri.authority().map(Authority::as_str))
// @TODO can we get the sni host if in secure context ?
.unwrap_or_else(|| cfg.host())
}
/// Extract default scheme from request or server configuration.
fn default_scheme<'a>(req: &'a RequestHead, cfg: &'a AppConfig) -> &'a str {
req.uri
.scheme()
.map(Scheme::as_str)
.or_else(|| Some("https").filter(|_| cfg.secure()))
.unwrap_or("http")
} }
/// HTTP connection information. /// HTTP connection information.
@ -70,6 +83,9 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<
/// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded` /// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded`
/// is preferred. /// is preferred.
/// ///
/// Header are parsed only if the peer address is trusted and the header is trusted, otherwise the
/// request is considered to be direct and the headers are ignored.
///
/// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239 /// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239
/// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2 /// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2
/// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3 /// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3
@ -83,67 +99,151 @@ pub struct ConnectionInfo {
impl ConnectionInfo { impl ConnectionInfo {
pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo {
let (host, scheme, peer_addr, realip_remote_addr) =
match req.peer_addr.map(|addr| addr.ip()) {
// since we don't have a peer address, we can't determine the real IP and we cannot trust any headers
// set the host and scheme to the server's configuration
None => (
default_host(req, cfg).to_string(),
default_scheme(req, cfg).to_string(),
None,
None,
),
Some(ip) => {
if !cfg.trusted_proxies().trust_ip(&ip) {
// if the peer address is not trusted, we can't trust the headers
// set the host and scheme to the server's configuration
(
default_host(req, cfg).to_string(),
default_scheme(req, cfg).to_string(),
Some(ip.to_string()),
None,
)
} else {
// if the peer address is trusted, we can start to check trusted header to get correct information
let mut host = None; let mut host = None;
let mut scheme = None; let mut scheme = None;
let mut realip_remote_addr = None; let mut realip_remote_addr = None;
for (name, val) in req // first check the forwarded header if it is trusted
if cfg.trusted_proxies().trust_header(&header::FORWARDED) {
// quote from RFC 7239:
// A proxy server that wants to add a new "Forwarded" header field value
// can either append it to the last existing "Forwarded" header field
// after a comma separator or add a new field at the end of the header
// block.
// --- https://datatracker.ietf.org/doc/html/rfc7239#section-4
// so we get the values in reverse order as we want to get the first untrusted value
let forwarded_list = req
.headers .headers
.get_all(&header::FORWARDED) .get_all(&header::FORWARDED)
.filter_map(|hdr| hdr.to_str().ok()) .filter_map(|hdr| hdr.to_str().ok())
// "for=1.2.3.4, for=5.6.7.8; scheme=https" // "for=1.2.3.4, for=5.6.7.8; scheme=https"
.flat_map(|val| val.split(';'))
// ["for=1.2.3.4, for=5.6.7.8", " scheme=https"]
.flat_map(|vals| vals.split(',')) .flat_map(|vals| vals.split(','))
// ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"] // ["for=1.2.3.4", "for=5.6.7.8; scheme=https"]
.flat_map(|pair| { .rev();
let mut items = pair.trim().splitn(2, '=');
Some((items.next()?, items.next()?))
})
{
// [(name , val ), ... ]
// [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")]
// taking the first value for each property is correct because spec states that first 'forwaded: for forwarded in forwarded_list {
// "for" value is client and rest are proxies; multiple values other properties have for (key, value) in forwarded.split(';').map(|item| {
// no defined semantics let mut kv = item.splitn(2, '=');
//
// > In a chain of proxy servers where this is fully utilized, the first
// > "for" parameter will disclose the client where the request was first
// > made, followed by any subsequent proxy identifiers.
// --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2
match name.trim().to_lowercase().as_str() { (
"for" => realip_remote_addr.get_or_insert_with(|| bare_address(unquote(val))), kv.next().map(|s| s.trim()).unwrap_or_default(),
"proto" => scheme.get_or_insert_with(|| unquote(val)), kv.next().map(|s| unquote(s.trim())).unwrap_or_default(),
"host" => host.get_or_insert_with(|| unquote(val)), )
}) {
match key.to_lowercase().as_str() {
"for" => {
if let Ok(ip) = bare_address(value).parse::<IpAddr>() {
if cfg.trusted_proxies().trust_ip(&ip) {
host = None;
scheme = None;
realip_remote_addr = None;
continue 'forwaded;
}
}
realip_remote_addr = Some(bare_address(value));
}
"proto" => {
scheme = Some(value);
}
"host" => {
host = Some(value);
}
"by" => { "by" => {
// TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1 // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1
}
_ => {}
}
}
break 'forwaded;
}
}
if realip_remote_addr.is_none()
&& cfg.trusted_proxies().trust_header(&header::X_FORWARDED_FOR)
{
for value in req
.headers
.get_all(&header::X_FORWARDED_FOR)
.filter_map(|hdr| hdr.to_str().ok())
.flat_map(|vals| vals.split(','))
.rev()
{
if let Ok(ip) = bare_address(value).parse::<IpAddr>() {
if cfg.trusted_proxies().trust_ip(&ip) {
continue; continue;
} }
_ => continue,
};
} }
let scheme = scheme realip_remote_addr = Some(bare_address(value));
.or_else(|| first_header_value(req, &X_FORWARDED_PROTO)) break;
.or_else(|| req.uri.scheme().map(Scheme::as_str)) }
.or_else(|| Some("https").filter(|_| cfg.secure())) }
.unwrap_or("http")
.to_owned();
let host = host if host.is_none()
.or_else(|| first_header_value(req, &X_FORWARDED_HOST)) && cfg
.or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) .trusted_proxies()
.or_else(|| req.uri.authority().map(Authority::as_str)) .trust_header(&header::X_FORWARDED_HOST)
.unwrap_or_else(|| cfg.host()) {
.to_owned(); host = req
.headers
.get_all(&header::X_FORWARDED_HOST)
.filter_map(|hdr| hdr.to_str().ok())
.flat_map(|vals| vals.split(','))
.rev()
.next();
}
let realip_remote_addr = realip_remote_addr if scheme.is_none()
.or_else(|| first_header_value(req, &X_FORWARDED_FOR)) && cfg
.map(str::to_owned); .trusted_proxies()
.trust_header(&header::X_FORWARDED_PROTO)
{
scheme = req
.headers
.get_all(&header::X_FORWARDED_PROTO)
.filter_map(|hdr| hdr.to_str().ok())
.flat_map(|vals| vals.split(','))
.rev()
.next();
}
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); (
host.unwrap_or_else(|| default_host(req, cfg)).to_string(),
scheme
.unwrap_or_else(|| default_scheme(req, cfg))
.to_string(),
Some(ip.to_string()),
realip_remote_addr.map(|s| s.to_string()),
)
}
}
};
ConnectionInfo { ConnectionInfo {
host, host,
@ -270,7 +370,7 @@ impl FromRequest for PeerAddr {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test::TestRequest; use crate::{test::TestRequest, trusted_proxies::TrustedProxies};
const X_FORWARDED_FOR: &str = "x-forwarded-for"; const X_FORWARDED_FOR: &str = "x-forwarded-for";
const X_FORWARDED_HOST: &str = "x-forwarded-host"; const X_FORWARDED_HOST: &str = "x-forwarded-host";
@ -297,8 +397,15 @@ mod tests {
} }
#[test] #[test]
fn x_forwarded_for_header() { fn x_forwarded_for_header_trusted() {
let mut trusted_proxies = TrustedProxies::new_local();
trusted_proxies.add_trusted_header(header::X_FORWARDED_FOR);
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.set_trusted_proxies(trusted_proxies)
.insert_header((X_FORWARDED_FOR, "192.0.2.60")) .insert_header((X_FORWARDED_FOR, "192.0.2.60"))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -306,18 +413,77 @@ mod tests {
} }
#[test] #[test]
fn x_forwarded_host_header() { fn x_forwarded_for_header_trusted_multiple() {
let mut trusted_proxies = TrustedProxies::new_local();
trusted_proxies
.add_trusted_proxy("192.0.2.60")
.expect("failed to add trusted proxy");
trusted_proxies.add_trusted_header(header::X_FORWARDED_FOR);
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.set_trusted_proxies(trusted_proxies)
.append_header((X_FORWARDED_FOR, "240.10.56.47"))
.append_header((X_FORWARDED_FOR, "192.0.2.60"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("240.10.56.47"));
}
#[test]
fn x_forwarded_for_header_untrusted() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default()
.peer_addr(addr)
.insert_header((X_FORWARDED_FOR, "192.0.2.60"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("127.0.0.1"));
}
#[test]
fn x_forwarded_host_header_trusted() {
let mut trusted_proxies = TrustedProxies::new_local();
trusted_proxies.add_trusted_header(header::X_FORWARDED_HOST);
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default()
.peer_addr(addr)
.set_trusted_proxies(trusted_proxies)
.insert_header((X_FORWARDED_HOST, "192.0.2.60")) .insert_header((X_FORWARDED_HOST, "192.0.2.60"))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
assert_eq!(info.host(), "192.0.2.60"); assert_eq!(info.host(), "192.0.2.60");
assert_eq!(info.realip_remote_addr(), None); assert_eq!(info.realip_remote_addr(), Some("127.0.0.1"));
} }
#[test] #[test]
fn x_forwarded_proto_header() { fn x_forwarded_host_header_untrusted() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((X_FORWARDED_HOST, "192.0.2.60"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.host(), "localhost:8080");
assert_eq!(info.realip_remote_addr(), Some("127.0.0.1"));
}
#[test]
fn x_forwarded_proto_header_trusted() {
let mut trusted_proxies = TrustedProxies::new_local();
trusted_proxies.add_trusted_header(header::X_FORWARDED_PROTO);
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default()
.peer_addr(addr)
.set_trusted_proxies(trusted_proxies)
.insert_header((X_FORWARDED_PROTO, "https")) .insert_header((X_FORWARDED_PROTO, "https"))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -325,8 +491,21 @@ mod tests {
} }
#[test] #[test]
fn forwarded_header() { fn x_forwarded_proto_header_untrusted() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((X_FORWARDED_PROTO, "https"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.scheme(), "http");
}
#[test]
fn forwarded_header() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default()
.peer_addr(addr)
.insert_header(( .insert_header((
header::FORWARDED, header::FORWARDED,
"for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
@ -339,6 +518,7 @@ mod tests {
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header(( .insert_header((
header::FORWARDED, header::FORWARDED,
"for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
@ -353,7 +533,9 @@ mod tests {
#[test] #[test]
fn forwarded_case_sensitivity() { fn forwarded_case_sensitivity() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((header::FORWARDED, "For=192.0.2.60")) .insert_header((header::FORWARDED, "For=192.0.2.60"))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -362,7 +544,9 @@ mod tests {
#[test] #[test]
fn forwarded_weird_whitespace() { fn forwarded_weird_whitespace() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https")) .insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https"))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -370,6 +554,7 @@ mod tests {
assert_eq!(info.scheme(), "https"); assert_eq!(info.scheme(), "https");
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((header::FORWARDED, " for = 1.2.3.4 ")) .insert_header((header::FORWARDED, " for = 1.2.3.4 "))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -378,7 +563,9 @@ mod tests {
#[test] #[test]
fn forwarded_for_quoted() { fn forwarded_for_quoted() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#)) .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -387,7 +574,9 @@ mod tests {
#[test] #[test]
fn forwarded_for_ipv6() { fn forwarded_for_ipv6() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#)) .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -396,7 +585,9 @@ mod tests {
#[test] #[test]
fn forwarded_for_ipv6_with_port() { fn forwarded_for_ipv6_with_port() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#)) .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
@ -405,11 +596,29 @@ mod tests {
#[test] #[test]
fn forwarded_for_multiple() { fn forwarded_for_multiple() {
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17")) .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17"))
.to_http_request(); .to_http_request();
let info = req.connection_info(); let info = req.connection_info();
// takes the first value
// takes the last untrusted value
assert_eq!(info.realip_remote_addr(), Some("198.51.100.17"));
let mut trusted_proxies = TrustedProxies::new_local();
trusted_proxies
.add_trusted_proxy("198.51.100.17")
.expect("Failed to add trusted proxy");
let req = TestRequest::default()
.set_trusted_proxies(trusted_proxies)
.peer_addr(addr)
.insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17"))
.to_http_request();
let info = req.connection_info();
// takes the last untrusted value
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
} }

View file

@ -105,6 +105,7 @@ mod server;
mod service; mod service;
pub mod test; pub mod test;
mod thin_data; mod thin_data;
pub mod trusted_proxies;
pub(crate) mod types; pub(crate) mod types;
pub mod web; pub mod web;

View file

@ -898,8 +898,10 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_remote_addr_format() { async fn test_remote_addr_format() {
let mut format = Format::new("%{r}a"); let mut format = Format::new("%{r}a");
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
.peer_addr(addr)
.insert_header(( .insert_header((
header::FORWARDED, header::FORWARDED,
header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),

View file

@ -17,7 +17,7 @@ use actix_service::{
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder}; use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder};
use crate::{config::AppConfig, Error}; use crate::{config::AppConfig, trusted_proxies::TrustedProxies, Error};
struct Socket { struct Socket {
scheme: &'static str, scheme: &'static str,
@ -26,6 +26,7 @@ struct Socket {
struct Config { struct Config {
host: Option<String>, host: Option<String>,
trusted_proxies: TrustedProxies,
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_request_timeout: Duration, client_request_timeout: Duration,
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
@ -110,6 +111,7 @@ where
factory, factory,
config: Arc::new(Mutex::new(Config { config: Arc::new(Mutex::new(Config {
host: None, host: None,
trusted_proxies: TrustedProxies::new_local(),
keep_alive: KeepAlive::default(), keep_alive: KeepAlive::default(),
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
client_disconnect_timeout: Duration::from_secs(1), client_disconnect_timeout: Duration::from_secs(1),
@ -142,6 +144,14 @@ where
self self
} }
/// Sets server trusted proxies configuration.
///
/// By default server trusts loopback, link-local, and private networks and Forwarded Header.
pub fn trusted_proxies(self, val: TrustedProxies) -> Self {
self.config.lock().unwrap().trusted_proxies = val;
self
}
/// Sets server keep-alive preference. /// Sets server keep-alive preference.
/// ///
/// By default keep-alive is set to 5 seconds. /// By default keep-alive is set to 5 seconds.
@ -526,6 +536,7 @@ where
.listen(format!("actix-web-service-{}", addr), lst, move || { .listen(format!("actix-web-service-{}", addr), lst, move || {
let cfg = cfg.lock().unwrap(); let cfg = cfg.lock().unwrap();
let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr));
let trusted_proxies = cfg.trusted_proxies.clone();
let mut svc = HttpService::build() let mut svc = HttpService::build()
.keep_alive(cfg.keep_alive) .keep_alive(cfg.keep_alive)
@ -543,7 +554,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(false, host.clone(), addr) AppConfig::new(false, host.clone(), addr, trusted_proxies.clone())
})) }))
.tcp() .tcp()
})?; })?;
@ -570,6 +581,7 @@ where
.listen(format!("actix-web-service-{}", addr), lst, move || { .listen(format!("actix-web-service-{}", addr), lst, move || {
let cfg = cfg.lock().unwrap(); let cfg = cfg.lock().unwrap();
let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr));
let trusted_proxies = cfg.trusted_proxies.clone();
let mut svc = HttpService::build() let mut svc = HttpService::build()
.keep_alive(cfg.keep_alive) .keep_alive(cfg.keep_alive)
@ -587,7 +599,7 @@ where
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(false, host.clone(), addr) AppConfig::new(false, host.clone(), addr, trusted_proxies.clone())
})) }))
.tcp_auto_h2c() .tcp_auto_h2c()
})?; })?;
@ -646,6 +658,7 @@ where
.listen(format!("actix-web-service-{}", addr), lst, move || { .listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap(); let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let trusted_proxies = c.trusted_proxies.clone();
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
@ -668,7 +681,7 @@ where
}; };
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr, trusted_proxies.clone())
})) }))
.rustls_with_config(config.clone(), acceptor_config) .rustls_with_config(config.clone(), acceptor_config)
})?; })?;
@ -697,6 +710,7 @@ where
.listen(format!("actix-web-service-{}", addr), lst, move || { .listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap(); let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let trusted_proxies = c.trusted_proxies.clone();
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
@ -719,7 +733,7 @@ where
}; };
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr, trusted_proxies.clone())
})) }))
.rustls_021_with_config(config.clone(), acceptor_config) .rustls_021_with_config(config.clone(), acceptor_config)
})?; })?;
@ -763,6 +777,7 @@ where
.listen(format!("actix-web-service-{}", addr), lst, move || { .listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap(); let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let trusted_proxies = c.trusted_proxies.clone();
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
@ -785,7 +800,7 @@ where
}; };
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr, trusted_proxies.clone())
})) }))
.rustls_0_22_with_config(config.clone(), acceptor_config) .rustls_0_22_with_config(config.clone(), acceptor_config)
})?; })?;
@ -829,6 +844,7 @@ where
.listen(format!("actix-web-service-{}", addr), lst, move || { .listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap(); let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let trusted_proxies = c.trusted_proxies.clone();
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
@ -851,7 +867,7 @@ where
}; };
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr, trusted_proxies.clone())
})) }))
.rustls_0_23_with_config(config.clone(), acceptor_config) .rustls_0_23_with_config(config.clone(), acceptor_config)
})?; })?;
@ -894,6 +910,7 @@ where
.listen(format!("actix-web-service-{}", addr), lst, move || { .listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap(); let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let trusted_proxies = c.trusted_proxies.clone();
let svc = HttpService::build() let svc = HttpService::build()
.keep_alive(c.keep_alive) .keep_alive(c.keep_alive)
@ -919,7 +936,7 @@ where
}; };
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr, trusted_proxies.clone())
})) }))
.openssl_with_config(acceptor.clone(), acceptor_config) .openssl_with_config(acceptor.clone(), acceptor_config)
})?; })?;
@ -956,6 +973,7 @@ where
false, false,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
socket_addr, socket_addr,
c.trusted_proxies.clone(),
); );
let fac = factory() let fac = factory()
@ -1001,6 +1019,7 @@ where
false, false,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
socket_addr, socket_addr,
c.trusted_proxies.clone(),
); );
fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({

View file

@ -5,6 +5,8 @@ use serde::Serialize;
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
use crate::cookie::{Cookie, CookieJar}; use crate::cookie::{Cookie, CookieJar};
#[cfg(test)]
use crate::trusted_proxies::TrustedProxies;
use crate::{ use crate::{
app_service::AppInitServiceState, app_service::AppInitServiceState,
config::AppConfig, config::AppConfig,
@ -229,6 +231,13 @@ impl TestRequest {
self self
} }
/// Set the trusted proxies for this request.
#[cfg(test)]
pub fn set_trusted_proxies(mut self, trusted_proxies: TrustedProxies) -> Self {
self.config.set_trusted_proxies(trusted_proxies);
self
}
/// Finalizes test request. /// Finalizes test request.
/// ///
/// This request builder will be useless after calling `finish()`. /// This request builder will be useless after calling `finish()`.

View file

@ -0,0 +1,106 @@
use std::net::IpAddr;
use actix_http::header::{HeaderName, FORWARDED};
use ipnet::{AddrParseError, IpNet};
/// TrustedProxies is a helper struct to manage trusted proxies and headers
///
/// This is used to determine if information from a request can be trusted or not.
///
/// By default, it trusts the following:
/// - IPV4 Loopback
/// - IPV4 Private Networks
/// - IPV6 Loopback
/// - IPV6 Private Networks
///
/// It also trusts the `FORWARDED` header by default.
///
/// # Example
/// ```
/// use actix_web::trusted_proxies::TrustedProxies;
/// use actix_web::{web, App, HttpResponse, HttpServer};
///
/// let mut trusted_proxies = TrustedProxies::new_local();
/// trusted_proxies.add_trusted_proxy("168.10.0.0/16").unwrap();
/// trusted_proxies.add_trusted_header("X-Forwarded-For".parse().unwrap());
///
/// HttpServer::new(|| {
/// App::new()
/// .service(web::resource("/").to(|| async { "hello world" }))
/// }).trusted_proxies(trusted_proxies);
/// ```
#[derive(Debug, Clone)]
pub struct TrustedProxies(Vec<IpNet>, Vec<HeaderName>);
impl Default for TrustedProxies {
fn default() -> Self {
Self::new_local()
}
}
impl TrustedProxies {
/// Create a new TrustedProxies instance with no trusted proxies or headers
pub fn new() -> Self {
Self(vec![], vec![])
}
/// Create a new TrustedProxies instance with local and private networks and FORWARDED header trusted
pub fn new_local() -> Self {
Self(
vec![
// IPV4 Loopback
"127.0.0.0/8".parse().unwrap(),
// IPV4 Private Networks
"10.0.0.0/8".parse().unwrap(),
"172.16.0.0/12".parse().unwrap(),
"192.168.0.0/16".parse().unwrap(),
// IPV6 Loopback
"::1/128".parse().unwrap(),
// IPV6 Private network
"fd00::/8".parse().unwrap(),
],
vec![FORWARDED],
)
}
/// Add a trusted header to the list of trusted headers
pub fn add_trusted_header(&mut self, header: HeaderName) {
self.1.push(header);
}
/// Add a trusted proxy to the list of trusted proxies
///
/// proxy can be an IP address or a CIDR
pub fn add_trusted_proxy(&mut self, proxy: &str) -> Result<(), AddrParseError> {
match proxy.parse() {
Ok(v) => {
self.0.push(v);
Ok(())
}
Err(e) => match proxy.parse::<IpAddr>() {
Ok(v) => {
self.0.push(IpNet::from(v));
Ok(())
}
_ => Err(e),
},
}
}
/// Check if a remote address is trusted given the list of trusted proxies
pub fn trust_ip(&self, remote_addr: &IpAddr) -> bool {
for proxy in &self.0 {
if proxy.contains(remote_addr) {
return true;
}
}
false
}
/// Check if a header is trusted
pub fn trust_header(&self, header: &HeaderName) -> bool {
self.1.contains(header)
}
}