v0.25.0
This commit is contained in:
1 parent
2e4aa4df4d
commit
87e571c170
106 files changed
+17952
-6185
No files matched your search
+97
-30
@@ -41,6 +41,8 @@ pub struct Exchange {
|
||||
pub upstream_status: AtomicU16,
|
||||
pub upstream_error: Mutex<Option<String>>,
|
||||
pub client_ip: Mutex<Option<String>>,
|
||||
pub user_agent: Option<String>,
|
||||
pub country: Mutex<Option<String>>,
|
||||
pub started: Instant,
|
||||
pub status: AtomicU16,
|
||||
pub cache_status: AtomicU16,
|
||||
@@ -51,18 +53,28 @@ pub struct Exchange {
|
||||
pub response_bytes: AtomicU64,
|
||||
pub metrics: Arc<Metrics>,
|
||||
pub logs: Arc<Logs>,
|
||||
pub traffic: Arc<crate::traffic::Analytics>,
|
||||
pub _permit: OwnedSemaphorePermit,
|
||||
}
|
||||
impl Exchange {
|
||||
pub fn websocket_phase(&self, phase: &'static str) {
|
||||
if self.websocket_bootstrap.is_some() {
|
||||
*self.websocket_phase.lock().unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
*self
|
||||
.websocket_phase
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
}
|
||||
}
|
||||
pub fn websocket_error(&self, phase: &'static str, error: &'static str) {
|
||||
if self.websocket_bootstrap.is_some() {
|
||||
*self.websocket_phase.lock().unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
*self.websocket_error.lock().unwrap_or_else(|e| e.into_inner()) = Some(error);
|
||||
*self
|
||||
.websocket_phase
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(phase);
|
||||
*self
|
||||
.websocket_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(error);
|
||||
}
|
||||
}
|
||||
pub fn h3_reset(&self, code: &'static str) {
|
||||
@@ -126,13 +138,71 @@ impl Drop for Exchange {
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.as_ref()
|
||||
.map(|l| l.member.backend.id.clone());
|
||||
let websocket_phase = *self.websocket_phase.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let websocket_phase = *self
|
||||
.websocket_phase
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let route = self.route.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let cache_effective_enabled = *self
|
||||
.cache_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let transform_effective_enabled = *self
|
||||
.transform_effective_enabled
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let client_ip = self
|
||||
.client_ip
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
let country = self
|
||||
.country
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone();
|
||||
let request_bytes = self.request_bytes.load(Ordering::Relaxed);
|
||||
let response_bytes = self.response_bytes.load(Ordering::Relaxed);
|
||||
let duration_ms = duration.as_millis() as u64;
|
||||
let cache = match self.cache_status.load(Ordering::Relaxed) {
|
||||
1 => "HIT",
|
||||
2 => "MISS",
|
||||
3 => "BYPASS",
|
||||
_ => "-",
|
||||
};
|
||||
let outcome = if failed {
|
||||
"body_error"
|
||||
} else if aborted {
|
||||
"cancelled"
|
||||
} else if status >= 500 {
|
||||
"proxy_or_upstream_error"
|
||||
} else if status == 101 || websocket_phase == Some("tunnel_closed") {
|
||||
"websocket_closed"
|
||||
} else {
|
||||
"complete"
|
||||
};
|
||||
if let Some(vhost) = self.vhost.clone() {
|
||||
self.traffic.push(crate::traffic::Sample {
|
||||
vhost,
|
||||
route: route.clone(),
|
||||
method: self.method.clone(),
|
||||
path: self.path.clone(),
|
||||
client_ip: client_ip.clone(),
|
||||
status,
|
||||
duration_ms,
|
||||
request_bytes,
|
||||
response_bytes,
|
||||
cache,
|
||||
user_agent: self.user_agent.clone(),
|
||||
country: country.clone(),
|
||||
});
|
||||
}
|
||||
self.logs.push(LogEntry {
|
||||
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
vhost: self.vhost.clone(),
|
||||
route: self.route.lock().unwrap_or_else(|e| e.into_inner()).clone(),
|
||||
cache_effective_enabled: *self.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
transform_effective_enabled: *self.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
route,
|
||||
cache_effective_enabled,
|
||||
transform_effective_enabled,
|
||||
backend,
|
||||
id: self.id,
|
||||
time_ms: now_ms(),
|
||||
@@ -143,35 +213,29 @@ impl Drop for Exchange {
|
||||
transport: self.transport,
|
||||
websocket_bootstrap: self.websocket_bootstrap,
|
||||
websocket_phase,
|
||||
websocket_error: *self.websocket_error.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
websocket_error: *self
|
||||
.websocket_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()),
|
||||
h3_reset_code: *self.h3_reset_code.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
upstream_status: match self.upstream_status.load(Ordering::Relaxed) {
|
||||
0 => None,
|
||||
value => Some(value),
|
||||
},
|
||||
upstream_error: self.upstream_error.lock().unwrap_or_else(|e| e.into_inner()).clone(),
|
||||
client_ip: self.client_ip.lock().unwrap_or_else(|e| e.into_inner()).clone(),
|
||||
upstream_error: self
|
||||
.upstream_error
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone(),
|
||||
client_ip,
|
||||
user_agent: self.user_agent.clone(),
|
||||
country,
|
||||
status,
|
||||
duration_ms: duration.as_millis() as u64,
|
||||
cache: match self.cache_status.load(Ordering::Relaxed) {
|
||||
1 => "HIT",
|
||||
2 => "MISS",
|
||||
3 => "BYPASS",
|
||||
_ => "-",
|
||||
},
|
||||
request_bytes: self.request_bytes.load(Ordering::Relaxed),
|
||||
response_bytes: self.response_bytes.load(Ordering::Relaxed),
|
||||
outcome: if failed {
|
||||
"body_error"
|
||||
} else if aborted {
|
||||
"cancelled"
|
||||
} else if status >= 500 {
|
||||
"proxy_or_upstream_error"
|
||||
} else if status == 101 || websocket_phase == Some("tunnel_closed") {
|
||||
"websocket_closed"
|
||||
} else {
|
||||
"complete"
|
||||
},
|
||||
duration_ms,
|
||||
cache,
|
||||
request_bytes,
|
||||
response_bytes,
|
||||
outcome,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -332,6 +396,8 @@ mod tests {
|
||||
upstream_status: AtomicU16::new(0),
|
||||
upstream_error: Mutex::new(None),
|
||||
client_ip: Mutex::new(Some("127.0.0.1".into())),
|
||||
user_agent: Some("RustProxy-Test/1.0".into()),
|
||||
country: Mutex::new(Some("PL".into())),
|
||||
started: Instant::now(),
|
||||
status: AtomicU16::new(200),
|
||||
cache_status: AtomicU16::new(0),
|
||||
@@ -342,6 +408,7 @@ mod tests {
|
||||
response_bytes: AtomicU64::new(0),
|
||||
metrics,
|
||||
logs: Arc::new(Logs::default()),
|
||||
traffic: crate::traffic::Analytics::start(tokio_util::sync::CancellationToken::new()),
|
||||
_permit: permit,
|
||||
});
|
||||
(exchange, slots)
|
||||
|
||||
+11
-3
@@ -62,12 +62,18 @@ mod tests {
|
||||
#[test]
|
||||
fn admin_cookie_can_be_restored_only_from_captured_fragment() {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert("cookie", "a=1; rustproxy_session=secret; b=2".parse().unwrap());
|
||||
h.insert(
|
||||
"cookie",
|
||||
"a=1; rustproxy_session=secret; b=2".parse().unwrap(),
|
||||
);
|
||||
let admin = strip_admin_cookie(&mut h);
|
||||
assert_eq!(h.get("cookie").unwrap(), "a=1; b=2");
|
||||
assert_eq!(admin, vec!["rustproxy_session=secret"]);
|
||||
restore_admin_cookie(&mut h, &admin);
|
||||
assert_eq!(h.get("cookie").unwrap(), "a=1; b=2; rustproxy_session=secret");
|
||||
assert_eq!(
|
||||
h.get("cookie").unwrap(),
|
||||
"a=1; b=2; rustproxy_session=secret"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +91,9 @@ pub fn strip_admin_cookie(headers: &mut HeaderMap) -> Vec<String> {
|
||||
.flat_map(|h| h.split(';'))
|
||||
{
|
||||
let part = part.trim();
|
||||
let Some((name, _)) = part.split_once('=') else { continue; };
|
||||
let Some((name, _)) = part.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
if name == crate::auth::COOKIE_NAME {
|
||||
admin.push(part.to_owned());
|
||||
} else {
|
||||
|
||||
+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"
|
||||
);
|
||||
}
|
||||
}
|
||||
+59
-21
@@ -19,8 +19,8 @@ use http_body_util::BodyExt;
|
||||
use hyper::upgrade::{OnUpgrade, Upgraded};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use sha1::{Digest, Sha1};
|
||||
use serde::Serialize;
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
future::Future,
|
||||
@@ -288,8 +288,7 @@ pub fn validate_response(
|
||||
"invalid upstream upgrade"
|
||||
);
|
||||
ensure!(
|
||||
!headers.contains_key("transfer-encoding")
|
||||
&& !headers.contains_key("content-length"),
|
||||
!headers.contains_key("transfer-encoding") && !headers.contains_key("content-length"),
|
||||
"101 response must not include body framing"
|
||||
);
|
||||
for value in headers.get_all("connection").iter() {
|
||||
@@ -829,7 +828,11 @@ pub fn spawn_h2_idle_watch(state: Arc<App>, session: &Arc<TunnelSession>) {
|
||||
return;
|
||||
};
|
||||
if session.inactive_for() >= idle {
|
||||
session._active.exchange.failed.store(true, Ordering::Relaxed);
|
||||
session
|
||||
._active
|
||||
.exchange
|
||||
.failed
|
||||
.store(true, Ordering::Relaxed);
|
||||
session
|
||||
._active
|
||||
.exchange
|
||||
@@ -858,7 +861,9 @@ pub fn bridge_h2_to_h1(
|
||||
lease: Option<Arc<ConnectionLease>>,
|
||||
stop: Option<ServerShutdown>,
|
||||
) -> Body {
|
||||
let stop = stop.map(|value| value.0).unwrap_or_else(|| state.shutdown.clone());
|
||||
let stop = stop
|
||||
.map(|value| value.0)
|
||||
.unwrap_or_else(|| state.shutdown.clone());
|
||||
let session = state.tunnel_registry.register(
|
||||
exchange.clone(),
|
||||
upstream_label,
|
||||
@@ -919,7 +924,10 @@ pub fn bridge_h2_to_h1(
|
||||
Ok(read) => {
|
||||
buffer.truncate(read);
|
||||
record_session_bytes(&exchange, &session, read as u64, false);
|
||||
Some((Ok::<Bytes, io::Error>(Bytes::from(buffer)), (upstream_read, session, exchange)))
|
||||
Some((
|
||||
Ok::<Bytes, io::Error>(Bytes::from(buffer)),
|
||||
(upstream_read, session, exchange),
|
||||
))
|
||||
}
|
||||
Err(error) => Some((Err(error), (upstream_read, session, exchange))),
|
||||
}
|
||||
@@ -1061,21 +1069,32 @@ pub async fn run_h3(
|
||||
let stop = stop.map(|v| v.0).unwrap_or_else(|| state.shutdown.clone());
|
||||
let started = Instant::now();
|
||||
let activity = Arc::new(AtomicU64::new(0));
|
||||
let session = state.tunnel_registry.register(
|
||||
exchange.clone(),
|
||||
upstream_label,
|
||||
"h3-rfc9220",
|
||||
stop,
|
||||
lease,
|
||||
);
|
||||
let session =
|
||||
state
|
||||
.tunnel_registry
|
||||
.register(exchange.clone(), upstream_label, "h3-rfc9220", stop, lease);
|
||||
let stop = session.cancellation();
|
||||
let upstream = TokioIo::new(upstream);
|
||||
let (mut upstream_read, mut upstream_write) = tokio::io::split(upstream);
|
||||
let result = {
|
||||
let copying = async {
|
||||
tokio::try_join!(
|
||||
h3_client_to_backend(receive, &mut upstream_write, &exchange, &activity, started, &session),
|
||||
backend_to_h3(&mut upstream_read, send, &exchange, &activity, started, &session)
|
||||
h3_client_to_backend(
|
||||
receive,
|
||||
&mut upstream_write,
|
||||
&exchange,
|
||||
&activity,
|
||||
started,
|
||||
&session
|
||||
),
|
||||
backend_to_h3(
|
||||
&mut upstream_read,
|
||||
send,
|
||||
&exchange,
|
||||
&activity,
|
||||
started,
|
||||
&session
|
||||
)
|
||||
)?;
|
||||
Ok::<(), io::Error>(())
|
||||
};
|
||||
@@ -1312,22 +1331,41 @@ mod tests {
|
||||
#[test]
|
||||
fn rfc8441_h2_response_rejects_h1_framing_fields() {
|
||||
let handshake = validate_h2(&h2_request()).unwrap();
|
||||
for (name, value) in [("connection", "close"), ("upgrade", "websocket"), ("content-length", "0")] {
|
||||
for (name, value) in [
|
||||
("connection", "close"),
|
||||
("upgrade", "websocket"),
|
||||
("content-length", "0"),
|
||||
] {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
http::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
||||
value.parse().unwrap(),
|
||||
);
|
||||
assert!(validate_h2_response(&headers, &handshake).is_err(), "accepted forbidden response {name}");
|
||||
assert!(
|
||||
validate_h2_response(&headers, &handshake).is_err(),
|
||||
"accepted forbidden response {name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_transport_metrics_classify_frontend_protocol() {
|
||||
assert!(matches!(TunnelTransport::from_protocol("h1-upgrade"), TunnelTransport::H1));
|
||||
assert!(matches!(TunnelTransport::from_protocol("h2-rfc8441-h1-upstream"), TunnelTransport::H2));
|
||||
assert!(matches!(TunnelTransport::from_protocol("h2-rfc8441-h2-upstream"), TunnelTransport::H2));
|
||||
assert!(matches!(TunnelTransport::from_protocol("h3-rfc9220"), TunnelTransport::H3));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h1-upgrade"),
|
||||
TunnelTransport::H1
|
||||
));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h2-rfc8441-h1-upstream"),
|
||||
TunnelTransport::H2
|
||||
));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h2-rfc8441-h2-upstream"),
|
||||
TunnelTransport::H2
|
||||
));
|
||||
assert!(matches!(
|
||||
TunnelTransport::from_protocol("h3-rfc9220"),
|
||||
TunnelTransport::H3
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user