v0.34.0_4

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-18 07:00:34 +02:00
1 parent f20c8da718
commit 5910e6e901
21 files changed
+377 -51

No files matched your search

+2 -2
View File
@@ -172,9 +172,9 @@ fn response_context(
let authority = effective_host(request, tls).ok()?;
let host = request_host(authority.to_str().ok()?).ok()?;
let frontend = snapshot.frontend(&host, local, tls).cloned();
let route = crate::acl::routing_path(request.uri().path())
let route = crate::acl::routing_path_view(request.uri().path())
.ok()
.and_then(|path| snapshot.select_on_arc(&host, &path, local, tls).cloned());
.and_then(|path| snapshot.select_on_arc(&host, path.as_ref(), local, tls).cloned());
Some((frontend, route))
})();
context.unwrap_or((None, None))
+3 -3
View File
@@ -42,7 +42,7 @@ if let Some(ticket) = &cache_ticket {
uri: uri.clone(),
public_suffix: public_suffix.clone(),
host: host.clone(),
path: path.clone(),
path: path.as_ref().to_owned(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
@@ -79,7 +79,7 @@ if let Some(ticket) = &cache_ticket {
uri: uri.clone(),
public_suffix: public_suffix.clone(),
host: host.clone(),
path: path.clone(),
path: path.as_ref().to_owned(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
@@ -240,7 +240,7 @@ if handshake.is_none()
uri: uri.clone(),
public_suffix: public_suffix.clone(),
host: host.clone(),
path: path.clone(),
path: path.as_ref().to_owned(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
+1 -1
View File
@@ -170,7 +170,7 @@ if let (Some(ticket), Some(plan), Some(transformed)) =
uri: active_uri.clone(),
public_suffix: public_suffix.clone(),
host: host.clone(),
path: path.clone(),
path: path.as_ref().to_owned(),
scope: scope.to_owned(),
timeout: frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
+1 -1
View File
@@ -7,7 +7,7 @@
}
let service_path = match route.rewritten_path(&path) {
Ok(Some(value)) => value,
Ok(None) => path.clone(),
Ok(None) => path.as_ref().to_owned(),
Err(_) => return error(StatusCode::BAD_REQUEST,
"RustProxy: invalid Web Server rewrite path\n"),
};
+15 -4
View File
@@ -101,7 +101,12 @@ async fn forward_request(
.extensions()
.get::<InternalFallbackRoute>()
.cloned();
let Ok(path) = crate::acl::routing_path(request.uri().path()) else {
// Retain the original URI's shared backing bytes independently of `request`.
// A normalized, already-safe path can then be borrowed for the entire
// forwarding operation, even after the request URI/headers are rewritten.
// Encoded/slash-folded paths still allocate via the strict decoder.
let routing_uri = request.uri().clone();
let Ok(path) = crate::acl::routing_path_view(routing_uri.path()) else {
return error(
StatusCode::BAD_REQUEST,
"RustProxy: ambiguous request path\n",
@@ -216,9 +221,15 @@ async fn forward_request(
cloudflare_country = crate::forwarding::cloudflare_country(request.headers(), peer.0);
}
}
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) =
Some(context.client_ip.to_string());
// `observe` already stored the actual transport peer. Most requests
// don't use a trusted forwarding hop, so avoid formatting the same IP
// and taking the per-request mutex a second time. IPv4-mapped peers
// still take this path so their canonical form is correctly logged.
if context.client_ip != peer.0.ip() {
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) =
Some(context.client_ip.to_string());
}
}
// GeoIP stays off the normal request path. Resolve lazily for any vhost or
// location policy that actually needs a country code, or for log capture.
+21 -9
View File
@@ -4,15 +4,27 @@ use http::{
};
/// Remove both standard hop-by-hop fields and fields named by Connection.
pub fn strip_hop_by_hop(headers: &mut HeaderMap) {
let nominated: Vec<HeaderName> = headers
.get_all(CONNECTION)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|v| v.split(','))
.filter_map(|v| v.trim().parse().ok())
.collect();
for name in nominated {
headers.remove(name);
// The common HTTP/1.1 "Connection: keep-alive" case has no dynamic
// nominated headers to collect. Keep the general multi-value/token
// parser for all other requests (including maliciously repeated fields).
let only_keep_alive = {
let mut values = headers.get_all(CONNECTION).iter();
values.next().is_some_and(|value| {
value.as_bytes().eq_ignore_ascii_case(b"keep-alive")
&& values.next().is_none()
})
};
if !only_keep_alive {
let nominated: Vec<HeaderName> = headers
.get_all(CONNECTION)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|v| v.split(','))
.filter_map(|v| v.trim().parse().ok())
.collect();
for name in nominated {
headers.remove(name);
}
}
for name in [
"connection",
+30
View File
@@ -52,4 +52,34 @@ mod tests {
restore_admin_cookie(&mut h, &admin);
assert_eq!(h.get("cookie").unwrap(), "a=1; b=2; rustproxy_session=secret");
}
#[test]
fn common_keep_alive_does_not_strip_unrelated_headers() {
for value in ["keep-alive", "KEEP-ALIVE", "Keep-Alive"] {
let mut headers = HeaderMap::new();
headers.insert("connection", value.parse().unwrap());
headers.insert("keep-alive", "timeout=2".parse().unwrap());
headers.insert("x-tenant", "alpha".parse().unwrap());
headers.insert("authorization", "Bearer allowed".parse().unwrap());
strip_hop_by_hop(&mut headers);
assert!(!headers.contains_key("connection"));
assert!(!headers.contains_key("keep-alive"));
assert_eq!(headers["x-tenant"], "alpha");
assert_eq!(headers["authorization"], "Bearer allowed");
}
}
#[test]
fn multiple_connection_fields_still_strip_all_nominated_headers() {
let mut headers = HeaderMap::new();
headers.append("connection", "keep-alive".parse().unwrap());
headers.append("connection", "x-hidden, upgrade".parse().unwrap());
headers.insert("x-hidden", "sensitive".parse().unwrap());
headers.insert("x-public", "allowed".parse().unwrap());
headers.insert("upgrade", "websocket".parse().unwrap());
strip_hop_by_hop(&mut headers);
assert!(!headers.contains_key("x-hidden"));
assert!(!headers.contains_key("connection"));
assert!(!headers.contains_key("upgrade"));
assert_eq!(headers["x-public"], "allowed");
}
}
+7 -5
View File
@@ -13,12 +13,14 @@ fn request_id_hash(sequence: u64) -> String {
fn request_id_hex(digest: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(digest.len() * 2);
for &byte in digest {
encoded.push(HEX[(byte >> 4) as usize] as char);
encoded.push(HEX[(byte & 0x0f) as usize] as char);
// Encode two ASCII bytes per digest byte in one allocation; avoid 64
// separate String::push bounds checks for the 32-byte SHA-256 digest.
let mut encoded = vec![0u8; digest.len() * 2];
for (i, &byte) in digest.iter().enumerate() {
encoded[2 * i] = HEX[(byte >> 4) as usize];
encoded[2 * i + 1] = HEX[(byte & 0x0f) as usize];
}
encoded
String::from_utf8(encoded).expect("hex digits are always valid UTF-8")
}
#[cfg(test)]