v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user