v0.25.5
This commit is contained in:
1 parent
1dea7f55c8
commit
c819f0a335
151 files changed
+7610
-4264
No files matched your search
+46
-10
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(())
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user