This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 09:30:21 +02:00
1 parent 1dea7f55c8
commit c819f0a335
151 files changed
+7610 -4264

No files matched your search

+46 -10
View File
@@ -25,6 +25,7 @@ pub struct Exchange {
pub vhost_metrics: Option<Arc<crate::metrics::VhostMetrics>>,
pub vhost: Option<String>,
pub route: Mutex<Option<String>>,
pub source: Mutex<&'static str>,
pub cache_effective_enabled: Mutex<Option<bool>>,
pub transform_effective_enabled: Mutex<Option<bool>>,
pub backend: Mutex<Option<Arc<crate::balance::Lease>>>,
@@ -60,13 +61,22 @@ pub struct Exchange {
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) {
@@ -130,12 +140,30 @@ 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 source = *self.source.lock().unwrap_or_else(|e| e.into_inner());
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;
@@ -168,6 +196,7 @@ impl Drop for Exchange {
request_bytes,
response_bytes,
cache,
source,
user_agent: self.user_agent.clone(),
country: country.clone(),
});
@@ -189,13 +218,20 @@ 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(),
upstream_error: self
.upstream_error
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone(),
client_ip,
user_agent: self.user_agent.clone(),
country,
+1
View File
@@ -11,6 +11,7 @@ mod tests {
vhost_metrics: None,
vhost: None,
route: Mutex::new(None),
source: Mutex::new("unresolved"),
cache_effective_enabled: Mutex::new(None),
transform_effective_enabled: Mutex::new(None),
backend: Mutex::new(None),
+9 -11
View File
@@ -42,10 +42,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;
@@ -120,10 +118,7 @@ fn error(status: StatusCode, message: &'static str) -> Response {
response
}
fn method_policy_denial(
policy: &crate::vhost::MethodPolicy,
method: &Method,
) -> Option<Response> {
fn method_policy_denial(policy: &crate::vhost::MethodPolicy, method: &Method) -> Option<Response> {
if policy.allows(method) {
return None;
}
@@ -142,7 +137,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);
@@ -191,8 +190,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
+11
View File
@@ -25,6 +25,17 @@ if !snapshot.host_allowed(&host, context.client_ip)
"RustProxy: denied by access policy\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.source.lock().unwrap_or_else(|e| e.into_inner()) = if route.response.is_some() {
"local_response"
} else {
match &route.source {
crate::config::CompiledSource::WebServer(_) => "web_server",
crate::config::CompiledSource::ReverseProxy => "reverse_proxy",
}
};
}
// Local location responses intentionally still pass host/profile/location ACLs
// and the outer response-header/security pipeline, but never select/open an upstream.
if let Some(spec) = &route.response {
+61 -17
View File
@@ -95,8 +95,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.
@@ -147,7 +147,9 @@ async fn forward(
return response;
}
if !tls {
if let Some(response) = frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri())) {
if let Some(response) =
frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri()))
{
return response;
}
}
@@ -167,6 +169,7 @@ async fn forward(
)
}
};
let mut cloudflare_country = None;
if frontend.is_some_and(|frontend| frontend.cloudflare_client_ip) {
if let Some(client) = crate::forwarding::cloudflare_client_ip(request.headers(), peer.0) {
if context.override_client_ip(client).is_err() {
@@ -175,10 +178,12 @@ async fn forward(
"RustProxy: invalid Cloudflare client address\n",
);
}
cloudflare_country = crate::forwarding::cloudflare_country(request.headers(), peer.0);
}
}
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());
}
// GeoIP stays off the normal request path. Resolve lazily for any vhost or
// location policy that actually needs a country code, or for log capture.
@@ -190,7 +195,9 @@ async fn forward(
|| frontend.profile_country_access.needs_geoip()
|| frontend.rate_limit.needs_geoip()
{
country = state.geoip.country(context.client_ip);
country = cloudflare_country
.clone()
.or_else(|| state.geoip.country(context.client_ip));
country_resolved = true;
}
if capture_country {
@@ -245,12 +252,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);
@@ -275,7 +286,9 @@ async fn forward(
if (route.profile_country_access.needs_geoip() || route.country_access.needs_geoip())
&& !country_resolved
{
country = state.geoip.country(context.client_ip);
country = cloudflare_country
.clone()
.or_else(|| state.geoip.country(context.client_ip));
if capture_country {
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
@@ -360,20 +373,51 @@ async fn forward(
});
};
let (
backend, backend_lease, affinity_cookie, admin_control_plane, cache_settings,
transform_settings, scope, ignore_static_cookie, only_cached, public_suffix,
origin_suffix, uri, exchange, lease, stop, downstream, mut h2_downstream,
range_selection, if_range, range_requested, effective_reason, revalidation_reason,
cache_ticket, client_conditionals, cached_revalidation, freshness_revalidation_reason,
pending_revalidation, mut miss_recorded,
backend,
backend_lease,
affinity_cookie,
admin_control_plane,
cache_settings,
transform_settings,
scope,
ignore_static_cookie,
only_cached,
public_suffix,
origin_suffix,
uri,
exchange,
lease,
stop,
downstream,
mut h2_downstream,
range_selection,
if_range,
range_requested,
effective_reason,
revalidation_reason,
cache_ticket,
client_conditionals,
cached_revalidation,
freshness_revalidation_reason,
pending_revalidation,
mut miss_recorded,
) = include!("forward/route_cache.rs");
let (serve_stale, mut fill_leader) = include!("forward/cache_lookup.rs");
let (
websocket_upstream_h2, revalidation_sent, mut h2_direct_session, request_body_empty,
proactive_base_headers, upstream_started, active_backend,
active_lease, active_affinity_cookie, active_uri, retries, upstream_result,
websocket_upstream_h2,
revalidation_sent,
mut h2_direct_session,
request_body_empty,
proactive_base_headers,
upstream_started,
active_backend,
active_lease,
active_affinity_cookie,
active_uri,
retries,
upstream_result,
final_attempt_elapsed,
) = include!("forward/upstream.rs");
+3 -1
View File
@@ -54,7 +54,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 {
+38 -17
View File
@@ -57,9 +57,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)
@@ -171,7 +170,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() {
@@ -188,11 +190,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());
}
@@ -261,18 +261,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"
);
}
}
+20 -16
View File
@@ -27,9 +27,13 @@ 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)]
@@ -82,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 {
@@ -117,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",
@@ -219,7 +228,6 @@ where
detail
}
#[derive(Clone)]
struct CacheRefreshContext {
state: Arc<App>,
@@ -273,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 {
+75 -24
View File
@@ -105,13 +105,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 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"),
@@ -128,6 +132,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
vhost_metrics: frontend_context.as_ref().map(|f| f.metrics.clone()),
vhost: frontend_context.as_ref().map(|f| f.id.clone()),
route: Mutex::new(None),
source: Mutex::new("unresolved"),
cache_effective_enabled: Mutex::new(None),
transform_effective_enabled: Mutex::new(None),
backend: Mutex::new(None),
@@ -180,7 +185,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()
@@ -201,17 +209,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()
@@ -241,10 +264,18 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
&request_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(
@@ -267,9 +298,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::<StaticSourceResponse>().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>()
@@ -300,8 +340,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.
@@ -358,7 +404,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 {
@@ -377,7 +426,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()
+2 -3
View File
@@ -14,8 +14,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,
@@ -283,8 +283,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() {
+35 -12
View File
@@ -20,9 +20,11 @@ mod tests {
.header("sec-websocket-version", "13")
.body(Body::empty())
.unwrap();
request
.extensions_mut()
.insert("websocket".parse::<h3::ext::Protocol>().unwrap_or_else(|_| panic!("static websocket H3 protocol token must parse")));
request.extensions_mut().insert(
"websocket"
.parse::<h3::ext::Protocol>()
.unwrap_or_else(|_| panic!("static websocket H3 protocol token must parse")),
);
request
}
fn h2_request() -> Request {
@@ -80,22 +82,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]
@@ -139,9 +160,11 @@ mod tests {
assert!(validate_h3(&request).is_err(), "accepted forbidden {name}");
}
let mut request = h3_request();
request
.extensions_mut()
.insert("connect-udp".parse::<h3::ext::Protocol>().unwrap_or_else(|_| panic!("static connect-udp H3 protocol token must parse")));
request.extensions_mut().insert(
"connect-udp"
.parse::<h3::ext::Protocol>()
.unwrap_or_else(|_| panic!("static connect-udp H3 protocol token must parse")),
);
assert!(validate_h3(&request).is_err());
}
#[test]
+5 -1
View File
@@ -406,7 +406,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
+27 -11
View File
@@ -9,7 +9,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,
@@ -70,7 +72,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))),
}
@@ -212,21 +217,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>(())
};