This commit is contained in:
Mateusz Gruszczyński committed 2026-10-21 14:39:51 +02:00
1 parent e037190c14
commit 5cb3b37627
37 files changed
+1031 -138

No files matched your search

+39 -15
View File
@@ -88,10 +88,7 @@ async fn forward_request(
.get::<Arc<crate::config::Snapshot>>()
.cloned()
.unwrap_or_else(|| state.config.load_full());
let internal_dispatch = request
.extensions()
.get::<InternalFallbackRoute>()
.cloned();
let internal_dispatch = request.extensions().get::<InternalFallbackRoute>().cloned();
// 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.
@@ -115,6 +112,14 @@ async fn forward_request(
// rerunning the observation middleware.
snapshot.frontend(&host, local.0, tls)
};
// Host-level denylist: before cache, custom locations, redirects, local
// Web Server/PHP-FPM, or any origin connection. No request body is parsed.
if frontend.is_some_and(|item| item.content_type_policy.blocks(request.headers())) {
return error(
StatusCode::FORBIDDEN,
"RustProxy: request Content-Type denied by vhost policy\n",
);
}
if snapshot.user_agent_blocking.blocks(request.headers())
|| frontend.is_some_and(|frontend| frontend.user_agent_blocking.blocks(request.headers()))
{
@@ -181,9 +186,9 @@ async fn forward_request(
return response;
}
if !tls {
if let Some(response) = frontend.and_then(|frontend| {
force_ssl_redirect(frontend, &host, request.uri())
}) {
if let Some(response) =
frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri()))
{
return response;
}
}
@@ -196,7 +201,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>>()
request
.extensions()
.get::<Arc<crate::forwarding::PeerFormatting>>()
.map(Arc::as_ref),
host_already_validated,
) {
@@ -237,7 +244,10 @@ async fn forward_request(
let mut country: Option<String> = None;
if let Some(frontend) = frontend {
if capture_country
|| frontend.profile_policies.iter().any(|profile| profile.countries.needs_geoip())
|| frontend
.profile_policies
.iter()
.any(|profile| profile.countries.needs_geoip())
|| frontend.rate_limit.needs_geoip()
{
country = cloudflare_country
@@ -271,7 +281,10 @@ async fn forward_request(
&frontend.id,
context.client_ip,
country.as_deref(),
request.headers().get(http::header::USER_AGENT).and_then(|value| value.to_str().ok()),
request
.headers()
.get(http::header::USER_AGENT)
.and_then(|value| value.to_str().ok()),
&frontend.rate_limit,
) {
let mut response = error(
@@ -295,7 +308,17 @@ async fn forward_request(
);
}
if cache_http_invalidation_method(request.method()) {
return cache_http_invalidation(&state, &snapshot, frontend, &host, request, context.client_ip, peer.0.ip(), context.scheme).await;
return cache_http_invalidation(
&state,
&snapshot,
frontend,
&host,
request,
context.client_ip,
peer.0.ip(),
context.scheme,
)
.await;
}
if let Some((_redirect, rule)) = redirect {
let authority = original_host.to_str().unwrap_or(&host);
@@ -434,10 +457,11 @@ async fn forward_request(
)
});
};
let fallback_body = match prepare_error_fallback_body(&mut request, route.error_fallback.is_some()).await {
Ok(body) => body,
Err(response) => return response,
};
let fallback_body =
match prepare_error_fallback_body(&mut request, route.error_fallback.is_some()).await {
Ok(body) => body,
Err(response) => return response,
};
let (
backend,
backend_lease,
+39
View File
@@ -86,6 +86,45 @@ mod cache_fast_path_tests {
config
}
#[tokio::test]
async fn content_type_policy_denies_before_local_web_server_and_cache_and_updates_live() {
let directory = TempDir::new().unwrap();
let mut config = web_config(&directory, true);
config.vhosts[0].content_type_policy = crate::vhost::ContentTypePolicy {
enabled: true,
blocked: vec!["multipart/form-data".into(), "image/*".into()],
};
let fixture = Fixture::new(config).await;
// Warm the cache before testing a header-denied cache hit.
assert_eq!(fixture.get("/style.css", &[]).await.status, StatusCode::OK);
assert_eq!(fixture.get("/style.css", &[]).await.status, StatusCode::OK);
assert_eq!(fixture.get("/style.css", &[("content-type", "multipart/form-data; boundary=example")]).await.status, StatusCode::FORBIDDEN);
assert_eq!(fixture.get("/style.css", &[("content-type", "IMAGE/PNG")]).await.status, StatusCode::FORBIDDEN);
assert_eq!(fixture.send(Method::POST, "/style.css", &[("content-type", "multipart/form-data; boundary=x")], Body::from("sample")).await.status, StatusCode::FORBIDDEN);
assert_eq!(fixture.get("/style.css", &[("content-type", "text/plain")]).await.status, StatusCode::OK);
assert_eq!(fixture.get("/style.css", &[("content-type", "application/json, multipart/form-data")]).await.status, StatusCode::FORBIDDEN);
fixture.change(|config| config.vhosts[0].content_type_policy.enabled = false).await;
assert_eq!(fixture.get("/style.css", &[("content-type", "multipart/form-data")]).await.status, StatusCode::OK);
fixture.stop().await;
}
#[tokio::test]
async fn content_type_policy_denies_before_reverse_proxy_upstream_request() {
let origin = origin().await;
let mut config = config_for(origin.addr, false);
config.vhosts[0].content_type_policy = crate::vhost::ContentTypePolicy {
enabled: true, blocked: vec!["multipart/form-data".into()],
};
let fixture = Fixture::new(config).await;
let blocked = fixture.send(Method::POST, "/", &[("content-type", "Multipart/Form-Data; boundary=abc")], Body::from("--abc\r\n")).await;
assert_eq!(blocked.status, StatusCode::FORBIDDEN);
assert_eq!(origin.requests.load(Ordering::Relaxed), 0);
let allowed = fixture.get("/", &[("content-type", "application/json")]).await;
assert_eq!(allowed.status, StatusCode::OK);
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
fixture.stop().await;
}
#[tokio::test]
async fn web_cache_disabled_keeps_get_head_range_conditionals_and_observability() {
let directory = TempDir::new().unwrap();