283 lines
14 KiB
Rust
283 lines
14 KiB
Rust
{
|
|
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(value)) => value,
|
|
Ok(None) => path.as_ref().to_owned(),
|
|
Err(_) => return error(StatusCode::BAD_REQUEST,
|
|
"RustProxy: invalid Web Server rewrite path\n"),
|
|
};
|
|
#[cfg(target_os = "linux")]
|
|
let kernel_sendfile = request.extensions()
|
|
.get::<crate::server::sendfile::SendfileSlots>().cloned();
|
|
#[cfg(target_os = "linux")]
|
|
let identity_request = request.version() == http::Version::HTTP_11
|
|
&& !request.headers().contains_key("accept-encoding")
|
|
&& !request.headers().contains_key("range")
|
|
&& !request.headers().contains_key("if-range");
|
|
let cache_settings = route.frontend.as_ref()
|
|
.map(|frontend| &frontend.cache)
|
|
.unwrap_or(&snapshot.envelope.config.cache);
|
|
let transform_settings = route.frontend.as_ref().map(|frontend| &frontend.transform);
|
|
let scope = route.frontend.as_ref().map(|frontend| frontend.id.as_str()).unwrap_or("");
|
|
let enabled = cache_settings.active_for(route.cache)
|
|
&& frontend.is_none_or(|frontend| frontend.basic_auth.is_none() && frontend.tinyauth.is_none());
|
|
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
|
exchange.set_cache_enabled(enabled);
|
|
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
|
|
}
|
|
request.headers_mut().insert(HOST, original_host.clone());
|
|
let request_method = request.method().clone();
|
|
let mut request_headers = std::mem::take(request.headers_mut());
|
|
let request_uri = request.uri().clone();
|
|
let request_version = request.version();
|
|
let public_suffix = request_uri.path_and_query()
|
|
.map(|item| item.as_str()).unwrap_or("/").to_owned();
|
|
let empty_body = if enabled && (request_method == Method::GET || request_method == Method::HEAD) {
|
|
match empty_for_cache(&mut request).await {
|
|
Ok(empty) => empty,
|
|
Err(_) => return error(StatusCode::BAD_REQUEST,
|
|
"RustProxy: invalid or incomplete request body\n"),
|
|
}
|
|
} else { enabled && request.body().is_end_stream() };
|
|
let cache_plan = cache_plan::CachePlan::new(
|
|
(!enabled).then_some("cache_disabled"), cache_settings, &path,
|
|
|| route.frontend.as_ref().is_some_and(|frontend|
|
|
frontend.force_cache.matches(&path, &public_suffix)),
|
|
);
|
|
let force_cache = cache_plan.force_cache();
|
|
let ignore_cookie = cache_plan.ignore_cookie();
|
|
let policy = cache_plan.request_policy(
|
|
&request_method, &request_headers, empty_body, &path,
|
|
request_uri.query().is_some(), cache_settings,
|
|
);
|
|
let eligible = enabled && policy.bypass_reason().is_none();
|
|
let key_headers = if eligible && ignore_cookie && request_headers.contains_key("cookie") {
|
|
let mut headers = request_headers.clone();
|
|
headers.remove("cookie");
|
|
Some(headers)
|
|
} else { None };
|
|
let cache_ticket = if eligible {
|
|
state.cache.ticket_scoped_for_config(snapshot.envelope.revision,
|
|
state.cache.revision(), scope, context.scheme,
|
|
original_host.to_str().unwrap_or(""), &route.id, &public_suffix,
|
|
key_headers.as_ref().unwrap_or(&request_headers))
|
|
} else { None };
|
|
let range = cache_plan::range_selection(eligible, &request_method, &request_headers);
|
|
let if_range = if eligible { request_headers.get("if-range").cloned() } else { None };
|
|
let range_requested = !matches!(range, crate::cache::RangeSelection::None);
|
|
let cached_revalidation = if let Some(ticket) = cache_ticket.as_ref() {
|
|
state.cache.revalidation_tiered(ticket).await
|
|
} else { None };
|
|
let needs_validation = policy.revalidation_reason().is_some()
|
|
|| cached_revalidation.as_ref().is_some_and(|cached|
|
|
!cached.reusable_for_request(&request_headers));
|
|
if let Some(ticket) = &cache_ticket {
|
|
if !needs_validation {
|
|
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await {
|
|
if hit.stale {
|
|
} else {
|
|
let conditional = crate::cache::conditional_not_modified(
|
|
&request_headers, hit.response.headers());
|
|
if !conditional {
|
|
if let crate::cache::RangeSelection::Single(requested) = range {
|
|
hit = hit.apply_range(requested, if_range.as_ref());
|
|
}
|
|
}
|
|
let mut response = if conditional {
|
|
crate::cache::make_not_modified(hit.response)
|
|
} else { hit.response };
|
|
if request_method == Method::HEAD {
|
|
*response.body_mut() = Body::empty();
|
|
}
|
|
cache_settings.annotate_reason(response.headers_mut(), "HIT", hit.hits,
|
|
hit.remaining_ttl, "cache_hit");
|
|
cache_settings.annotate_transformed(response.headers_mut(), hit.transformed);
|
|
cache_settings.annotate_resident_age(response.headers_mut(), hit.resident_age_seconds);
|
|
state.cache.record(scope, &host, "HIT", "cache_hit");
|
|
if hit.transformed { state.transform_metrics.record_cache_hit(scope); }
|
|
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
|
exchange.cache_status.store(1, Ordering::Relaxed);
|
|
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = "cache_hit";
|
|
}
|
|
response.extensions_mut().insert(ResponseRulesApplied);
|
|
response.extensions_mut().insert(StaticSourceResponse);
|
|
return intercept_route_origin_error(response, route, &snapshot);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if cache_ticket.is_some() && crate::cache::only_if_cached(&request_headers) {
|
|
return error(StatusCode::GATEWAY_TIMEOUT,
|
|
"RustProxy: only-if-cached request has no usable Web Server representation\n");
|
|
}
|
|
let static_body = std::mem::replace(request.body_mut(), Body::empty());
|
|
let (origin_request_headers, revalidation_sent) = if cache_ticket.is_some() {
|
|
web_cache_revalidation::web_origin_request_headers(
|
|
&request_headers, cached_revalidation.as_ref())
|
|
} else {
|
|
(std::mem::take(&mut request_headers), false)
|
|
};
|
|
let started = Instant::now();
|
|
let response = crate::web_server::serve(service, request_method.clone(),
|
|
origin_request_headers, request_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: request_version,
|
|
}).await;
|
|
if let Some(fallback) = execute_error_fallback(
|
|
state.clone(), peer, local, &snapshot, &host, tls, route,
|
|
response.status(), &fallback_request, &fallback_body,
|
|
).await { return fallback; }
|
|
|
|
let mut response = intercept_route_origin_error(response, route, &snapshot);
|
|
if revalidation_sent && response.status() == StatusCode::NOT_MODIFIED {
|
|
if let (Some(ticket), Some(cached)) =
|
|
(cache_ticket.as_ref(), cached_revalidation.as_ref())
|
|
{
|
|
let revalidated = web_cache_revalidation::finish_web_revalidation(
|
|
&state.cache, ticket, cached, response.headers(), cache_settings,
|
|
started.elapsed(), &request_headers, &request_method,
|
|
ignore_cookie, force_cache,
|
|
|headers| apply_response_rules(headers, &snapshot,
|
|
route.frontend.as_deref(), Some(route), false),
|
|
);
|
|
if let Some(mut hit) = revalidated {
|
|
state.cache.record(scope, &host, "HIT", hit.reason);
|
|
if hit.transformed { state.transform_metrics.record_cache_hit(scope); }
|
|
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
|
exchange.cache_status.store(1, Ordering::Relaxed);
|
|
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = hit.reason;
|
|
}
|
|
hit.response.extensions_mut().insert(ResponseRulesApplied);
|
|
hit.response.extensions_mut().insert(StaticSourceResponse);
|
|
return intercept_route_origin_error(hit.response, route, &snapshot);
|
|
}
|
|
return error(StatusCode::SERVICE_UNAVAILABLE,
|
|
"RustProxy: Web Server cache entry invalidated during revalidation\n");
|
|
}
|
|
}
|
|
if cached_revalidation.is_some() && response.status() != StatusCode::NOT_MODIFIED
|
|
&& response.status().as_u16() < 500
|
|
{
|
|
if let Some(ticket) = cache_ticket.as_ref() {
|
|
state.cache.discard(ticket);
|
|
}
|
|
}
|
|
let origin_headers = response.headers();
|
|
let origin_safe = cache_ticket.is_some() && {
|
|
let explicit_origin_policy = origin_headers.contains_key("cache-control")
|
|
|| origin_headers.contains_key("expires")
|
|
|| origin_headers.contains_key("set-cookie")
|
|
|| origin_headers.contains_key("vary");
|
|
!explicit_origin_policy || crate::cache::plan(response.status(), origin_headers,
|
|
cache_settings, started.elapsed()).is_some()
|
|
};
|
|
let no_transform = crate::transform::no_transform(origin_headers);
|
|
let skip_compression = crate::compression::no_transform(origin_headers);
|
|
let origin_validators = crate::transform::OriginValidators {
|
|
etag: origin_headers.get("etag").cloned(),
|
|
last_modified: origin_headers.get("last-modified").cloned(),
|
|
};
|
|
if skip_compression || range_requested {
|
|
response.extensions_mut().insert(crate::compression::Skip);
|
|
}
|
|
response.extensions_mut().insert(origin_validators);
|
|
apply_response_rules(response.headers_mut(), &snapshot, route.frontend.as_deref(),
|
|
Some(route), false);
|
|
if response.extensions().get::<crate::pages::PageServed>().is_some() {
|
|
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
|
|
}
|
|
response.extensions_mut().insert(ResponseRulesApplied);
|
|
response.extensions_mut().insert(StaticSourceResponse);
|
|
|
|
let origin_eligible_plan = if origin_safe
|
|
&& !(ignore_cookie && crate::cache::varies_on(response.headers(), "cookie"))
|
|
{
|
|
crate::cache::plan(response.status(), response.headers(),
|
|
cache_settings, started.elapsed())
|
|
} else { None };
|
|
if request_method == Method::GET && !range_requested {
|
|
if let Some(settings) = transform_settings.filter(|settings| settings.enabled) {
|
|
let transform_start = Instant::now();
|
|
response = match crate::transform::apply(response, &public_suffix,
|
|
settings, no_transform).await {
|
|
Ok(value) => {
|
|
state.transform_metrics.record_response(scope, &value, transform_start.elapsed());
|
|
value
|
|
}
|
|
Err(cause) => {
|
|
state.transform_metrics.record_error(scope, transform_start.elapsed());
|
|
tracing::debug!(error = %cause, "Web Server transform failed");
|
|
return error(StatusCode::BAD_GATEWAY,
|
|
"RustProxy: Web Server response could not be transformed\n");
|
|
}
|
|
};
|
|
}
|
|
}
|
|
let fill_plan = if origin_eligible_plan.is_some() {
|
|
crate::cache::plan(response.status(), response.headers(),
|
|
cache_settings, started.elapsed())
|
|
} else { None };
|
|
let reason = if !enabled { "cache_disabled" }
|
|
else if let Some(reason) = policy.bypass_reason() { reason }
|
|
else if needs_validation { "request_revalidated" }
|
|
else if cache_ticket.is_some() && fill_plan.is_none() { "response_headers_policy" }
|
|
else if cache_ticket.is_some() { "cache_miss" }
|
|
else { "cache_unavailable" };
|
|
let fillable = request_method == Method::GET && !range_requested;
|
|
let outcome = if cache_ticket.is_some() { "MISS" } else { "BYPASS" };
|
|
if let (Some(ticket), Some(origin), Some(transformed)) = (
|
|
cache_ticket, origin_eligible_plan, fill_plan,
|
|
) {
|
|
if fillable {
|
|
let canonical = response.extensions().get::<crate::transform::CanonicalIdentity>().is_some();
|
|
let mut plan = if canonical { origin.without_vary("accept-encoding") }
|
|
else { origin };
|
|
if force_cache {
|
|
if request_headers.contains_key("cookie") { plan = plan.with_vary("cookie"); }
|
|
if request_headers.contains_key("authorization") { plan = plan.with_vary("authorization"); }
|
|
}
|
|
response = state.cache.wrap_with_fill(response, ticket,
|
|
plan.restrict(transformed), cache_settings, None);
|
|
}
|
|
}
|
|
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
|
exchange.cache_status.store(if outcome == "MISS" { 2 } else { 3 }, Ordering::Relaxed);
|
|
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
|
|
}
|
|
state.cache.record(scope, &host, outcome, reason);
|
|
cache_settings.annotate_reason(response.headers_mut(), outcome, 0, 0, reason);
|
|
let transformed = response.extensions().get::<crate::transform::Applied>().is_some();
|
|
cache_settings.annotate_transformed(response.headers_mut(), transformed);
|
|
#[cfg(target_os = "linux")]
|
|
if !enabled && identity_request && request_method == Method::GET
|
|
&& !transform_settings.is_some_and(|settings| settings.enabled)
|
|
&& response.status() == StatusCode::OK
|
|
&& !response.headers().contains_key("content-encoding")
|
|
&& !response.headers().contains_key("transfer-encoding")
|
|
&& !response.headers().contains_key("content-range")
|
|
{
|
|
if let (Some(slots), Some(candidate)) = (
|
|
kernel_sendfile,
|
|
response.extensions().get::<crate::server::sendfile::SendfileCandidate>().cloned(),
|
|
) {
|
|
let correct_length = response.headers().get("content-length")
|
|
.and_then(|v| v.to_str().ok())
|
|
.and_then(|v| v.parse::<u64>().ok()) == Some(candidate.length);
|
|
if correct_length {
|
|
if let Some(body) = slots.create_body(candidate, service.web_metrics.clone()) {
|
|
*response.body_mut() = Body::new(body);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
response
|
|
}
|