v0.34.0_10

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-18 09:24:03 +02:00
1 parent 8c4cbc583f
commit dd2502dd11
13 files changed
+389 -113

No files matched your search

+28 -12
View File
@@ -157,27 +157,43 @@ fn encoded_origin_path(path: &str) -> Option<String> {
fn response_context(
snapshot: &crate::config::Snapshot,
request: &Request,
) -> (
Option<Arc<crate::config::CompiledFrontend>>,
Option<Arc<crate::config::CompiledRoute>>,
) {
) -> SelectedRouteContext {
let tls = request
.extensions()
.get::<Transport>()
.copied()
.unwrap_or_default()
.tls;
let context = (|| {
let selected = (|| {
let local = request.extensions().get::<LocalAddr>()?.0;
let authority = effective_host(request, tls).ok()?;
let host = request_host(authority.to_str().ok()?).ok()?;
let original_host = effective_host(request, tls).ok()?;
let host = request_host(original_host.to_str().ok()?).ok()?;
let frontend = snapshot.frontend(&host, local, tls).cloned();
let route = crate::acl::routing_path_view(request.uri().path())
.ok()
.and_then(|path| snapshot.select_on_arc(&host, path.as_ref(), local, tls).cloned());
Some((frontend, route))
let path = crate::acl::routing_path_view(request.uri().path()).ok();
let route = path.as_ref().and_then(|path| {
snapshot.select_on_arc(&host, path.as_ref(), local, tls).cloned()
});
// An ordinary path borrows the original URI in forward_request. Only
// a decoded path needs to be moved between middleware and forwarding.
let decoded_path = path.and_then(|path| match path {
std::borrow::Cow::Borrowed(_) => None,
std::borrow::Cow::Owned(path) => Some(path),
});
Some(SelectedRouteContext {
frontend,
route,
validated: Some(ValidatedRoutingIdentity {
original_host,
host,
decoded_path,
}),
})
})();
context.unwrap_or((None, None))
selected.unwrap_or(SelectedRouteContext {
frontend: None,
route: None,
validated: None,
})
}
/// Reserved administration-session cookies may cross the proxy boundary only
+19 -21
View File
@@ -27,20 +27,11 @@ async fn forward_request(
.copied()
.unwrap_or_default()
.tls;
let Ok(original_host) = effective_host(&request, tls) else {
return error(
StatusCode::BAD_REQUEST,
"RustProxy: invalid or conflicting request authority\n",
);
};
let Ok(host) = original_host
.to_str()
.ok()
.and_then(|v| request_host(v).ok())
.ok_or(())
else {
return error(StatusCode::BAD_REQUEST, "RustProxy: invalid hostname\n");
};
let (selected_context, original_host, host, decoded_path, host_already_validated) =
match take_routing_identity(&mut request, tls) {
Ok(identity) => identity,
Err(response) => return response,
};
let handshake = if request.method() == Method::CONNECT {
let protocol = match request.version() {
Version::HTTP_2 => websocket::h2_protocol(&request),
@@ -106,13 +97,17 @@ async fn forward_request(
// 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",
);
let path = if let Some(path) = decoded_path.as_deref() {
std::borrow::Cow::Borrowed(path)
} else {
let Ok(path) = crate::acl::routing_path_view(routing_uri.path()) else {
return error(
StatusCode::BAD_REQUEST,
"RustProxy: ambiguous request path\n",
);
};
path
};
let selected_context = request.extensions().get::<SelectedRouteContext>().cloned();
let frontend = if let Some(selection) = &selected_context {
selection.frontend.as_ref()
} else {
@@ -193,7 +188,7 @@ async fn forward_request(
}
}
}
let mut context = match snapshot.forwarding.context_with_original_url(
let mut context = match snapshot.forwarding.context_with_peer(
request.headers(),
peer.0,
local.0,
@@ -201,6 +196,9 @@ async fn forward_request(
&original_host,
request.uri(),
frontend.is_some_and(|frontend| frontend.tinyauth.is_some()),
request.extensions().get::<Arc<crate::forwarding::PeerFormatting>>()
.map(Arc::as_ref),
host_already_validated,
) {
Ok(context) => context,
Err(_) => {
+43
View File
@@ -0,0 +1,43 @@
// Observation has already validated routing identity. Transfer it to the
// forward handler instead of repeating hostname parsing and percent decoding.
#[derive(Clone)]
struct ValidatedRoutingIdentity {
original_host: HeaderValue,
host: String,
decoded_path: Option<String>,
}
#[derive(Clone)]
struct SelectedRouteContext {
frontend: Option<Arc<crate::config::CompiledFrontend>>,
route: Option<Arc<crate::config::CompiledRoute>>,
validated: Option<ValidatedRoutingIdentity>,
}
/// Transfer middleware-validated identity without cloning strings. Internal
/// fallback dispatches still use the complete authority validation path.
fn take_routing_identity(
request: &mut Request,
tls: bool,
) -> Result<(Option<SelectedRouteContext>, HeaderValue, String, Option<String>, bool), Response> {
let mut selected_context = request.extensions_mut().remove::<SelectedRouteContext>();
let validated = selected_context.as_mut().and_then(|context| context.validated.take());
let host_already_validated = validated.is_some();
let (original_host, host, decoded_path) = match validated {
Some(validated) => (
validated.original_host,
validated.host,
validated.decoded_path,
),
None => {
let original_host = effective_host(request, tls).map_err(|_| error(
StatusCode::BAD_REQUEST,
"RustProxy: invalid or conflicting request authority\n",
))?;
let host = original_host.to_str().ok()
.and_then(|v| request_host(v).ok())
.ok_or_else(|| error(StatusCode::BAD_REQUEST,
"RustProxy: invalid hostname\n"))?;
(original_host, host, None)
}
};
Ok((selected_context, original_host, host, decoded_path, host_already_validated))
}
+1
View File
@@ -1,4 +1,5 @@
include!("types.rs");
include!("identity.rs");
include!("error_fallback.rs");
include!("cache_helpers.rs");
mod cache_plan;
+26
View File
@@ -11,6 +11,32 @@ mod authority_tests {
}
}
#[test]
fn observation_transfers_validated_host_and_decoded_path() {
let config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
for (raw, normalized) in [
("/assets/app.js?rev=5", None),
("/assets/%61pp.js?rev=5", Some("/assets/app.js")),
("/assets//app.js", Some("/assets/app.js")),
] {
let mut request = Request::builder().method(Method::GET).uri(raw)
.header(HOST, "localhost").body(Body::empty()).unwrap();
request.extensions_mut().insert(LocalAddr("127.0.0.1:8080".parse().unwrap()));
let context = response_context(&snapshot, &request);
let validated = context.validated.unwrap();
assert_eq!(validated.host, "localhost");
assert_eq!(validated.original_host, "localhost");
assert_eq!(validated.decoded_path.as_deref(), normalized);
}
let mut invalid = Request::builder().uri("/ok")
.header(HOST, "localhost").header(HOST, "another.example")
.body(Body::empty()).unwrap();
invalid.extensions_mut().insert(LocalAddr("127.0.0.1:8080".parse().unwrap()));
assert!(response_context(&snapshot, &invalid).validated.is_none());
assert!(crate::acl::routing_path_view("/encoded%2fslash").is_err());
}
#[test]
fn retry_is_limited_to_safe_methods() {
for method in [Method::GET, Method::HEAD, Method::OPTIONS] {
+1 -7
View File
@@ -21,13 +21,7 @@ use http::{
};
use http_body::Body as HttpBody;
use http_body_util::BodyExt;
/// A route resolved against the immutable snapshot in the observation layer.
/// The internal error-fallback dispatcher must resolve its target independently.
#[derive(Clone)]
struct SelectedRouteContext {
frontend: Option<Arc<crate::config::CompiledFrontend>>,
route: Option<Arc<crate::config::CompiledRoute>>,
}
/// Mark responses whose configured outbound policies were already applied.
#[derive(Clone)]
struct ResponseRulesApplied;
#[derive(Clone)]
+6 -7
View File
@@ -37,13 +37,12 @@ fn should_replace_error_page(response: &Response, minimal_welcome: bool) -> bool
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
let snapshot = state.config.load_full();
let (frontend_context, route_context) = response_context(&snapshot, &request);
// The same snapshot is inserted into the request. No second host/regex
// selection or expensive full CompiledRoute clone is necessary in forward().
request.extensions_mut().insert(SelectedRouteContext {
frontend: frontend_context.clone(),
route: route_context.clone(),
});
let selected = response_context(&snapshot, &request);
let frontend_context = selected.frontend.clone();
let route_context = selected.route.clone();
// The same validated request identity and snapshot are transferred to
// forward(); no second hostname parsing or encoded-path decoding.
request.extensions_mut().insert(selected);
let local = request.extensions().get::<LocalAddr>().map(|v| v.0);
let transport_tls = request
.extensions()