v0.34.0_11
This commit is contained in:
1 parent
dd2502dd11
commit
943d51b528
4 files changed
+142
-33
No files matched your search
+42
-28
@@ -268,36 +268,50 @@ impl Compiled {
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let peer_text = cached.is_none().then(|| peer_ip.to_string());
|
||||
let ipv6_node = (cached.is_none() && peer_ip.is_ipv6()).then(|| node(peer_ip));
|
||||
let peer_header = if let Some(cached) = cached {
|
||||
cached.peer_header.clone()
|
||||
// Repeated HTTP/1.1 requests generally share Host and TLS state.
|
||||
// Cache only the untrusted-peer metadata for this accepted socket;
|
||||
// X-Forwarded-Uri, original URL and all trust decisions remain per-request.
|
||||
let target_header: HeaderValue = target.parse()?;
|
||||
let mut values = if let Some(values) =
|
||||
cached.and_then(|peer| peer.cached_untrusted(authority, tls))
|
||||
{
|
||||
values
|
||||
} else {
|
||||
HeaderValue::from_str(peer_text.as_deref().unwrap_or(""))?
|
||||
let peer_text = cached.is_none().then(|| peer_ip.to_string());
|
||||
let ipv6_node = (cached.is_none() && peer_ip.is_ipv6()).then(|| node(peer_ip));
|
||||
let peer_header = if let Some(cached) = cached {
|
||||
cached.peer_header.clone()
|
||||
} else {
|
||||
HeaderValue::from_str(peer_text.as_deref().unwrap_or(""))?
|
||||
};
|
||||
let by_text = cached.is_none().then(|| node(canonical(local.ip())));
|
||||
let peer_node = cached.map_or_else(
|
||||
|| if peer_ip.is_ipv4() { peer_text.as_deref().unwrap_or("") }
|
||||
else { ipv6_node.as_deref().unwrap_or("") },
|
||||
|cached| cached.peer_node.as_str(),
|
||||
);
|
||||
let by_node = cached.map_or_else(|| by_text.as_deref().unwrap_or(""),
|
||||
|cached| cached.by_node.as_str());
|
||||
let forwarded = format!(
|
||||
"for={peer_node};by={by_node};proto={scheme};host=\"{authority_text}\""
|
||||
);
|
||||
let mut values = HeaderMap::with_capacity(7);
|
||||
values.insert("forwarded", forwarded.parse()?);
|
||||
values.insert("x-forwarded-for", peer_header.clone());
|
||||
values.insert("x-real-ip", peer_header);
|
||||
values.insert("x-forwarded-host", authority.clone());
|
||||
values.insert("x-forwarded-proto", HeaderValue::from_static(scheme));
|
||||
values.insert("x-forwarded-port", if let Some(cached) = cached {
|
||||
cached.port_header.clone()
|
||||
} else {
|
||||
local.port().to_string().parse()?
|
||||
});
|
||||
if let Some(peer) = cached {
|
||||
peer.remember_untrusted(authority, tls, &values);
|
||||
}
|
||||
values
|
||||
};
|
||||
let by_text = cached.is_none().then(|| node(canonical(local.ip())));
|
||||
let peer_node = cached.map_or_else(
|
||||
|| if peer_ip.is_ipv4() { peer_text.as_deref().unwrap_or("") }
|
||||
else { ipv6_node.as_deref().unwrap_or("") },
|
||||
|cached| cached.peer_node.as_str(),
|
||||
);
|
||||
let by_node = cached.map_or_else(|| by_text.as_deref().unwrap_or(""),
|
||||
|cached| cached.by_node.as_str());
|
||||
let forwarded = format!(
|
||||
"for={peer_node};by={by_node};proto={scheme};host=\"{authority_text}\""
|
||||
);
|
||||
let mut values = HeaderMap::with_capacity(7);
|
||||
values.insert("forwarded", forwarded.parse()?);
|
||||
values.insert("x-forwarded-for", peer_header.clone());
|
||||
values.insert("x-real-ip", peer_header);
|
||||
values.insert("x-forwarded-host", authority.clone());
|
||||
values.insert("x-forwarded-proto", HeaderValue::from_static(scheme));
|
||||
values.insert("x-forwarded-port", if let Some(cached) = cached {
|
||||
cached.port_header.clone()
|
||||
} else {
|
||||
local.port().to_string().parse()?
|
||||
});
|
||||
values.insert("x-forwarded-uri", target.parse()?);
|
||||
values.insert("x-forwarded-uri", target_header);
|
||||
return Ok(Context {
|
||||
client_ip: peer_ip,
|
||||
scheme,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Immutable formatting shared by all HTTP/1 requests on one accepted socket.
|
||||
/// Socket formatting shared by all HTTP/1 requests on one accepted socket.
|
||||
/// No routing or trust decision is cached: those follow the current snapshot.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PeerFormatting {
|
||||
@@ -9,6 +9,15 @@ pub(crate) struct PeerFormatting {
|
||||
peer_node: String,
|
||||
by_node: String,
|
||||
port_header: HeaderValue,
|
||||
// One-entry, per-accepted-socket cache. No policy or client identity is cached.
|
||||
untrusted_headers: std::sync::Mutex<Option<UntrustedHeaders>>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct UntrustedHeaders {
|
||||
authority: HeaderValue,
|
||||
tls: bool,
|
||||
// The URI is intentionally excluded: it can change on every request.
|
||||
base: HeaderMap,
|
||||
}
|
||||
impl PeerFormatting {
|
||||
pub(crate) fn new(peer: SocketAddr, local: SocketAddr) -> Self {
|
||||
@@ -23,7 +32,25 @@ impl PeerFormatting {
|
||||
by_node: node(canonical(local.ip())),
|
||||
port_header: HeaderValue::from_str(&local.port().to_string())
|
||||
.expect("TCP port is a valid header"),
|
||||
untrusted_headers: std::sync::Mutex::new(None),
|
||||
}
|
||||
}
|
||||
fn peer_header(&self) -> &HeaderValue { &self.peer_header }
|
||||
|
||||
fn cached_untrusted(&self, authority: &HeaderValue, tls: bool) -> Option<HeaderMap> {
|
||||
self.untrusted_headers.lock().unwrap_or_else(|e| e.into_inner())
|
||||
.as_ref()
|
||||
.filter(|entry| entry.tls == tls
|
||||
&& entry.authority.as_bytes() == authority.as_bytes())
|
||||
.map(|entry| entry.base.clone())
|
||||
}
|
||||
|
||||
fn remember_untrusted(&self, authority: &HeaderValue, tls: bool, base: &HeaderMap) {
|
||||
*self.untrusted_headers.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(UntrustedHeaders {
|
||||
authority: authority.clone(),
|
||||
tls,
|
||||
base: base.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,74 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untrusted_socket_header_cache_respects_host_uri_scheme_and_trust() {
|
||||
for (peer, local) in [
|
||||
("192.0.2.51:32000", "127.0.0.1:8080"),
|
||||
("[2001:db8::8]:32100", "[2001:db8::1]:8443"),
|
||||
] {
|
||||
let peer: SocketAddr = peer.parse().unwrap();
|
||||
let local: SocketAddr = local.parse().unwrap();
|
||||
let formatter = PeerFormatting::new(peer, local);
|
||||
let compiled = Settings::default().compile().unwrap();
|
||||
let mut incoming = HeaderMap::new();
|
||||
incoming.append("x-forwarded-for", "203.0.113.11".parse().unwrap());
|
||||
incoming.append("forwarded", "for=203.0.113.12".parse().unwrap());
|
||||
for (host, tls, path) in [
|
||||
("app.example:8080", false, "/small.html"),
|
||||
("app.example:8080", false, "/small.html"),
|
||||
("app.example:8080", false, "/other?x=1"),
|
||||
("other.example:8080", false, "/small.html"),
|
||||
("other.example:8080", true, "/small.html"),
|
||||
("app.example:8080", true, "/last?x=1&y=2"),
|
||||
] {
|
||||
let host = HeaderValue::from_str(host).unwrap();
|
||||
let uri: Uri = path.parse().unwrap();
|
||||
for original_url in [false, true] {
|
||||
let direct = compiled.context_with_original_url(
|
||||
&incoming, peer, local, tls, &host, &uri, original_url,
|
||||
).unwrap();
|
||||
let cached = compiled.context_with_peer(
|
||||
&incoming, peer, local, tls, &host, &uri, original_url,
|
||||
Some(&formatter), false,
|
||||
).unwrap();
|
||||
assert_eq!(cached.client_ip, direct.client_ip);
|
||||
assert_eq!(cached.scheme, direct.scheme);
|
||||
assert_eq!(cached.original_url, direct.original_url);
|
||||
assert_eq!(cached.values, direct.values);
|
||||
let mut direct_headers = incoming.clone();
|
||||
let mut cached_headers = incoming.clone();
|
||||
direct.apply_upstream(&mut direct_headers, true);
|
||||
cached.apply_upstream(&mut cached_headers, true);
|
||||
assert_eq!(direct_headers, cached_headers);
|
||||
}
|
||||
}
|
||||
// When the peer becomes trusted after a config swap, the single-entry
|
||||
// untrusted cache must never override the incoming proxy chain.
|
||||
if peer.is_ipv4() {
|
||||
let trusted = Settings {
|
||||
trusted_proxies: vec!["192.0.2.0/24".to_owned()],
|
||||
trust_forwarded_proto: true,
|
||||
}.compile().unwrap();
|
||||
let mut forwarded = HeaderMap::new();
|
||||
forwarded.insert("x-forwarded-for", "198.51.100.9".parse().unwrap());
|
||||
forwarded.insert("x-forwarded-proto", "https".parse().unwrap());
|
||||
let host = HeaderValue::from_static("app.example");
|
||||
let uri: Uri = "/trusted".parse().unwrap();
|
||||
let direct = trusted.context_with_original_url(
|
||||
&forwarded, peer, local, false, &host, &uri, false,
|
||||
).unwrap();
|
||||
let cached = trusted.context_with_peer(
|
||||
&forwarded, peer, local, false, &host, &uri, false,
|
||||
Some(&formatter), false,
|
||||
).unwrap();
|
||||
assert_eq!(cached.client_ip, "198.51.100.9".parse::<IpAddr>().unwrap());
|
||||
assert_eq!(cached.scheme, "https");
|
||||
assert_eq!(cached.values, direct.values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_forwarding_headers_replace_duplicate_untrusted_values() {
|
||||
let compiled = Settings::default().compile().unwrap();
|
||||
|
||||
Reference in new issue
Block a user