v0.25.0
This commit is contained in:
1 parent
2e4aa4df4d
commit
87e571c170
106 files changed
+17952
-6185
No files matched your search
+388
-198
@@ -15,7 +15,7 @@ use axum::{
|
||||
};
|
||||
use body::{Direction, Exchange, MeteredBody};
|
||||
use http::{
|
||||
header::{HeaderValue, HOST},
|
||||
header::{HeaderValue, HOST, USER_AGENT},
|
||||
HeaderMap, Method, StatusCode, Uri, Version,
|
||||
};
|
||||
use http_body::Body as HttpBody;
|
||||
@@ -24,10 +24,16 @@ use http_body_util::BodyExt;
|
||||
struct ResponseRulesApplied;
|
||||
#[derive(Clone)]
|
||||
struct LocalGeneratedResponse;
|
||||
#[derive(Clone)]
|
||||
struct StaticSourceResponse;
|
||||
#[derive(Clone, Copy)]
|
||||
struct RedirectResponse { http3_enabled: bool }
|
||||
struct RedirectResponse {
|
||||
http3_enabled: bool,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct LocalHostResponse { http3_enabled: bool }
|
||||
struct LocalHostResponse {
|
||||
http3_enabled: bool,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct OriginTiming(Duration);
|
||||
#[derive(Clone)]
|
||||
@@ -80,7 +86,9 @@ fn format_error_chain(error: &(dyn std::error::Error + 'static)) -> String {
|
||||
parts.join(" -> ")
|
||||
}
|
||||
|
||||
fn classify_stale_error(error: &(dyn std::error::Error + 'static)) -> crate::cache::StaleErrorClass {
|
||||
fn classify_stale_error(
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
) -> crate::cache::StaleErrorClass {
|
||||
let mut current = Some(error);
|
||||
let mut text = String::new();
|
||||
for _ in 0..12 {
|
||||
@@ -115,7 +123,10 @@ fn classify_stale_error(error: &(dyn std::error::Error + 'static)) -> crate::cac
|
||||
}
|
||||
}
|
||||
|
||||
fn upstream_error_class(error: &(dyn std::error::Error + 'static), connect_failure: bool) -> &'static str {
|
||||
fn upstream_error_class(
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
connect_failure: bool,
|
||||
) -> &'static str {
|
||||
match classify_stale_error(error) {
|
||||
crate::cache::StaleErrorClass::Dns => "dns",
|
||||
crate::cache::StaleErrorClass::Tls => "tls",
|
||||
@@ -217,7 +228,6 @@ where
|
||||
detail
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CacheRefreshContext {
|
||||
state: Arc<App>,
|
||||
@@ -271,21 +281,17 @@ async fn refresh_cache_once(
|
||||
*refresh.version_mut() = Version::HTTP_11;
|
||||
*refresh.headers_mut() = refresh_headers;
|
||||
let started = Instant::now();
|
||||
let Ok(Ok(origin)) = tokio::time::timeout(
|
||||
context.timeout,
|
||||
context.backend.client.request(refresh),
|
||||
)
|
||||
.await
|
||||
let Ok(Ok(origin)) =
|
||||
tokio::time::timeout(context.timeout, context.backend.client.request(refresh)).await
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let origin_validation_headers = origin.headers().clone();
|
||||
let Some(route) = context.snapshot.select_on(
|
||||
&context.host,
|
||||
&context.path,
|
||||
context.local,
|
||||
context.tls,
|
||||
) else {
|
||||
let Some(route) =
|
||||
context
|
||||
.snapshot
|
||||
.select_on(&context.host, &context.path, context.local, context.tls)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if origin.status() == StatusCode::NOT_MODIFIED {
|
||||
@@ -464,10 +470,8 @@ fn schedule_proactive_refresh(
|
||||
delay = 5;
|
||||
continue;
|
||||
};
|
||||
let Some(revalidation_guard) = context
|
||||
.state
|
||||
.cache
|
||||
.begin_background_revalidation(&cached)
|
||||
let Some(revalidation_guard) =
|
||||
context.state.cache.begin_background_revalidation(&cached)
|
||||
else {
|
||||
drop(inflight_guard);
|
||||
delay = 1;
|
||||
@@ -547,7 +551,11 @@ fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Metho
|
||||
let body_allowed = !matches!(status.as_u16(), 100..=199 | 204 | 304) && *method != Method::HEAD;
|
||||
let mut response = Response::builder()
|
||||
.status(status)
|
||||
.body(if body_allowed { Body::from(spec.body.clone()) } else { Body::empty() })
|
||||
.body(if body_allowed {
|
||||
Body::from(spec.body.clone())
|
||||
} else {
|
||||
Body::empty()
|
||||
})
|
||||
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, Body::empty()).into_response());
|
||||
if let Ok(value) = HeaderValue::from_str(&spec.content_type) {
|
||||
response.headers_mut().insert("content-type", value);
|
||||
@@ -596,8 +604,7 @@ fn admin_session_passthrough(
|
||||
tls: bool,
|
||||
backend: &crate::config::CompiledBackend,
|
||||
) -> bool {
|
||||
tls
|
||||
&& backend.accept_local_listener_risk
|
||||
tls && backend.accept_local_listener_risk
|
||||
&& backend.targets_admin_listener
|
||||
&& snapshot
|
||||
.envelope
|
||||
@@ -904,6 +911,17 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.extensions()
|
||||
.get::<PeerAddr>()
|
||||
.map(|peer| peer.0.ip().to_string());
|
||||
let user_agent = state
|
||||
.logs
|
||||
.capture_user_agent()
|
||||
.then(|| {
|
||||
request
|
||||
.headers()
|
||||
.get(USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(|value| value.chars().take(512).collect::<String>())
|
||||
})
|
||||
.flatten();
|
||||
let (http_version, transport) = match request.version() {
|
||||
Version::HTTP_10 => ("1.0", "h1"),
|
||||
Version::HTTP_11 => ("1.1", "h1"),
|
||||
@@ -939,6 +957,8 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
// the verified effective client address after trusted-proxy/Cloudflare
|
||||
// processing, so policy and live-log identity remain aligned.
|
||||
client_ip: Mutex::new(initial_client_ip),
|
||||
user_agent,
|
||||
country: Mutex::new(None),
|
||||
started: Instant::now(),
|
||||
status: AtomicU16::new(500),
|
||||
cache_status: AtomicU16::new(0),
|
||||
@@ -949,6 +969,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
response_bytes: AtomicU64::new(0),
|
||||
metrics: state.metrics.clone(),
|
||||
logs: state.logs.clone(),
|
||||
traffic: state.traffic.clone(),
|
||||
_permit: permit,
|
||||
});
|
||||
if let Ok(value) = HeaderValue::from_str(&id.to_string()) {
|
||||
@@ -968,7 +989,10 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
))
|
||||
});
|
||||
let mut response = next.run(request).await;
|
||||
let minimal_welcome = response.extensions().get::<crate::pages::MinimalWelcome>().is_some();
|
||||
let minimal_welcome = response
|
||||
.extensions()
|
||||
.get::<crate::pages::MinimalWelcome>()
|
||||
.is_some();
|
||||
let code = response.status().as_u16();
|
||||
let custom_error = frontend_context
|
||||
.as_ref()
|
||||
@@ -989,17 +1013,32 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
// minimal. It is never cacheable and carries no optional RustProxy
|
||||
// diagnostics/server/security/compression/cache-diagnostic headers.
|
||||
for name in [
|
||||
"server", "server-timing", "alt-svc", "x-rustproxy-request-id",
|
||||
"x-rustproxy-revision", "x-rust-proxy-backend",
|
||||
"x-rust-proxy-backend-time", "x-rustproxy-backend-time", "x-cache", "x-cache-hits",
|
||||
"x-cache-status", "x-cache-resident-age", "content-encoding", "vary",
|
||||
"strict-transport-security", "content-security-policy",
|
||||
"permissions-policy", "referrer-policy", "x-frame-options",
|
||||
"server",
|
||||
"server-timing",
|
||||
"alt-svc",
|
||||
"x-rustproxy-request-id",
|
||||
"x-rustproxy-revision",
|
||||
"x-rust-proxy-backend",
|
||||
"x-rust-proxy-backend-time",
|
||||
"x-rustproxy-backend-time",
|
||||
"x-cache",
|
||||
"x-cache-hits",
|
||||
"x-cache-status",
|
||||
"x-cache-resident-age",
|
||||
"content-encoding",
|
||||
"vary",
|
||||
"strict-transport-security",
|
||||
"content-security-policy",
|
||||
"permissions-policy",
|
||||
"referrer-policy",
|
||||
"x-frame-options",
|
||||
"x-content-type-options",
|
||||
] {
|
||||
response.headers_mut().remove(name);
|
||||
}
|
||||
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
} else {
|
||||
if response
|
||||
.extensions()
|
||||
@@ -1029,10 +1068,18 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
id,
|
||||
&state.active_listeners,
|
||||
);
|
||||
if response.extensions().get::<RedirectResponse>().is_some_and(|redirect| !redirect.http3_enabled) {
|
||||
if response
|
||||
.extensions()
|
||||
.get::<RedirectResponse>()
|
||||
.is_some_and(|redirect| !redirect.http3_enabled)
|
||||
{
|
||||
response.headers_mut().remove("alt-svc");
|
||||
}
|
||||
if response.extensions().get::<LocalHostResponse>().is_some_and(|item| !item.http3_enabled) {
|
||||
if response
|
||||
.extensions()
|
||||
.get::<LocalHostResponse>()
|
||||
.is_some_and(|item| !item.http3_enabled)
|
||||
{
|
||||
response.headers_mut().remove("alt-svc");
|
||||
}
|
||||
response = crate::compression::encode(
|
||||
@@ -1055,8 +1102,18 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.is_none()
|
||||
&& response.extensions().get::<RedirectResponse>().is_none()
|
||||
&& response.extensions().get::<LocalHostResponse>().is_none()
|
||||
&& response.extensions().get::<crate::pages::PageServed>().is_none()
|
||||
&& response.extensions().get::<LocalGeneratedResponse>().is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<crate::pages::PageServed>()
|
||||
.is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<LocalGeneratedResponse>()
|
||||
.is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<StaticSourceResponse>()
|
||||
.is_none()
|
||||
&& response
|
||||
.extensions()
|
||||
.get::<crate::tinyauth::AuthenticationFailure>()
|
||||
@@ -1087,8 +1144,14 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
cache.annotate_transformed(response.headers_mut(), transformed);
|
||||
}
|
||||
}
|
||||
if response.extensions().get::<crate::pages::PageServed>().is_some()
|
||||
|| response.extensions().get::<LocalGeneratedResponse>().is_some()
|
||||
if response
|
||||
.extensions()
|
||||
.get::<crate::pages::PageServed>()
|
||||
.is_some()
|
||||
|| response
|
||||
.extensions()
|
||||
.get::<LocalGeneratedResponse>()
|
||||
.is_some()
|
||||
{
|
||||
// Static/placeholder/default pages are generated locally and must never
|
||||
// look cacheable or carry RustProxy cache diagnostics/validators.
|
||||
@@ -1145,7 +1208,10 @@ 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 path = uri
|
||||
.path_and_query()
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or("/");
|
||||
let location = if port == 443 {
|
||||
format!("https://{host}{path}")
|
||||
} else {
|
||||
@@ -1164,7 +1230,9 @@ fn force_ssl_redirect(
|
||||
}
|
||||
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);
|
||||
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()
|
||||
@@ -1295,8 +1363,8 @@ async fn forward(
|
||||
);
|
||||
};
|
||||
let frontend = snapshot.frontend(&host, local.0, tls);
|
||||
let websocket_legacy_support = handshake.is_some()
|
||||
&& frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
|
||||
let websocket_legacy_support =
|
||||
handshake.is_some() && frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
|
||||
let redirect = snapshot.redirect_on(&host, &path, local.0, tls);
|
||||
let local_host = snapshot.not_found_host_on(&host, local.0, tls);
|
||||
// TLS ALPN normally prevents HTTP/2 from reaching a frontend that disabled it.
|
||||
@@ -1342,7 +1410,9 @@ async fn forward(
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1373,10 +1443,24 @@ async fn forward(
|
||||
}
|
||||
}
|
||||
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());
|
||||
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(context.client_ip.to_string());
|
||||
}
|
||||
if let Some(frontend) = frontend {
|
||||
let country = state.geoip.country(context.client_ip);
|
||||
// GeoIP is deliberately off the default request path. Resolve only when
|
||||
// a country policy/rate-limit requires it or the operator explicitly
|
||||
// enables country capture for access logs + traffic analytics.
|
||||
let capture_country = state.logs.capture_country();
|
||||
let country = (capture_country
|
||||
|| frontend.profile_country_access.needs_geoip()
|
||||
|| frontend.rate_limit.needs_geoip())
|
||||
.then(|| state.geoip.country(context.client_ip))
|
||||
.flatten();
|
||||
if capture_country {
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
|
||||
}
|
||||
}
|
||||
if !frontend.profile_country_access.allows(country.as_deref())
|
||||
|| !frontend.rate_limit.country_allowed(country.as_deref())
|
||||
{
|
||||
@@ -1424,12 +1508,16 @@ async fn forward(
|
||||
"RustProxy: redirect target could not be rendered\n",
|
||||
);
|
||||
};
|
||||
response.extensions_mut().insert(RedirectResponse { http3_enabled: _redirect.http3_enabled });
|
||||
response.extensions_mut().insert(RedirectResponse {
|
||||
http3_enabled: _redirect.http3_enabled,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
if let Some(item) = local_host {
|
||||
let mut response = item.page.serve(request.method(), StatusCode::NOT_FOUND);
|
||||
response.extensions_mut().insert(LocalHostResponse { http3_enabled: item.http3_enabled });
|
||||
response.extensions_mut().insert(LocalHostResponse {
|
||||
http3_enabled: item.http3_enabled,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
let selected = snapshot.select_on(&host, &path, local.0, tls);
|
||||
@@ -1537,12 +1625,69 @@ async fn forward(
|
||||
if let Some(spec) = &route.response {
|
||||
return local_location_response(spec, request.method());
|
||||
}
|
||||
if let crate::config::CompiledSource::WebServer(service) = &route.source {
|
||||
if handshake.is_some() {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: Web Server source does not support WebSocket upgrades\n",
|
||||
);
|
||||
}
|
||||
let service_path = match route.rewritten_path(&path) {
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => path.clone(),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server rewrite path\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
|
||||
*exchange
|
||||
.cache_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(false);
|
||||
*exchange
|
||||
.transform_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(false);
|
||||
}
|
||||
// Do not hold a shared reference to Request<Body> across await. Axum handlers
|
||||
// must return Send futures, while Body is not Sync. Clone only static-request
|
||||
// metadata needed by the filesystem source before entering the async server.
|
||||
let static_method = request.method().clone();
|
||||
let static_headers = request.headers().clone();
|
||||
let static_uri = request.uri().clone();
|
||||
let static_version = request.version();
|
||||
let static_body = std::mem::replace(request.body_mut(), Body::empty());
|
||||
let mut response = crate::web_server::serve(
|
||||
service,
|
||||
static_method,
|
||||
static_headers,
|
||||
static_uri,
|
||||
&service_path,
|
||||
static_body,
|
||||
crate::web_server::RequestContext {
|
||||
client_ip: context.client_ip,
|
||||
client_port: Some(peer.0.port()),
|
||||
server_name: host.clone(),
|
||||
server_port: local.0.port(),
|
||||
tls,
|
||||
version: static_version,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
response.extensions_mut().insert(StaticSourceResponse);
|
||||
return response;
|
||||
}
|
||||
let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool {
|
||||
let selection = match &route.backend_id {
|
||||
Some(id) => pool.select_member_wait(id).await,
|
||||
None => pool
|
||||
.select_wait(context.client_ip, request.headers(), tls)
|
||||
.await,
|
||||
None => {
|
||||
pool.select_wait(context.client_ip, request.headers(), tls)
|
||||
.await
|
||||
}
|
||||
};
|
||||
let selection = match selection {
|
||||
Ok(selection) => selection,
|
||||
@@ -1573,7 +1718,13 @@ async fn forward(
|
||||
selection.cookie,
|
||||
)
|
||||
} else {
|
||||
(route.backend.clone(), None, None)
|
||||
let Some(backend) = route.backend.clone() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: Reverse Proxy route has no backend\n",
|
||||
);
|
||||
};
|
||||
(backend, None, None)
|
||||
};
|
||||
let admin_control_plane = admin_session_passthrough(&snapshot, &host, tls, &backend);
|
||||
if admin_control_plane {
|
||||
@@ -1588,9 +1739,14 @@ async fn forward(
|
||||
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());
|
||||
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(cache_settings.active_for(route.cache));
|
||||
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
*exchange
|
||||
.cache_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(cache_settings.active_for(route.cache));
|
||||
*exchange
|
||||
.transform_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(transform_settings.is_some_and(|settings| settings.enabled));
|
||||
}
|
||||
let policy_reason = if admin_control_plane {
|
||||
@@ -1654,7 +1810,10 @@ async fn forward(
|
||||
Ok(None) => public_suffix.clone(),
|
||||
Ok(Some(rewritten)) => {
|
||||
let Some(mut encoded) = encoded_origin_path(&rewritten) else {
|
||||
return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n");
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid rewritten request target\n",
|
||||
);
|
||||
};
|
||||
if let Some(query) = request.uri().query() {
|
||||
encoded.push('?');
|
||||
@@ -1662,7 +1821,12 @@ async fn forward(
|
||||
}
|
||||
encoded
|
||||
}
|
||||
Err(_) => return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n"),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid rewritten request target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
let Some(uri) = upstream_uri(&backend, &origin_suffix) else {
|
||||
return error(
|
||||
@@ -1684,8 +1848,7 @@ async fn forward(
|
||||
None
|
||||
};
|
||||
let mut h2_downstream = if handshake.as_ref().is_some_and(|handshake| handshake.h2())
|
||||
&& (backend.protocol != crate::config::UpstreamProtocol::Http2
|
||||
|| websocket_legacy_support)
|
||||
&& (backend.protocol != crate::config::UpstreamProtocol::Http2 || websocket_legacy_support)
|
||||
{
|
||||
Some(std::mem::replace(request.body_mut(), Body::empty()))
|
||||
} else {
|
||||
@@ -1847,7 +2010,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -1867,10 +2032,7 @@ async fn forward(
|
||||
});
|
||||
}
|
||||
}
|
||||
if cache_settings.proactive_refresh_enabled
|
||||
&& !hit.stale
|
||||
&& hit.remaining_ttl > 0
|
||||
{
|
||||
if cache_settings.proactive_refresh_enabled && !hit.stale && hit.remaining_ttl > 0 {
|
||||
schedule_proactive_refresh(
|
||||
CacheRefreshContext {
|
||||
state: state.clone(),
|
||||
@@ -1884,7 +2046,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -1918,10 +2082,8 @@ async fn forward(
|
||||
if hit.transformed {
|
||||
state.transform_metrics.record_cache_hit(scope);
|
||||
}
|
||||
cache_settings.annotate_resident_age(
|
||||
hit.response.headers_mut(),
|
||||
hit.resident_age_seconds,
|
||||
);
|
||||
cache_settings
|
||||
.annotate_resident_age(hit.response.headers_mut(), hit.resident_age_seconds);
|
||||
hit.response.extensions_mut().insert(ResponseRulesApplied);
|
||||
return hit.response;
|
||||
}
|
||||
@@ -1959,39 +2121,35 @@ async fn forward(
|
||||
.unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
}
|
||||
let serve_stale = |reason: &'static str, elapsed: Duration, origin_backend: &str| -> Option<Response> {
|
||||
let cached = cached_revalidation.as_ref()?;
|
||||
let mut hit = state.cache.stale_if_error_hit(cached)?;
|
||||
if let crate::cache::RangeSelection::Single(range) = range_selection {
|
||||
hit = hit.apply_range(range, if_range.as_ref());
|
||||
}
|
||||
cache_settings.annotate_reason(
|
||||
hit.response.headers_mut(),
|
||||
"HIT",
|
||||
hit.hits,
|
||||
0,
|
||||
reason,
|
||||
);
|
||||
cache_settings.annotate_transformed(hit.response.headers_mut(), hit.transformed);
|
||||
if hit.transformed {
|
||||
state.transform_metrics.record_cache_hit(scope);
|
||||
}
|
||||
cache_settings.annotate_resident_age(
|
||||
hit.response.headers_mut(),
|
||||
hit.resident_age_seconds,
|
||||
);
|
||||
hit.response.extensions_mut().insert(ResponseRulesApplied);
|
||||
hit.response.extensions_mut().insert(OriginTiming(elapsed));
|
||||
hit.response
|
||||
.extensions_mut()
|
||||
.insert(OriginBackend(origin_backend.to_owned()));
|
||||
state.cache.record(scope, &host, "HIT", reason);
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.cache_status.store(1, Ordering::Relaxed);
|
||||
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
Some(hit.response)
|
||||
};
|
||||
let serve_stale =
|
||||
|reason: &'static str, elapsed: Duration, origin_backend: &str| -> Option<Response> {
|
||||
let cached = cached_revalidation.as_ref()?;
|
||||
let mut hit = state.cache.stale_if_error_hit(cached)?;
|
||||
if let crate::cache::RangeSelection::Single(range) = range_selection {
|
||||
hit = hit.apply_range(range, if_range.as_ref());
|
||||
}
|
||||
cache_settings.annotate_reason(hit.response.headers_mut(), "HIT", hit.hits, 0, reason);
|
||||
cache_settings.annotate_transformed(hit.response.headers_mut(), hit.transformed);
|
||||
if hit.transformed {
|
||||
state.transform_metrics.record_cache_hit(scope);
|
||||
}
|
||||
cache_settings
|
||||
.annotate_resident_age(hit.response.headers_mut(), hit.resident_age_seconds);
|
||||
hit.response.extensions_mut().insert(ResponseRulesApplied);
|
||||
hit.response.extensions_mut().insert(OriginTiming(elapsed));
|
||||
hit.response
|
||||
.extensions_mut()
|
||||
.insert(OriginBackend(origin_backend.to_owned()));
|
||||
state.cache.record(scope, &host, "HIT", reason);
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.cache_status.store(1, Ordering::Relaxed);
|
||||
*exchange
|
||||
.cache_reason
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
Some(hit.response)
|
||||
};
|
||||
if only_cached {
|
||||
let mut response = error(
|
||||
StatusCode::GATEWAY_TIMEOUT,
|
||||
@@ -2006,9 +2164,7 @@ async fn forward(
|
||||
// A follower can wake after a different Vary representation was published; in
|
||||
// that case it competes for the next flight instead of stampeding the origin.
|
||||
let mut fill_leader = None;
|
||||
if handshake.is_none()
|
||||
&& matches!(range_selection, crate::cache::RangeSelection::None)
|
||||
{
|
||||
if handshake.is_none() && matches!(range_selection, crate::cache::RangeSelection::None) {
|
||||
if let Some(ticket) = cache_ticket.as_ref() {
|
||||
let mut collapse_rounds = 0u8;
|
||||
loop {
|
||||
@@ -2018,7 +2174,8 @@ async fn forward(
|
||||
break;
|
||||
}
|
||||
crate::cache::FillClaim::Waited => {
|
||||
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await {
|
||||
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await
|
||||
{
|
||||
let conditional = crate::cache::conditional_not_modified(
|
||||
&client_conditionals,
|
||||
hit.response.headers(),
|
||||
@@ -2045,7 +2202,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -2087,11 +2246,9 @@ async fn forward(
|
||||
}
|
||||
collapse_rounds = collapse_rounds.saturating_add(1);
|
||||
if collapse_rounds >= 8 {
|
||||
state.cache.record_reason(
|
||||
scope,
|
||||
&host,
|
||||
"cache_collapse_variant_limit",
|
||||
);
|
||||
state
|
||||
.cache
|
||||
.record_reason(scope, &host, "cache_collapse_variant_limit");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2100,13 +2257,17 @@ async fn forward(
|
||||
cache_settings.collapse_timeout_fallback,
|
||||
crate::cache::CollapseTimeoutFallback::StaleIfAvailable
|
||||
) {
|
||||
if let Some(stale) =
|
||||
serve_stale("cache_collapse_timeout_stale", Duration::ZERO, &backend.id)
|
||||
{
|
||||
if let Some(stale) = serve_stale(
|
||||
"cache_collapse_timeout_stale",
|
||||
Duration::ZERO,
|
||||
&backend.id,
|
||||
) {
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
state.cache.record_reason(scope, &host, "cache_collapse_timeout_origin");
|
||||
state
|
||||
.cache
|
||||
.record_reason(scope, &host, "cache_collapse_timeout_origin");
|
||||
break;
|
||||
}
|
||||
crate::cache::FillClaim::Disabled => break,
|
||||
@@ -2250,7 +2411,9 @@ async fn forward(
|
||||
&& handshake.is_none()
|
||||
&& request_body_empty
|
||||
&& retry_safe_method(&retry_method)
|
||||
&& retry_policy.as_ref().is_some_and(|policy| policy.max_retries > 0);
|
||||
&& retry_policy
|
||||
.as_ref()
|
||||
.is_some_and(|policy| policy.max_retries > 0);
|
||||
let upstream_started = Instant::now();
|
||||
let mut attempt_started = Instant::now();
|
||||
let mut active_backend = backend.clone();
|
||||
@@ -2269,7 +2432,11 @@ async fn forward(
|
||||
};
|
||||
let result = tokio::time::timeout(
|
||||
response_header_timeout,
|
||||
client.request(request_attempt.take().expect("upstream request attempt missing")),
|
||||
client.request(
|
||||
request_attempt
|
||||
.take()
|
||||
.expect("upstream request attempt missing"),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let attempt_elapsed = attempt_started.elapsed();
|
||||
@@ -2292,12 +2459,7 @@ async fn forward(
|
||||
break result;
|
||||
};
|
||||
let selection = match pool
|
||||
.select_retry_wait(
|
||||
context.client_ip,
|
||||
&retry_headers,
|
||||
tls,
|
||||
&attempted_backends,
|
||||
)
|
||||
.select_retry_wait(context.client_ip, &retry_headers, tls, &attempted_backends)
|
||||
.await
|
||||
{
|
||||
Ok(selection) => selection,
|
||||
@@ -2472,8 +2634,7 @@ async fn forward(
|
||||
};
|
||||
if handshake.as_ref().is_some_and(|handshake| {
|
||||
handshake.h3() || (handshake.h2() && !websocket_upstream_h2)
|
||||
})
|
||||
&& response.status().is_success()
|
||||
}) && response.status().is_success()
|
||||
&& response.status() != StatusCode::SWITCHING_PROTOCOLS
|
||||
{
|
||||
if let Some(exchange) = &exchange {
|
||||
@@ -2535,8 +2696,8 @@ async fn forward(
|
||||
if let Some(cookie) = active_affinity_cookie {
|
||||
parts.headers.append("set-cookie", cookie);
|
||||
}
|
||||
let h3_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h3())
|
||||
&& upgraded.is_some();
|
||||
let h3_tunnel =
|
||||
handshake.as_ref().is_some_and(|handshake| handshake.h3()) && upgraded.is_some();
|
||||
let h2_bridge_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h2())
|
||||
&& !websocket_upstream_h2
|
||||
&& upgraded.is_some();
|
||||
@@ -2589,15 +2750,13 @@ async fn forward(
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove("transfer-encoding");
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
parts
|
||||
.extensions
|
||||
.insert(websocket::H3Tunnel::new(
|
||||
upstream,
|
||||
exchange,
|
||||
active_backend.origin.clone(),
|
||||
lease,
|
||||
stop,
|
||||
));
|
||||
parts.extensions.insert(websocket::H3Tunnel::new(
|
||||
upstream,
|
||||
exchange,
|
||||
active_backend.origin.clone(),
|
||||
lease,
|
||||
stop,
|
||||
));
|
||||
Response::from_parts(parts, Body::empty())
|
||||
} else if h2_bridge_tunnel {
|
||||
let Some(upstream) = upgraded.take() else {
|
||||
@@ -2716,41 +2875,41 @@ async fn forward(
|
||||
.insert(OriginBackend(active_backend.id.clone()));
|
||||
if !range_requested && handshake.is_none() {
|
||||
if let Some(settings) = transform_settings.filter(|settings| settings.enabled) {
|
||||
let transform_started = Instant::now();
|
||||
response = match crate::transform::apply(
|
||||
response,
|
||||
&public_suffix,
|
||||
settings,
|
||||
origin_no_transform,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
state.transform_metrics.record_response(
|
||||
scope,
|
||||
&response,
|
||||
transform_started.elapsed(),
|
||||
);
|
||||
response
|
||||
}
|
||||
Err(cause) => {
|
||||
state
|
||||
.transform_metrics
|
||||
.record_error(scope, transform_started.elapsed());
|
||||
state.cache.record_reason(scope, &host, "transform_error");
|
||||
if let Some(exchange) = &exchange {
|
||||
*exchange
|
||||
.cache_reason
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = "transform_error";
|
||||
let transform_started = Instant::now();
|
||||
response = match crate::transform::apply(
|
||||
response,
|
||||
&public_suffix,
|
||||
settings,
|
||||
origin_no_transform,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
state.transform_metrics.record_response(
|
||||
scope,
|
||||
&response,
|
||||
transform_started.elapsed(),
|
||||
);
|
||||
response
|
||||
}
|
||||
tracing::debug!(error = %cause, "static response transformation failed");
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: upstream response body could not be transformed\n",
|
||||
);
|
||||
}
|
||||
};
|
||||
Err(cause) => {
|
||||
state
|
||||
.transform_metrics
|
||||
.record_error(scope, transform_started.elapsed());
|
||||
state.cache.record_reason(scope, &host, "transform_error");
|
||||
if let Some(exchange) = &exchange {
|
||||
*exchange
|
||||
.cache_reason
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = "transform_error";
|
||||
}
|
||||
tracing::debug!(error = %cause, "static response transformation failed");
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: upstream response body could not be transformed\n",
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2927,7 +3086,9 @@ async fn forward(
|
||||
path: path.clone(),
|
||||
scope: scope.to_owned(),
|
||||
timeout: frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.and_then(|frontend| {
|
||||
frontend.proxy_timeouts.response_header_timeout()
|
||||
})
|
||||
.unwrap_or(state.header_timeout),
|
||||
local: local.0,
|
||||
tls,
|
||||
@@ -2959,7 +3120,10 @@ async fn forward(
|
||||
let detail = upstream_failure_detail(&cause, &active_backend, elapsed, connect_failure);
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_request_failed", "upstream_http_error");
|
||||
*exchange.upstream_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(detail.clone());
|
||||
*exchange
|
||||
.upstream_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(detail.clone());
|
||||
}
|
||||
if let Some(lease) = &active_lease {
|
||||
lease.error_detail(
|
||||
@@ -2994,7 +3158,9 @@ async fn forward(
|
||||
};
|
||||
let stale_class = classify_stale_error(&cause);
|
||||
if cache_settings.allows_stale_error_class(stale_class) {
|
||||
if let Some(stale) = serve_stale("cache_stale_if_error", elapsed, &active_backend.id) {
|
||||
if let Some(stale) =
|
||||
serve_stale("cache_stale_if_error", elapsed, &active_backend.id)
|
||||
{
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
@@ -3022,7 +3188,10 @@ async fn forward(
|
||||
}
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_timeout", "header_timeout");
|
||||
*exchange.upstream_error.lock().unwrap_or_else(|e| e.into_inner()) = Some("response_header_timeout".into());
|
||||
*exchange
|
||||
.upstream_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some("response_header_timeout".into());
|
||||
}
|
||||
if let Some(lease) = &active_lease {
|
||||
lease.error_detail(
|
||||
@@ -3031,12 +3200,12 @@ async fn forward(
|
||||
final_attempt_elapsed,
|
||||
);
|
||||
}
|
||||
if cache_settings
|
||||
.allows_stale_error_class(crate::cache::StaleErrorClass::Timeout)
|
||||
{
|
||||
if let Some(stale) =
|
||||
serve_stale("cache_stale_if_error", upstream_started.elapsed(), &active_backend.id)
|
||||
{
|
||||
if cache_settings.allows_stale_error_class(crate::cache::StaleErrorClass::Timeout) {
|
||||
if let Some(stale) = serve_stale(
|
||||
"cache_stale_if_error",
|
||||
upstream_started.elapsed(),
|
||||
&active_backend.id,
|
||||
) {
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
@@ -3078,7 +3247,7 @@ mod authority_tests {
|
||||
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
||||
let route = snapshot.select("localhost", "/").unwrap();
|
||||
assert_eq!(
|
||||
upstream_request_version(&route.backend, false),
|
||||
upstream_request_version(route.backend.as_ref().unwrap(), false),
|
||||
Version::HTTP_2
|
||||
);
|
||||
}
|
||||
@@ -3107,9 +3276,8 @@ mod authority_tests {
|
||||
#[tokio::test]
|
||||
async fn h3_delete_unknown_nonempty_body_is_replayed_not_buffered_away() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::iter([Ok::<_, std::io::Error>(
|
||||
Bytes::from_static(b"payload"),
|
||||
)]);
|
||||
let stream =
|
||||
futures_util::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"payload"))]);
|
||||
let mut request = Request::builder()
|
||||
.method(Method::DELETE)
|
||||
.version(Version::HTTP_3)
|
||||
@@ -3221,7 +3389,10 @@ mod authority_tests {
|
||||
for name in ["etag", "x-cache", "x-cache-reason", "cache-status"] {
|
||||
assert!(!response.headers().contains_key(name), "{name}");
|
||||
}
|
||||
assert!(response.extensions().get::<LocalGeneratedResponse>().is_some());
|
||||
assert!(response
|
||||
.extensions()
|
||||
.get::<LocalGeneratedResponse>()
|
||||
.is_some());
|
||||
}
|
||||
#[test]
|
||||
fn http2_uses_authority_and_rejects_conflicting_host() {
|
||||
@@ -3238,11 +3409,9 @@ mod authority_tests {
|
||||
}
|
||||
#[test]
|
||||
fn legacy_websocket_origin_matches_upstream_scheme_and_host() {
|
||||
let origin = legacy_websocket_origin(
|
||||
"https://ilo.internal:443",
|
||||
&"10.0.0.5:443".parse().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let origin =
|
||||
legacy_websocket_origin("https://ilo.internal:443", &"10.0.0.5:443".parse().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(origin, "https://10.0.0.5:443");
|
||||
assert!(legacy_websocket_origin("not-an-origin", &"10.0.0.5".parse().unwrap()).is_none());
|
||||
}
|
||||
@@ -3311,18 +3480,39 @@ mod authority_tests {
|
||||
#[test]
|
||||
fn backend_diagnostics_are_opt_in_and_strip_spoofed_values() {
|
||||
let mut response = Response::new(Body::empty());
|
||||
response.headers_mut().insert("x-rust-proxy-backend", HeaderValue::from_static("spoofed"));
|
||||
response.headers_mut().insert("x-rust-proxy-backend-time", HeaderValue::from_static("legacy-spoof"));
|
||||
response.headers_mut().insert("x-rustproxy-backend-time", HeaderValue::from_static("999ms"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("x-rust-proxy-backend", HeaderValue::from_static("spoofed"));
|
||||
response.headers_mut().insert(
|
||||
"x-rust-proxy-backend-time",
|
||||
HeaderValue::from_static("legacy-spoof"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"x-rustproxy-backend-time",
|
||||
HeaderValue::from_static("999ms"),
|
||||
);
|
||||
apply_backend_diagnostic_values(&mut response, false, false);
|
||||
assert!(response.headers().get("x-rust-proxy-backend").is_none());
|
||||
assert!(response.headers().get("x-rust-proxy-backend-time").is_none());
|
||||
assert!(response
|
||||
.headers()
|
||||
.get("x-rust-proxy-backend-time")
|
||||
.is_none());
|
||||
assert!(response.headers().get("x-rustproxy-backend-time").is_none());
|
||||
|
||||
response.extensions_mut().insert(OriginBackend("origin-a".into()));
|
||||
response.extensions_mut().insert(OriginTiming(Duration::from_millis(12)));
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(OriginBackend("origin-a".into()));
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(OriginTiming(Duration::from_millis(12)));
|
||||
apply_backend_diagnostic_values(&mut response, true, true);
|
||||
assert_eq!(response.headers().get("x-rust-proxy-backend").unwrap(), "origin-a");
|
||||
assert_eq!(response.headers().get("x-rustproxy-backend-time").unwrap(), "12.00ms");
|
||||
assert_eq!(
|
||||
response.headers().get("x-rust-proxy-backend").unwrap(),
|
||||
"origin-a"
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers().get("x-rustproxy-backend-time").unwrap(),
|
||||
"12.00ms"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user