v0.34.0_2

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 23:37:43 +02:00
1 parent 0a8bc6f040
commit 434e789144
38 files changed
+899 -238

No files matched your search

+14 -1
View File
@@ -301,6 +301,7 @@ pub struct MeteredBody {
/// For a response, backend selection is final before this wrapper is made.
/// Keep the selected lease alive and avoid locking Exchange.backend per frame.
response_backend: Option<Arc<crate::balance::Lease>>,
request_backend: Option<Arc<crate::balance::Lease>>,
direction: Direction,
timer: Option<Pin<Box<Sleep>>>,
deadline: tokio::time::Instant,
@@ -317,6 +318,7 @@ impl MeteredBody {
inner,
exchange: Some(exchange),
response_backend,
request_backend: None,
direction,
// Ready/empty bodies never need a timer allocation. Keep the
// original deadline so a delayed first poll still times out.
@@ -330,6 +332,7 @@ impl MeteredBody {
self.finished = true;
self.timer.take();
self.response_backend.take();
self.request_backend.take();
self.exchange.take();
}
}
@@ -350,7 +353,17 @@ impl HttpBody for MeteredBody {
if let (Some(data), Some(exchange)) = (frame.data_ref(), this.exchange.as_ref()) {
let count = data.len() as u64;
match this.direction {
Direction::Request => exchange.backend_bytes(count, true),
Direction::Request => {
// Backend is selected before a non-empty upstream
// upload is polled. No retry of a streamed write.
if this.request_backend.is_none() {
this.request_backend = exchange.backend
.lock().unwrap_or_else(|e| e.into_inner()).clone();
}
if let Some(lease) = &this.request_backend {
lease.bytes(count, true);
}
}
Direction::Response => {
if let Some(lease) = &this.response_backend {
lease.bytes(count, false);
+3 -1
View File
@@ -125,7 +125,9 @@ let cache_settings = route
let transform_settings = route.frontend.as_ref().map(|f| &f.transform);
let scope = route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or("");
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
// The resolved route was already recorded before local response, LB and
// ACL decisions. Do not acquire the route mutex and copy its ID twice on
// every reverse-proxy request.
exchange.set_cache_enabled(cache_settings.active_for(route.cache));
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
}
+4 -5
View File
@@ -1,11 +1,10 @@
fn request_id_hash(sequence: u64) -> String {
use rand::TryRng;
use sha2::{Digest, Sha256};
let mut nonce = [0u8; 32];
let mut rng = rand::rngs::SysRng;
rng.try_fill_bytes(&mut nonce)
.expect("secure operating system random source unavailable");
// Thread-local CSPRNG is seeded (and periodically reseeded) from the OS.
// Do not make a getrandom syscall on the hot path for every HTTP request.
// Keep hashing the per-process sequence as before for the same ID format.
let nonce: [u8; 32] = rand::random();
let mut hash = Sha256::new();
hash.update(sequence.to_be_bytes());
hash.update(nonce);
+1 -1
View File
@@ -25,7 +25,7 @@ mod request_id_tests {
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)));
assert!(
seen.insert(id),
"fixed sequence must still use independent OS entropy"
"fixed sequence must still use independent CSPRNG output"
);
}
}
+19
View File
@@ -595,4 +595,23 @@ mod graceful_h2_tests {
assert!(!is_graceful_h2_goaway(&protocol));
assert!(!is_graceful_h2_goaway(&local));
}
#[test]
fn compiled_request_origin_matches_previous_uri_parser() {
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
config.vhosts[0].backends[0].url = "https://127.0.0.1:9000".into();
config.vhosts[0].backends[0].tls_server_name = Some("backend.example".into());
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let backend = &snapshot.pools[0].members[0].backend;
assert_eq!(backend.request_origin_uri.to_string(), backend.request_origin);
for suffix in [
"/", "/static/main.css", "/path?x=1&y=2", "/p%2Fq?url=https%3A//example.org/a",
"/?q=%C5%82&empty=", "/double//slash", "/item#frag",
] {
let previous = format!("{}{suffix}", backend.request_origin).parse::<Uri>().ok();
assert_eq!(upstream_uri(backend, suffix), previous, "{suffix}");
}
assert!(upstream_uri(backend, "http://attacker.invalid/").is_none());
assert_eq!(backend.origin, "https://127.0.0.1:9000");
}
}
+7 -1
View File
@@ -152,7 +152,13 @@ fn retry_safe_method(method: &Method) -> bool {
}
fn upstream_uri(backend: &crate::config::CompiledBackend, suffix: &str) -> Option<Uri> {
format!("{}{suffix}", backend.request_origin).parse().ok()
// Reuse the validated scheme and authority from the immutable compiled
// backend. Only the untrusted path/query is parsed per request. This also
// prevents accidental authority changes from rewritten paths.
let path: http::uri::PathAndQuery = suffix.parse().ok()?;
let mut parts = backend.request_origin_uri.clone().into_parts();
parts.path_and_query = Some(path);
Uri::from_parts(parts).ok()
}
fn legacy_websocket_origin(
+5 -8
View File
@@ -56,14 +56,11 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|| (request.version() == Version::HTTP_3
&& request.method() == Method::CONNECT
&& websocket::h3_protocol(&request) == Some("websocket"));
if request.version() == Version::HTTP_11 {
state.metrics.http1_requests.fetch_add(1, Ordering::Relaxed);
}
if request.version() == Version::HTTP_2 {
state.metrics.http2_requests.fetch_add(1, Ordering::Relaxed);
}
if request.version() == Version::HTTP_3 {
state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed);
match request.version() {
Version::HTTP_11 => { state.metrics.http1_requests.fetch_add(1, Ordering::Relaxed); }
Version::HTTP_2 => { state.metrics.http2_requests.fetch_add(1, Ordering::Relaxed); }
Version::HTTP_3 => { state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed); }
_ => {}
}
let id = state.metrics.requests.fetch_add(1, Ordering::Relaxed) + 1;
let request_id = request_id_hash(id);