Files
RustProxy/src/proxy/forward/web_source.rs
T

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
}