This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+398
View File
@@ -0,0 +1,398 @@
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
let snapshot = state.config.load_full();
let (frontend_context, route_context) = response_context(&snapshot, &request);
let local = request.extensions().get::<LocalAddr>().map(|v| v.0);
let transport_tls = request
.extensions()
.get::<Transport>()
.copied()
.unwrap_or_default()
.tls;
request.extensions_mut().insert(snapshot.clone());
let original_headers = request.headers().clone();
let method = request.method().clone();
let is_head = method == Method::HEAD;
let ws_attempt = request
.headers()
.get("upgrade")
.is_some_and(|v| v.as_bytes().eq_ignore_ascii_case(b"websocket"))
|| (request.version() == Version::HTTP_2
&& request.method() == Method::CONNECT
&& websocket::h2_protocol(&request) == Some("websocket"))
|| (request.version() == Version::HTTP_3
&& request.method() == Method::CONNECT
&& websocket::h3_protocol(&request) == Some("websocket"));
if request.version() == Version::HTTP_11 {
state.metrics.http1_requests.fetch_add(1, Ordering::Relaxed);
}
if request.version() == Version::HTTP_2 {
state.metrics.http2_requests.fetch_add(1, Ordering::Relaxed);
}
if request.version() == Version::HTTP_3 {
state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed);
}
let id = state.metrics.requests.fetch_add(1, Ordering::Relaxed) + 1;
if let Some(frontend) = &frontend_context {
frontend.metrics.requests.fetch_add(1, Ordering::Relaxed);
}
let Ok(permit) = state.inflight.clone().try_acquire_owned() else {
state
.metrics
.rejected_requests
.fetch_add(1, Ordering::Relaxed);
if let Some(frontend) = &frontend_context {
frontend.metrics.rejected.fetch_add(1, Ordering::Relaxed);
}
let mut response = error(
StatusCode::SERVICE_UNAVAILABLE,
"RustProxy: in-flight limit reached\n",
);
if let Some(page) = frontend_context
.as_ref()
.and_then(|f| f.pages.errors.get(&503))
.or_else(|| snapshot.envelope.config.pages.errors.get(&503))
{
crate::pages::replace_error(&mut response, page);
}
if is_head {
*response.body_mut() = Body::empty();
}
apply_response_rules(
response.headers_mut(),
&snapshot,
frontend_context.as_deref(),
route_context.as_ref(),
false,
);
finish_headers(
&mut response,
&snapshot,
frontend_context.as_deref(),
local,
transport_tls,
id,
&state.active_listeners,
);
return response;
};
state.metrics.active.fetch_add(1, Ordering::Relaxed);
if let Some(frontend) = &frontend_context {
frontend.metrics.active.fetch_add(1, Ordering::Relaxed);
}
let host = request
.headers()
.get(HOST)
.and_then(|h| h.to_str().ok())
.unwrap_or_else(|| request.uri().authority().map(|v| v.as_str()).unwrap_or(""))
.chars()
.take(253)
.collect();
let initial_client_ip = request
.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"),
Version::HTTP_2 => ("2", "h2"),
Version::HTTP_3 => ("3", "h3"),
_ => ("unknown", "unknown"),
};
let websocket_bootstrap = ws_attempt.then_some(match request.version() {
Version::HTTP_2 => "h2_extended_connect",
Version::HTTP_3 => "h3_extended_connect",
_ => "h1_upgrade",
});
let exchange = Arc::new(Exchange {
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),
cache_effective_enabled: Mutex::new(None),
transform_effective_enabled: Mutex::new(None),
backend: Mutex::new(None),
id,
method: request.method().to_string(),
host,
path: request.uri().path().chars().take(256).collect(),
http_version,
transport,
websocket_bootstrap,
websocket_phase: Mutex::new(websocket_bootstrap.map(|_| "request_received")),
websocket_error: Mutex::new(None),
h3_reset_code: Mutex::new(None),
upstream_status: AtomicU16::new(0),
upstream_error: Mutex::new(None),
// Always log at least the transport peer. `forward` replaces this with
// 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),
cache_reason: Mutex::new("not_proxy_response"),
failed: AtomicBool::new(false),
aborted: AtomicBool::new(false),
request_bytes: AtomicU64::new(0),
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()) {
request.headers_mut().insert("x-request-id", value);
}
request.extensions_mut().insert(exchange.clone());
let request_send_timeout = frontend_context
.as_ref()
.and_then(|frontend| frontend.proxy_timeouts.send_timeout())
.unwrap_or(state.body_idle_timeout);
let request = request.map(|body| {
Body::new(MeteredBody::new(
body,
exchange.clone(),
Direction::Request,
request_send_timeout,
))
});
let mut response = next.run(request).await;
let minimal_welcome = response.extensions().get::<crate::pages::MinimalWelcome>().is_some();
let code = response.status().as_u16();
let custom_error = frontend_context
.as_ref()
.and_then(|f| f.pages.errors.get(&code))
.or_else(|| snapshot.envelope.config.pages.errors.get(&code));
if !minimal_welcome
&& response
.extensions()
.get::<crate::tinyauth::AuthenticationFailure>()
.is_none()
{
if let Some(page) = custom_error {
crate::pages::replace_error(&mut response, page);
}
}
if minimal_welcome {
// Keep the built-in "RustProxy is running" response intentionally
// 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",
"x-content-type-options",
] {
response.headers_mut().remove(name);
}
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
} else {
if response
.extensions()
.get::<ResponseRulesApplied>()
.is_none()
&& response
.extensions()
.get::<crate::tinyauth::AuthenticationFailure>()
.is_none()
{
apply_response_rules(
response.headers_mut(),
&snapshot,
frontend_context.as_deref(),
route_context.as_ref(),
false,
);
}
apply_timing_headers(&mut response, frontend_context.as_deref(), &exchange);
apply_backend_diagnostic_headers(&mut response, frontend_context.as_deref());
finish_headers(
&mut response,
&snapshot,
frontend_context.as_deref(),
local,
transport_tls,
id,
&state.active_listeners,
);
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) {
response.headers_mut().remove("alt-svc");
}
response = crate::compression::encode(
response,
&original_headers,
&method,
&snapshot.envelope.config.compression,
)
.await;
if let Some(encoding) = response
.extensions()
.get::<crate::compression::AppliedEncoding>()
.copied()
{
state.metrics.record_compression(encoding);
}
if response
.extensions()
.get::<ResponseRulesApplied>()
.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::tinyauth::AuthenticationFailure>()
.is_none()
{
let outcome = match exchange.cache_status.load(Ordering::Relaxed) {
1 => "HIT",
2 => "MISS",
_ => "BYPASS",
};
let cache = frontend_context
.as_ref()
.map(|f| &f.cache)
.unwrap_or(&snapshot.envelope.config.cache);
let reason = *exchange
.cache_reason
.lock()
.unwrap_or_else(|e| e.into_inner());
cache.annotate_reason(response.headers_mut(), outcome, 0, 0, reason);
let transformed = response
.extensions()
.get::<crate::transform::Applied>()
.is_some()
|| response
.extensions()
.get::<crate::transform::CachedTransformed>()
.is_some();
cache.annotate_transformed(response.headers_mut(), transformed);
}
}
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.
enforce_local_page_no_cache(&mut response);
}
if response.extensions().get::<LocalHostResponse>().is_some() {
// 404 hosts are fully local and never participate in RustProxy caching.
// Keep their wire response free of cache policy, validators and cache diagnostics.
for name in [
"cache-control",
"age",
"expires",
"pragma",
"etag",
"last-modified",
"cdn-cache-control",
"surrogate-control",
"x-cache",
"x-cache-hits",
"x-cache-status",
"x-cache-resident-age",
"x-cache-reason",
"cache-status",
] {
response.headers_mut().remove(name);
}
}
exchange
.status
.store(response.status().as_u16(), Ordering::Relaxed);
let ws_success = response.status() == StatusCode::SWITCHING_PROTOCOLS
|| response.extensions().get::<websocket::H3Tunnel>().is_some();
if ws_attempt && !ws_success {
state
.metrics
.websocket_handshake_errors
.fetch_add(1, Ordering::Relaxed);
}
if is_head {
*response.body_mut() = Body::empty();
}
let response_read_timeout = frontend_context
.as_ref()
.and_then(|frontend| frontend.proxy_timeouts.read_timeout())
.unwrap_or(state.body_idle_timeout);
response.map(|body| {
Body::new(MeteredBody::new(
body,
exchange,
Direction::Response,
response_read_timeout,
))
})
}
/// Build a same-host HTTPS redirect while preserving the request path and query.
fn force_ssl_location(host: &str, uri: &Uri, port: u16) -> Option<HeaderValue> {
let path = uri.path_and_query().map(|value| value.as_str()).unwrap_or("/");
let location = if port == 443 {
format!("https://{host}{path}")
} else {
format!("https://{host}:{port}{path}")
};
HeaderValue::from_str(&location).ok()
}
fn force_ssl_redirect(
frontend: &crate::config::CompiledFrontend,
host: &str,
uri: &Uri,
) -> Option<Response> {
if !frontend.force_ssl {
return None;
}
let location = force_ssl_location(host, uri, frontend.https_redirect_port?)?;
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
response.headers_mut().insert(http::header::LOCATION, location);
// Do not pin a redirect in a browser after an operator later disables Force SSL.
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
Some(response)
}
/// HTTP/2 and HTTP/3 authority is a pseudo-header, not necessarily a Host field.
/// HTTP/1 absolute-form remains forbidden: this is not a forward proxy.
fn effective_host(request: &Request, tls: bool) -> Result<HeaderValue, ()> {
let count = request.headers().get_all(HOST).iter().count();
if matches!(request.version(), Version::HTTP_2 | Version::HTTP_3) {
let authority = request.uri().authority().ok_or(())?;
if request.uri().scheme_str() != Some(if tls { "https" } else { "http" }) || count > 1 {
return Err(());
}
if let Some(host) = request.headers().get(HOST) {
if !host
.to_str()
.map_err(|_| ())?
.eq_ignore_ascii_case(authority.as_str())
{
return Err(());
}
}
HeaderValue::from_str(authority.as_str()).map_err(|_| ())
} else {
if request.uri().scheme().is_some() || request.uri().authority().is_some() || count != 1 {
return Err(());
}
request.headers().get(HOST).cloned().ok_or(())
}
}