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:
parent
002c1b5a19
commit
f4476fbd0d
9 changed files with 457 additions and 85 deletions
|
@ -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.
|
||||
- Update `brotli` dependency to `7`.
|
||||
- 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
|
||||
|
||||
|
|
|
@ -152,6 +152,7 @@ futures-core = { version = "0.3.17", default-features = false }
|
|||
futures-util = { version = "0.3.17", default-features = false }
|
||||
itoa = "1"
|
||||
impl-more = "0.1.4"
|
||||
ipnet = "2.10.1"
|
||||
language-tags = "0.3"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
|
|
|
@ -14,6 +14,7 @@ use crate::{
|
|||
AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper,
|
||||
ServiceRequest, ServiceResponse,
|
||||
},
|
||||
trusted_proxies::TrustedProxies,
|
||||
};
|
||||
|
||||
type Guards = Vec<Box<dyn Guard>>;
|
||||
|
@ -112,17 +113,28 @@ pub struct AppConfig {
|
|||
secure: bool,
|
||||
host: String,
|
||||
addr: SocketAddr,
|
||||
trusted_proxies: TrustedProxies,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self {
|
||||
AppConfig { secure, host, addr }
|
||||
pub(crate) fn new(
|
||||
secure: bool,
|
||||
host: String,
|
||||
addr: SocketAddr,
|
||||
trusted_proxies: TrustedProxies,
|
||||
) -> Self {
|
||||
AppConfig {
|
||||
secure,
|
||||
host,
|
||||
addr,
|
||||
trusted_proxies,
|
||||
}
|
||||
}
|
||||
|
||||
/// Needed in actix-test crate. Semver exempt.
|
||||
#[doc(hidden)]
|
||||
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.
|
||||
|
@ -146,6 +158,17 @@ impl AppConfig {
|
|||
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)]
|
||||
pub(crate) fn set_host(&mut self, host: &str) {
|
||||
host.clone_into(&mut self.host);
|
||||
|
@ -168,6 +191,7 @@ impl Default for AppConfig {
|
|||
false,
|
||||
"localhost:8080".to_owned(),
|
||||
"127.0.0.1:8080".parse().unwrap(),
|
||||
TrustedProxies::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 derive_more::derive::{Display, Error};
|
||||
|
@ -6,16 +9,12 @@ use derive_more::derive::{Display, Error};
|
|||
use crate::{
|
||||
dev::{AppConfig, Payload, RequestHead},
|
||||
http::{
|
||||
header::{self, HeaderName},
|
||||
header,
|
||||
uri::{Authority, Scheme},
|
||||
},
|
||||
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.
|
||||
fn unquote(val: &str) -> &str {
|
||||
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.
|
||||
fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> {
|
||||
let hdr = req.headers.get(name)?.to_str().ok()?;
|
||||
let val = hdr.split(',').next()?.trim();
|
||||
Some(val)
|
||||
/// Extract default host from request or server configuration.
|
||||
fn default_host<'a>(req: &'a RequestHead, cfg: &'a AppConfig) -> &'a str {
|
||||
req.headers
|
||||
.get(&header::HOST)
|
||||
.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.
|
||||
|
@ -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`
|
||||
/// 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-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2
|
||||
/// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3
|
||||
|
@ -83,67 +99,151 @@ pub struct ConnectionInfo {
|
|||
|
||||
impl ConnectionInfo {
|
||||
pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo {
|
||||
let mut host = None;
|
||||
let mut scheme = None;
|
||||
let mut realip_remote_addr = None;
|
||||
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
|
||||
|
||||
for (name, val) in req
|
||||
.headers
|
||||
.get_all(&header::FORWARDED)
|
||||
.filter_map(|hdr| hdr.to_str().ok())
|
||||
// "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(','))
|
||||
// ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"]
|
||||
.flat_map(|pair| {
|
||||
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")]
|
||||
let mut host = None;
|
||||
let mut scheme = None;
|
||||
let mut realip_remote_addr = None;
|
||||
|
||||
// taking the first value for each property is correct because spec states that first
|
||||
// "for" value is client and rest are proxies; multiple values other properties have
|
||||
// no defined semantics
|
||||
//
|
||||
// > 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
|
||||
// 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
|
||||
.get_all(&header::FORWARDED)
|
||||
.filter_map(|hdr| hdr.to_str().ok())
|
||||
// "for=1.2.3.4, for=5.6.7.8; scheme=https"
|
||||
.flat_map(|vals| vals.split(','))
|
||||
// ["for=1.2.3.4", "for=5.6.7.8; scheme=https"]
|
||||
.rev();
|
||||
|
||||
match name.trim().to_lowercase().as_str() {
|
||||
"for" => realip_remote_addr.get_or_insert_with(|| bare_address(unquote(val))),
|
||||
"proto" => scheme.get_or_insert_with(|| unquote(val)),
|
||||
"host" => host.get_or_insert_with(|| unquote(val)),
|
||||
"by" => {
|
||||
// TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1
|
||||
continue;
|
||||
'forwaded: for forwarded in forwarded_list {
|
||||
for (key, value) in forwarded.split(';').map(|item| {
|
||||
let mut kv = item.splitn(2, '=');
|
||||
|
||||
(
|
||||
kv.next().map(|s| s.trim()).unwrap_or_default(),
|
||||
kv.next().map(|s| unquote(s.trim())).unwrap_or_default(),
|
||||
)
|
||||
}) {
|
||||
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" => {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
realip_remote_addr = Some(bare_address(value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if host.is_none()
|
||||
&& cfg
|
||||
.trusted_proxies()
|
||||
.trust_header(&header::X_FORWARDED_HOST)
|
||||
{
|
||||
host = req
|
||||
.headers
|
||||
.get_all(&header::X_FORWARDED_HOST)
|
||||
.filter_map(|hdr| hdr.to_str().ok())
|
||||
.flat_map(|vals| vals.split(','))
|
||||
.rev()
|
||||
.next();
|
||||
}
|
||||
|
||||
if scheme.is_none()
|
||||
&& cfg
|
||||
.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();
|
||||
}
|
||||
|
||||
(
|
||||
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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
|
||||
let scheme = scheme
|
||||
.or_else(|| first_header_value(req, &X_FORWARDED_PROTO))
|
||||
.or_else(|| req.uri.scheme().map(Scheme::as_str))
|
||||
.or_else(|| Some("https").filter(|_| cfg.secure()))
|
||||
.unwrap_or("http")
|
||||
.to_owned();
|
||||
|
||||
let host = host
|
||||
.or_else(|| first_header_value(req, &X_FORWARDED_HOST))
|
||||
.or_else(|| req.headers.get(&header::HOST)?.to_str().ok())
|
||||
.or_else(|| req.uri.authority().map(Authority::as_str))
|
||||
.unwrap_or_else(|| cfg.host())
|
||||
.to_owned();
|
||||
|
||||
let realip_remote_addr = realip_remote_addr
|
||||
.or_else(|| first_header_value(req, &X_FORWARDED_FOR))
|
||||
.map(str::to_owned);
|
||||
|
||||
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());
|
||||
|
||||
ConnectionInfo {
|
||||
host,
|
||||
|
@ -270,7 +370,7 @@ impl FromRequest for PeerAddr {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::TestRequest;
|
||||
use crate::{test::TestRequest, trusted_proxies::TrustedProxies};
|
||||
|
||||
const X_FORWARDED_FOR: &str = "x-forwarded-for";
|
||||
const X_FORWARDED_HOST: &str = "x-forwarded-host";
|
||||
|
@ -297,8 +397,15 @@ mod tests {
|
|||
}
|
||||
|
||||
#[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()
|
||||
.peer_addr(addr)
|
||||
.set_trusted_proxies(trusted_proxies)
|
||||
.insert_header((X_FORWARDED_FOR, "192.0.2.60"))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -306,18 +413,77 @@ mod tests {
|
|||
}
|
||||
|
||||
#[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()
|
||||
.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"))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
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]
|
||||
fn x_forwarded_proto_header() {
|
||||
fn x_forwarded_host_header_untrusted() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
|
||||
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"))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -325,8 +491,21 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn forwarded_header() {
|
||||
fn x_forwarded_proto_header_untrusted() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
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((
|
||||
header::FORWARDED,
|
||||
"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"));
|
||||
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((
|
||||
header::FORWARDED,
|
||||
"for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
|
||||
|
@ -353,7 +533,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn forwarded_case_sensitivity() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((header::FORWARDED, "For=192.0.2.60"))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -362,7 +544,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn forwarded_weird_whitespace() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https"))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -370,6 +554,7 @@ mod tests {
|
|||
assert_eq!(info.scheme(), "https");
|
||||
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((header::FORWARDED, " for = 1.2.3.4 "))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -378,7 +563,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn forwarded_for_quoted() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -387,7 +574,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn forwarded_for_ipv6() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -396,7 +585,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn forwarded_for_ipv6_with_port() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#))
|
||||
.to_http_request();
|
||||
let info = req.connection_info();
|
||||
|
@ -405,11 +596,29 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn forwarded_for_multiple() {
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default()
|
||||
.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 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"));
|
||||
}
|
||||
|
||||
|
|
|
@ -105,6 +105,7 @@ mod server;
|
|||
mod service;
|
||||
pub mod test;
|
||||
mod thin_data;
|
||||
pub mod trusted_proxies;
|
||||
pub(crate) mod types;
|
||||
pub mod web;
|
||||
|
||||
|
|
|
@ -898,8 +898,10 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_remote_addr_format() {
|
||||
let mut format = Format::new("%{r}a");
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
|
||||
let req = TestRequest::default()
|
||||
.peer_addr(addr)
|
||||
.insert_header((
|
||||
header::FORWARDED,
|
||||
header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
|
||||
|
|
|
@ -17,7 +17,7 @@ use actix_service::{
|
|||
#[cfg(feature = "openssl")]
|
||||
use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder};
|
||||
|
||||
use crate::{config::AppConfig, Error};
|
||||
use crate::{config::AppConfig, trusted_proxies::TrustedProxies, Error};
|
||||
|
||||
struct Socket {
|
||||
scheme: &'static str,
|
||||
|
@ -26,6 +26,7 @@ struct Socket {
|
|||
|
||||
struct Config {
|
||||
host: Option<String>,
|
||||
trusted_proxies: TrustedProxies,
|
||||
keep_alive: KeepAlive,
|
||||
client_request_timeout: Duration,
|
||||
client_disconnect_timeout: Duration,
|
||||
|
@ -110,6 +111,7 @@ where
|
|||
factory,
|
||||
config: Arc::new(Mutex::new(Config {
|
||||
host: None,
|
||||
trusted_proxies: TrustedProxies::new_local(),
|
||||
keep_alive: KeepAlive::default(),
|
||||
client_request_timeout: Duration::from_secs(5),
|
||||
client_disconnect_timeout: Duration::from_secs(1),
|
||||
|
@ -142,6 +144,14 @@ where
|
|||
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.
|
||||
///
|
||||
/// By default keep-alive is set to 5 seconds.
|
||||
|
@ -526,6 +536,7 @@ where
|
|||
.listen(format!("actix-web-service-{}", addr), lst, move || {
|
||||
let cfg = cfg.lock().unwrap();
|
||||
let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr));
|
||||
let trusted_proxies = cfg.trusted_proxies.clone();
|
||||
|
||||
let mut svc = HttpService::build()
|
||||
.keep_alive(cfg.keep_alive)
|
||||
|
@ -543,7 +554,7 @@ where
|
|||
.map_err(|err| err.into().error_response());
|
||||
|
||||
svc.finish(map_config(fac, move |_| {
|
||||
AppConfig::new(false, host.clone(), addr)
|
||||
AppConfig::new(false, host.clone(), addr, trusted_proxies.clone())
|
||||
}))
|
||||
.tcp()
|
||||
})?;
|
||||
|
@ -570,6 +581,7 @@ where
|
|||
.listen(format!("actix-web-service-{}", addr), lst, move || {
|
||||
let cfg = cfg.lock().unwrap();
|
||||
let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr));
|
||||
let trusted_proxies = cfg.trusted_proxies.clone();
|
||||
|
||||
let mut svc = HttpService::build()
|
||||
.keep_alive(cfg.keep_alive)
|
||||
|
@ -587,7 +599,7 @@ where
|
|||
.map_err(|err| err.into().error_response());
|
||||
|
||||
svc.finish(map_config(fac, move |_| {
|
||||
AppConfig::new(false, host.clone(), addr)
|
||||
AppConfig::new(false, host.clone(), addr, trusted_proxies.clone())
|
||||
}))
|
||||
.tcp_auto_h2c()
|
||||
})?;
|
||||
|
@ -646,6 +658,7 @@ where
|
|||
.listen(format!("actix-web-service-{}", addr), lst, move || {
|
||||
let c = cfg.lock().unwrap();
|
||||
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
|
||||
let trusted_proxies = c.trusted_proxies.clone();
|
||||
|
||||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
|
@ -668,7 +681,7 @@ where
|
|||
};
|
||||
|
||||
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)
|
||||
})?;
|
||||
|
@ -697,6 +710,7 @@ where
|
|||
.listen(format!("actix-web-service-{}", addr), lst, move || {
|
||||
let c = cfg.lock().unwrap();
|
||||
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
|
||||
let trusted_proxies = c.trusted_proxies.clone();
|
||||
|
||||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
|
@ -719,7 +733,7 @@ where
|
|||
};
|
||||
|
||||
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)
|
||||
})?;
|
||||
|
@ -763,6 +777,7 @@ where
|
|||
.listen(format!("actix-web-service-{}", addr), lst, move || {
|
||||
let c = cfg.lock().unwrap();
|
||||
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
|
||||
let trusted_proxies = c.trusted_proxies.clone();
|
||||
|
||||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
|
@ -785,7 +800,7 @@ where
|
|||
};
|
||||
|
||||
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)
|
||||
})?;
|
||||
|
@ -829,6 +844,7 @@ where
|
|||
.listen(format!("actix-web-service-{}", addr), lst, move || {
|
||||
let c = cfg.lock().unwrap();
|
||||
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
|
||||
let trusted_proxies = c.trusted_proxies.clone();
|
||||
|
||||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
|
@ -851,7 +867,7 @@ where
|
|||
};
|
||||
|
||||
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)
|
||||
})?;
|
||||
|
@ -894,6 +910,7 @@ where
|
|||
.listen(format!("actix-web-service-{}", addr), lst, move || {
|
||||
let c = cfg.lock().unwrap();
|
||||
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
|
||||
let trusted_proxies = c.trusted_proxies.clone();
|
||||
|
||||
let svc = HttpService::build()
|
||||
.keep_alive(c.keep_alive)
|
||||
|
@ -919,7 +936,7 @@ where
|
|||
};
|
||||
|
||||
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)
|
||||
})?;
|
||||
|
@ -956,6 +973,7 @@ where
|
|||
false,
|
||||
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
|
||||
socket_addr,
|
||||
c.trusted_proxies.clone(),
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
|
@ -1001,6 +1019,7 @@ where
|
|||
false,
|
||||
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
|
||||
socket_addr,
|
||||
c.trusted_proxies.clone(),
|
||||
);
|
||||
|
||||
fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({
|
||||
|
|
|
@ -5,6 +5,8 @@ use serde::Serialize;
|
|||
|
||||
#[cfg(feature = "cookies")]
|
||||
use crate::cookie::{Cookie, CookieJar};
|
||||
#[cfg(test)]
|
||||
use crate::trusted_proxies::TrustedProxies;
|
||||
use crate::{
|
||||
app_service::AppInitServiceState,
|
||||
config::AppConfig,
|
||||
|
@ -229,6 +231,13 @@ impl TestRequest {
|
|||
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.
|
||||
///
|
||||
/// This request builder will be useless after calling `finish()`.
|
||||
|
|
106
actix-web/src/trusted_proxies.rs
Normal file
106
actix-web/src/trusted_proxies.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue