This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 15:59:37 +02:00
1 parent 65e082b9d1
commit 76cb4acef0
72 files changed
+6134 -4709

No files matched your search

+55 -25
View File
@@ -6,6 +6,8 @@ if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
*exchange.source.lock().unwrap_or_else(|e| e.into_inner()) = if route.response.is_some() {
"local_response"
} else if route.redirect.is_some() {
"local_redirect"
} else {
match &route.source {
crate::config::CompiledSource::WebServer(_) => "web_server",
@@ -18,6 +20,17 @@ if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
if let Some(spec) = &route.response {
return local_location_response(spec, request.method());
}
if let Some(spec) = &route.redirect {
let location = match route.redirect_location(&path, request.uri()) {
Ok(Some(location)) => location,
_ => return error(StatusCode::INTERNAL_SERVER_ERROR, "RustProxy: invalid location redirect\n"),
};
let status = StatusCode::from_u16(spec.status).unwrap_or(StatusCode::FOUND);
let mut response = status.into_response();
response.headers_mut().insert(http::header::LOCATION, location);
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
return response;
}
if route.unavailable {
let mut response = error(
StatusCode::SERVICE_UNAVAILABLE,
@@ -178,28 +191,38 @@ let body_empty = if cache_enabled
} else {
request.body().is_end_stream()
};
let ignore_static_cookie = cache_settings.ignore_cookie_for_path(&path);
let original_policy = policy_reason
.map(crate::cache::RequestPolicy::Bypass)
.unwrap_or_else(|| {
crate::cache::request_policy(
request.method(),
request.headers(),
body_empty,
&path,
request.uri().query().is_some(),
cache_settings,
)
});
let only_cached =
cache_settings.active_for(route.cache) && crate::cache::only_if_cached(request.headers());
let public_suffix = request
.uri()
.path_and_query()
.map(|v| v.as_str())
.unwrap_or("/")
.to_owned();
let force_cache = policy_reason.is_none()
&& cache_settings.active_for(route.cache)
&& route
.frontend
.as_ref()
.is_some_and(|frontend| frontend.force_cache.matches(&path, &public_suffix));
let ignore_static_cookie = !force_cache && cache_settings.ignore_cookie_for_path(&path);
let original_policy = policy_reason
.map(crate::cache::RequestPolicy::Bypass)
.unwrap_or_else(|| {
if force_cache {
crate::cache::forced_request_policy(request.method(), request.headers(), body_empty)
} else {
crate::cache::request_policy(
request.method(),
request.headers(),
body_empty,
&path,
request.uri().query().is_some(),
cache_settings,
)
}
});
let only_cached =
cache_settings.active_for(route.cache) && crate::cache::only_if_cached(request.headers());
let origin_suffix = match route.rewritten_path(&path) {
Ok(None) => public_suffix.clone(),
Ok(Some(rewritten)) => {
@@ -283,14 +306,20 @@ if websocket_legacy_support {
let range_selection = crate::cache::request_range(request.method(), request.headers());
let if_range = request.headers().get("if-range").cloned();
let range_requested = !matches!(range_selection, crate::cache::RangeSelection::None);
let post_policy = crate::cache::request_policy(
request.method(),
request.headers(),
body_empty,
&path,
request.uri().query().is_some(),
cache_settings,
);
let post_policy = if force_cache {
crate::cache::forced_request_policy(request.method(), request.headers(), body_empty)
} else {
crate::cache::request_policy(
request.method(),
request.headers(),
body_empty,
&path,
request.uri().query().is_some(),
cache_settings,
)
};
let force_vary_cookie = force_cache && request.headers().contains_key("cookie");
let force_vary_authorization = force_cache && request.headers().contains_key("authorization");
let effective_policy = match (original_policy, post_policy) {
(crate::cache::RequestPolicy::Bypass(reason), _) => {
crate::cache::RequestPolicy::Bypass(reason)
@@ -353,7 +382,8 @@ let pending_revalidation = revalidation_reason.is_some()
let miss_recorded = false;
(
backend, backend_lease, affinity_cookie, admin_control_plane, cache_settings,
transform_settings, scope, ignore_static_cookie, only_cached, public_suffix,
transform_settings, scope, ignore_static_cookie, force_cache, force_vary_cookie,
force_vary_authorization, only_cached, public_suffix,
origin_suffix, uri, exchange, lease, stop, downstream, h2_downstream,
range_selection, if_range, range_requested, effective_reason, revalidation_reason,
cache_ticket, client_conditionals, cached_revalidation, freshness_revalidation_reason,
+1 -1
View File
@@ -117,7 +117,7 @@ let reason = effective_reason
if cache_ticket.is_some() && transformed_plan.is_none() {
"response_headers_policy"
} else if cache_ticket.is_some() {
"cache_miss"
if force_cache { "cache_miss_forced" } else { "cache_miss" }
} else {
"configuration_changed"
}
+4 -1
View File
@@ -118,7 +118,10 @@ let origin_plan = if cache_ticket.is_some()
response.headers(),
cache_settings,
upstream_started.elapsed(),
)
).map(|plan| {
let plan = if force_vary_cookie { plan.with_vary("cookie") } else { plan };
if force_vary_authorization { plan.with_vary("authorization") } else { plan }
})
} else {
None
};
+8
View File
@@ -146,6 +146,11 @@ async fn forward(
{
return response;
}
if let Some(response) = frontend.and_then(|frontend| {
canonical_host_redirect(frontend, &host, tls, local.0.port(), request.uri())
}) {
return response;
}
if !tls {
if let Some(response) =
frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri()))
@@ -378,6 +383,9 @@ async fn forward(
transform_settings,
scope,
ignore_static_cookie,
force_cache,
force_vary_cookie,
force_vary_authorization,
only_cached,
public_suffix,
origin_suffix,
+1
View File
@@ -1,6 +1,7 @@
include!("types.rs");
include!("cache_helpers.rs");
include!("upstream_helpers.rs");
include!("redirect_helpers.rs");
include!("forward_entry.rs");
#[cfg(test)]
+66
View File
@@ -0,0 +1,66 @@
// Frontend redirect helpers.
/// Build a same-host HTTPS redirect while preserving the request path and query.
fn force_ssl_location(host: &str, uri: &Uri, port: u16) -> Option<HeaderValue> {
let path = uri
.path_and_query()
.map(|value| value.as_str())
.unwrap_or("/");
let location = if port == 443 {
format!("https://{host}{path}")
} else {
format!("https://{host}:{port}{path}")
};
HeaderValue::from_str(&location).ok()
}
fn canonical_host_redirect(
frontend: &crate::config::CompiledFrontend,
host: &str,
tls: bool,
local_port: u16,
uri: &Uri,
) -> Option<Response> {
let (from, to) = frontend.www_redirect.as_ref()?;
if !host.eq_ignore_ascii_case(from) {
return None;
}
let (scheme, port) = if tls {
("https", local_port)
} else if frontend.force_ssl {
("https", frontend.https_redirect_port?)
} else {
("http", local_port)
};
let path = uri.path_and_query().map(|value| value.as_str()).unwrap_or("/");
let default_port = (scheme == "https" && port == 443) || (scheme == "http" && port == 80);
let location = if default_port {
format!("{scheme}://{to}{path}")
} else {
format!("{scheme}://{to}:{port}{path}")
};
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
response.headers_mut().insert(http::header::LOCATION, HeaderValue::from_str(&location).ok()?);
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
Some(response)
}
fn force_ssl_redirect(
frontend: &crate::config::CompiledFrontend,
host: &str,
uri: &Uri,
) -> Option<Response> {
if !frontend.force_ssl {
return None;
}
let location = force_ssl_location(host, uri, frontend.https_redirect_port?)?;
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
response
.headers_mut()
.insert(http::header::LOCATION, location);
// Do not pin a redirect in a browser after an operator later disables Force SSL.
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
Some(response)
}
+10
View File
@@ -297,3 +297,13 @@ mod authority_tests {
);
}
}
#[test]
fn upstream_uri_preserves_nested_url_query_without_rejecting_valid_separators() {
let config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let backend = snapshot.select("localhost", "/").unwrap().backend.as_ref().unwrap();
let suffix = "/convert?url=https%3A//easylist.to/easylist/easylist.txt&ip=0.0.0.0";
let uri = upstream_uri(backend, suffix).unwrap();
assert_eq!(uri.path_and_query().unwrap().as_str(), suffix);
}
-34
View File
@@ -420,40 +420,6 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
))
})
}
/// Build a same-host HTTPS redirect while preserving the request path and query.
fn force_ssl_location(host: &str, uri: &Uri, port: u16) -> Option<HeaderValue> {
let path = uri
.path_and_query()
.map(|value| value.as_str())
.unwrap_or("/");
let location = if port == 443 {
format!("https://{host}{path}")
} else {
format!("https://{host}:{port}{path}")
};
HeaderValue::from_str(&location).ok()
}
fn force_ssl_redirect(
frontend: &crate::config::CompiledFrontend,
host: &str,
uri: &Uri,
) -> Option<Response> {
if !frontend.force_ssl {
return None;
}
let location = force_ssl_location(host, uri, frontend.https_redirect_port?)?;
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
response
.headers_mut()
.insert(http::header::LOCATION, location);
// Do not pin a redirect in a browser after an operator later disables Force SSL.
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
Some(response)
}
/// HTTP/2 and HTTP/3 authority is a pseudo-header, not necessarily a Host field.
/// HTTP/1 absolute-form remains forbidden: this is not a forward proxy.
fn effective_host(request: &Request, tls: bool) -> Result<HeaderValue, ()> {