v0.35.1: performance, AGPL licensing and documentation overhaul

- Web Server reads static files inline; proxy middleware chain flattened
- About window with license summary and project links; AGPL-3.0-or-later
- Rewritten documentation: feature list, screenshots, performance guide
- Fix false spikes in per-Vhost request-rate charts
This commit is contained in:
gru committed 2026-11-05 18:29:56 +01:00
1 parent d3b492874b
commit eba9f89283
135 files changed
+1425 -1064

No files matched your search

+22 -21
View File
@@ -97,7 +97,7 @@ fn enforce_local_page_no_cache(response: &mut Response) {
}
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
}
fn error(status: StatusCode, detail: &'static str) -> Response {
@@ -108,12 +108,10 @@ fn error(status: StatusCode, detail: &'static str) -> Response {
);
let mut response = if let Some(html) = crate::pages::builtin_gateway_error(status) {
let mut response = (status, html).into_response();
response.headers_mut().insert(
"content-type",
response.headers_mut().insert(::http::header::CONTENT_TYPE,
HeaderValue::from_static("text/html; charset=utf-8"),
);
response.headers_mut().insert(
"x-content-type-options",
response.headers_mut().insert(::http::header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
);
response
@@ -127,9 +125,8 @@ fn error(status: StatusCode, detail: &'static str) -> Response {
};
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
response.headers_mut().insert(
"x-content-type-options",
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
response.headers_mut().insert(::http::header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
);
response.extensions_mut().insert(LocalGeneratedResponse);
@@ -145,7 +142,7 @@ fn method_policy_denial(policy: &crate::vhost::MethodPolicy, method: &Method) ->
"RustProxy: HTTP method denied by access policy\n",
);
if let Some(value) = policy.allow_header() {
response.headers_mut().insert(http::header::ALLOW, value);
response.headers_mut().insert(::http::header::ALLOW, value);
}
Some(response)
}
@@ -162,7 +159,7 @@ fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Metho
})
.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);
response.headers_mut().insert(::http::header::CONTENT_TYPE, value);
}
response
}
@@ -283,8 +280,7 @@ fn finish_headers(
.unwrap_or(&snapshot.envelope.config.diagnostic_headers)
.apply(response.headers_mut(), request_id);
if frontend.is_some_and(|f| f.auth_provider.is_some() || f.basic_auth.is_some()) {
response.headers_mut().insert(
"cache-control",
response.headers_mut().insert(::http::header::CACHE_CONTROL,
HeaderValue::from_static("private, no-store"),
);
}
@@ -330,14 +326,21 @@ fn apply_backend_diagnostic_values(
backend_header: bool,
backend_timing_header: bool,
) {
response.headers_mut().remove("x-rustproxy-backend");
response.headers_mut().remove("x-rust-proxy-backend");
response.headers_mut().remove("x-rust-proxy-backend-time");
response.headers_mut().remove("x-rustproxy-backend-time");
const BACKEND: ::http::HeaderName = ::http::HeaderName::from_static("x-rustproxy-backend");
const LEGACY_BACKEND: ::http::HeaderName = ::http::HeaderName::from_static("x-rust-proxy-backend");
const LEGACY_BACKEND_TIME: ::http::HeaderName =
::http::HeaderName::from_static("x-rust-proxy-backend-time");
const BACKEND_TIME: ::http::HeaderName = ::http::HeaderName::from_static("x-rustproxy-backend-time");
let headers = response.headers_mut();
for name in [&BACKEND, &LEGACY_BACKEND, &LEGACY_BACKEND_TIME, &BACKEND_TIME] {
if headers.contains_key(name) {
headers.remove(name);
}
}
if backend_header {
if let Some(origin) = response.extensions().get::<OriginBackend>() {
if let Ok(value) = HeaderValue::from_str(&origin.0) {
response.headers_mut().insert("x-rust-proxy-backend", value);
response.headers_mut().insert(LEGACY_BACKEND, value);
}
}
}
@@ -345,9 +348,7 @@ fn apply_backend_diagnostic_values(
if let Some(origin) = response.extensions().get::<OriginTiming>() {
let value = format!("{:.2}ms", origin.0.as_secs_f64() * 1000.0);
if let Ok(value) = HeaderValue::from_str(&value) {
response
.headers_mut()
.insert("x-rustproxy-backend-time", value);
response.headers_mut().insert(BACKEND_TIME, value);
}
}
}
@@ -416,7 +417,7 @@ async fn normalize_h3_h1_unknown_body(
if request.version() != Version::HTTP_3
|| upstream_version != Version::HTTP_11
|| method == Method::CONNECT
|| request.headers().contains_key(http::header::CONTENT_LENGTH)
|| request.headers().contains_key(::http::header::CONTENT_LENGTH)
|| request.body().size_hint().exact().is_some()
{
return Ok(());
+4 -4
View File
@@ -11,17 +11,17 @@ fn cache_http_invalidation_request(
) -> Result<crate::cache::InvalidationRequest, &'static str> {
let mut lengths = request
.headers()
.get_all(http::header::CONTENT_LENGTH)
.get_all(::http::header::CONTENT_LENGTH)
.iter();
if let Some(length) = lengths.next() {
if lengths.next().is_some() || length.as_bytes() != b"0" {
return Err("BAN/PURGE must not contain a request body");
}
}
if request.headers().contains_key("upgrade")
if request.headers().contains_key(::http::header::UPGRADE)
|| request
.headers()
.contains_key(http::header::TRANSFER_ENCODING)
.contains_key(::http::header::TRANSFER_ENCODING)
|| !request.body().is_end_stream()
{
return Err("BAN/PURGE must not contain a request body or upgrade");
@@ -207,6 +207,6 @@ async fn cache_http_invalidation(
let mut response = (code, axum::Json(serde_json::json!({"result": result}))).into_response();
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
response
}
+1 -1
View File
@@ -90,7 +90,7 @@ pub(super) fn range_selection(
) -> crate::cache::RangeSelection {
if cache_eligible {
crate::cache::request_range(method, headers)
} else if headers.contains_key("range") {
} else if headers.contains_key(::http::header::RANGE) {
crate::cache::RangeSelection::Unsupported
} else {
crate::cache::RangeSelection::None
+1 -1
View File
@@ -207,7 +207,7 @@ if only_cached {
"RustProxy: no reusable cached response\n",
);
cache_settings.annotate(response.headers_mut(), "MISS", 0, 0);
response.headers_mut().remove("cache-status");
response.headers_mut().remove(::http::header::CACHE_STATUS);
return response;
}
+2 -2
View File
@@ -85,11 +85,11 @@
);
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
if status == StatusCode::SERVICE_UNAVAILABLE {
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
}
response
}
+11 -11
View File
@@ -22,8 +22,8 @@ if let Some(spec) = &route.redirect {
};
let status = StatusCode::from_u16(spec.status).unwrap_or(StatusCode::FOUND);
let mut response = status.into_response();
response.headers_mut().insert(http::header::LOCATION, location);
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
response.headers_mut().insert(::http::header::LOCATION, location);
response.headers_mut().insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
return response;
}
if route.unavailable {
@@ -33,7 +33,7 @@ if route.unavailable {
);
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
@@ -61,7 +61,7 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
);
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
@@ -77,7 +77,7 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
);
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
@@ -253,7 +253,7 @@ if let Some(identity) = identity {
if websocket_legacy_support {
if let Some(upstream_host) = request.headers().get(HOST).cloned() {
if let Some(origin) = legacy_websocket_origin(&backend.request_origin, &upstream_host) {
request.headers_mut().insert("origin", origin);
request.headers_mut().insert(::http::header::ORIGIN, origin);
}
}
}
@@ -263,8 +263,8 @@ let effective_policy = cache_plan::combine_policy(original_policy, || {
request.uri().query().is_some(), cache_settings,
)
});
let force_vary_cookie = force_cache && request.headers().contains_key("cookie");
let force_vary_authorization = force_cache && request.headers().contains_key("authorization");
let force_vary_cookie = force_cache && request.headers().contains_key(::http::header::COOKIE);
let force_vary_authorization = force_cache && request.headers().contains_key(::http::header::AUTHORIZATION);
let effective_reason = effective_policy.bypass_reason();
let revalidation_reason = effective_policy.revalidation_reason();
let effective_cacheable = effective_reason.is_none();
@@ -272,15 +272,15 @@ let range_selection = cache_plan::range_selection(
effective_cacheable, request.method(), request.headers(),
);
let if_range = if effective_cacheable {
request.headers().get("if-range").cloned()
request.headers().get(::http::header::IF_RANGE).cloned()
} else { None };
let range_requested = !matches!(range_selection, crate::cache::RangeSelection::None);
let cache_route = route.id.as_str();
let mut cache_key_headers = None;
if effective_cacheable && ignore_static_cookie && request.headers().contains_key("cookie") {
if effective_cacheable && ignore_static_cookie && request.headers().contains_key(::http::header::COOKIE) {
let mut headers = request.headers().clone();
headers.remove("cookie");
headers.remove(::http::header::COOKIE);
cache_key_headers = Some(headers);
}
let key_headers = cache_key_headers
+5
View File
@@ -135,6 +135,11 @@ if let Some(exchange) = &exchange {
.lock()
.unwrap_or_else(|e| e.into_inner()) = reason;
}
if fill_leader.is_some() && (origin_plan.is_none() || transformed_plan.is_none()) {
if let Some(ticket) = cache_ticket.as_ref() {
state.cache.remember_uncacheable(ticket);
}
}
if let (Some(ticket), Some(plan), Some(transformed)) =
(cache_ticket, origin_plan, transformed_plan)
{
+13 -14
View File
@@ -137,8 +137,8 @@ let origin_reason = (cache_ticket.is_some() && origin_plan.is_none()).then(|| {
let origin_no_transform = crate::transform::no_transform(response.headers());
let skip_compression = crate::compression::no_transform(response.headers());
let origin_validators = crate::transform::OriginValidators {
etag: response.headers().get("etag").cloned(),
last_modified: response.headers().get("last-modified").cloned(),
etag: response.headers().get(::http::header::ETAG).cloned(),
last_modified: response.headers().get(::http::header::LAST_MODIFIED).cloned(),
};
let (mut parts, body) = response.into_parts();
parts.extensions.insert(origin_validators);
@@ -147,8 +147,8 @@ if skip_compression || range_requested {
}
headers::strip_hop_by_hop(&mut parts.headers);
if websocket_legacy_support && handshake.is_some() {
parts.headers.remove("content-length");
parts.headers.remove("transfer-encoding");
parts.headers.remove(::http::header::CONTENT_LENGTH);
parts.headers.remove(::http::header::TRANSFER_ENCODING);
}
apply_response_rules(
&mut parts.headers,
@@ -161,7 +161,7 @@ if let Some(pool) = &route.pool {
pool.strip_affinity_response(&mut parts.headers);
}
if let Some(cookie) = active_affinity_cookie {
parts.headers.append("set-cookie", cookie);
parts.headers.append(::http::header::SET_COOKIE, cookie);
}
let h3_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h3())
&& upgraded.is_some();
@@ -208,9 +208,9 @@ let mut response = if h3_tunnel {
};
exchange.websocket_phase("tunnel_ready");
parts.status = StatusCode::OK;
parts.headers.remove("sec-websocket-accept");
parts.headers.remove("content-length");
parts.headers.remove("transfer-encoding");
parts.headers.remove(::http::header::SEC_WEBSOCKET_ACCEPT);
parts.headers.remove(::http::header::CONTENT_LENGTH);
parts.headers.remove(::http::header::TRANSFER_ENCODING);
parts.extensions.insert(crate::compression::Skip);
parts
.extensions
@@ -301,8 +301,8 @@ let mut response = if h3_tunnel {
"RustProxy: HTTP/2 WebSocket tunnel state unavailable\n",
);
};
parts.headers.remove("content-length");
parts.headers.remove("transfer-encoding");
parts.headers.remove(::http::header::CONTENT_LENGTH);
parts.headers.remove(::http::header::TRANSFER_ENCODING);
parts.extensions.insert(crate::compression::Skip);
Response::from_parts(
parts,
@@ -314,10 +314,10 @@ let mut response = if h3_tunnel {
{
parts
.headers
.insert("connection", HeaderValue::from_static("Upgrade"));
.insert(::http::header::CONNECTION, HeaderValue::from_static("Upgrade"));
parts
.headers
.insert("upgrade", HeaderValue::from_static("websocket"));
.insert(::http::header::UPGRADE, HeaderValue::from_static("websocket"));
websocket::spawn(
state.clone(),
downstream,
@@ -342,8 +342,7 @@ if intercept_origin_error {
Some(route),
admin_control_plane,
);
response.headers_mut().insert(
"cache-control",
response.headers_mut().insert(::http::header::CACHE_CONTROL,
HeaderValue::from_static("no-store"),
);
}
+17 -16
View File
@@ -4,27 +4,27 @@ let websocket_upstream_h2 = !websocket_legacy_support
&& matches!(backend.protocol,
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2);
if let Some(handshake) = &handshake {
request.headers_mut().remove("content-length");
request.headers_mut().remove("transfer-encoding");
request.headers_mut().remove(::http::header::CONTENT_LENGTH);
request.headers_mut().remove(::http::header::TRANSFER_ENCODING);
if websocket_upstream_h2 {
request.headers_mut().remove("connection");
request.headers_mut().remove("upgrade");
request.headers_mut().remove("sec-websocket-key");
request.headers_mut().remove("sec-websocket-accept");
request.headers_mut().remove(::http::header::CONNECTION);
request.headers_mut().remove(::http::header::UPGRADE);
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_KEY);
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_ACCEPT);
} else {
request
.headers_mut()
.insert("connection", HeaderValue::from_static("Upgrade"));
.insert(::http::header::CONNECTION, HeaderValue::from_static("Upgrade"));
request
.headers_mut()
.insert("upgrade", HeaderValue::from_static("websocket"));
.insert(::http::header::UPGRADE, HeaderValue::from_static("websocket"));
if !handshake.h1() {
*request.method_mut() = Method::GET;
request.headers_mut().remove("sec-websocket-key");
request.headers_mut().remove("sec-websocket-accept");
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_KEY);
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_ACCEPT);
if let Some(key) = handshake.upstream_key() {
if let Ok(key) = HeaderValue::from_str(key) {
request.headers_mut().insert("sec-websocket-key", key);
request.headers_mut().insert(::http::header::SEC_WEBSOCKET_KEY, key);
} else {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -39,7 +39,7 @@ if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
auth.apply_upstream(request.headers_mut(), basic_identity);
}
if range_requested || transform_settings.is_some_and(|settings| settings.enabled) {
request.headers_mut().remove("accept-encoding");
request.headers_mut().remove(::http::header::ACCEPT_ENCODING);
}
let mut revalidation_sent = false;
if pending_revalidation {
@@ -61,10 +61,11 @@ let pool_retry_possible = route.backend_id.is_none()
let h2_replay_possible = !admin_control_plane
&& handshake.is_none()
&& retry_safe_method(&retry_method)
&& matches!(backend.protocol,
crate::config::UpstreamProtocol::Http2
| crate::config::UpstreamProtocol::H2
| crate::config::UpstreamProtocol::Auto);
&& match backend.protocol {
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2 => true,
crate::config::UpstreamProtocol::Auto => !backend.origin.starts_with("http://"),
_ => false,
};
let replay_headers = ((pool_retry_possible || h2_replay_possible)
&& (request.body().size_hint().exact() == Some(0)
|| request.version() == Version::HTTP_3))
+19 -19
View File
@@ -14,9 +14,9 @@
.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");
&& !request.headers().contains_key(::http::header::ACCEPT_ENCODING)
&& !request.headers().contains_key(::http::header::RANGE)
&& !request.headers().contains_key(::http::header::IF_RANGE);
let cache_settings = route.frontend.as_ref()
.map(|frontend| &frontend.cache)
.unwrap_or(&snapshot.envelope.config.cache);
@@ -54,9 +54,9 @@
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 key_headers = if eligible && ignore_cookie && request_headers.contains_key(::http::header::COOKIE) {
let mut headers = request_headers.clone();
headers.remove("cookie");
headers.remove(::http::header::COOKIE);
Some(headers)
} else { None };
let cache_ticket = if eligible {
@@ -67,7 +67,7 @@
.map(|ticket| ticket.with_varnish_compatible(cache_settings.varnish_compatible))
} 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 if_range = if eligible { request_headers.get(::http::header::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
@@ -181,18 +181,18 @@
}
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");
let explicit_origin_policy = origin_headers.contains_key(::http::header::CACHE_CONTROL)
|| origin_headers.contains_key(::http::header::EXPIRES)
|| origin_headers.contains_key(::http::header::SET_COOKIE)
|| origin_headers.contains_key(::http::header::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(),
etag: origin_headers.get(::http::header::ETAG).cloned(),
last_modified: origin_headers.get(::http::header::LAST_MODIFIED).cloned(),
};
if skip_compression || range_requested {
response.extensions_mut().insert(crate::compression::Skip);
@@ -201,7 +201,7 @@
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.headers_mut().insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
}
response.extensions_mut().insert(ResponseRulesApplied);
response.extensions_mut().insert(StaticSourceResponse);
@@ -250,8 +250,8 @@
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"); }
if request_headers.contains_key(::http::header::COOKIE) { plan = plan.with_vary("cookie"); }
if request_headers.contains_key(::http::header::AUTHORIZATION) { plan = plan.with_vary("authorization"); }
}
response = state.cache.wrap_with_fill(response, ticket,
plan.restrict(transformed), cache_settings, None);
@@ -278,15 +278,15 @@
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")
&& !response.headers().contains_key(::http::header::CONTENT_ENCODING)
&& !response.headers().contains_key(::http::header::TRANSFER_ENCODING)
&& !response.headers().contains_key(::http::header::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")
let correct_length = response.headers().get(::http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok()) == Some(candidate.length);
if correct_length {
+7 -7
View File
@@ -1,7 +1,7 @@
async fn forward(
State(state): State<Arc<App>>,
Extension(peer): Extension<PeerAddr>,
Extension(local): Extension<LocalAddr>,
state: Arc<App>,
peer: PeerAddr,
local: LocalAddr,
mut request: Request,
) -> Response {
let cookies = crate::auth_provider::CookieSink::default();
@@ -279,7 +279,7 @@ async fn forward_request(
country.as_deref(),
request
.headers()
.get(http::header::USER_AGENT)
.get(::http::header::USER_AGENT)
.and_then(|value| value.to_str().ok()),
&snapshot.global_security.rate_limit,
) {
@@ -288,7 +288,7 @@ async fn forward_request(
"RustProxy: global request rate limit exceeded\n",
);
if let Ok(value) = HeaderValue::from_str(&limited.retry_after_seconds.to_string()) {
response.headers_mut().insert("retry-after", value);
response.headers_mut().insert(::http::header::RETRY_AFTER, value);
}
return response;
}
@@ -335,7 +335,7 @@ async fn forward_request(
country.as_deref(),
request
.headers()
.get(http::header::USER_AGENT)
.get(::http::header::USER_AGENT)
.and_then(|value| value.to_str().ok()),
&frontend.rate_limit,
) {
@@ -344,7 +344,7 @@ async fn forward_request(
"RustProxy: request rate limit exceeded\n",
);
if let Ok(value) = HeaderValue::from_str(&limited.retry_after_seconds.to_string()) {
response.headers_mut().insert("retry-after", value);
response.headers_mut().insert(::http::header::RETRY_AFTER, value);
}
return response;
}
+23 -21
View File
@@ -27,22 +27,24 @@ pub fn strip_hop_by_hop(headers: &mut HeaderMap) {
headers.remove(name);
}
}
const KEEP_ALIVE: HeaderName = HeaderName::from_static("keep-alive");
const PROXY_CONNECTION: HeaderName = HeaderName::from_static("proxy-connection");
for name in [
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
"trailer",
"te",
"proxy-authenticate",
"proxy-authorization",
CONNECTION,
KEEP_ALIVE,
PROXY_CONNECTION,
http::header::TRANSFER_ENCODING,
http::header::UPGRADE,
http::header::TRAILER,
http::header::TE,
http::header::PROXY_AUTHENTICATE,
http::header::PROXY_AUTHORIZATION,
] {
headers.remove(name);
}
}
pub fn requests_upgrade(headers: &HeaderMap) -> bool {
if headers.contains_key("upgrade") {
if headers.contains_key(::http::header::UPGRADE) {
return true;
}
let mut connection = headers.get_all(CONNECTION).iter();
@@ -67,13 +69,13 @@ pub fn requests_upgrade(headers: &HeaderMap) -> bool {
include!("tests.rs");
pub fn strip_admin_cookie(headers: &mut HeaderMap) -> Vec<String> {
if !headers.contains_key(http::header::COOKIE) {
if !headers.contains_key(::http::header::COOKIE) {
return Vec::new();
}
let mut retained = Vec::new();
let mut admin = Vec::new();
for part in headers
.get_all(http::header::COOKIE)
.get_all(::http::header::COOKIE)
.iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|h| h.split(';'))
@@ -88,10 +90,10 @@ pub fn strip_admin_cookie(headers: &mut HeaderMap) -> Vec<String> {
retained.push(part.to_owned());
}
}
headers.remove(http::header::COOKIE);
headers.remove(::http::header::COOKIE);
if !retained.is_empty() {
if let Ok(value) = retained.join("; ").parse() {
headers.insert(http::header::COOKIE, value);
headers.insert(::http::header::COOKIE, value);
}
}
admin
@@ -102,22 +104,22 @@ pub fn restore_admin_cookie(headers: &mut HeaderMap, admin: &[String]) {
return;
}
let mut values: Vec<String> = headers
.get_all(http::header::COOKIE)
.get_all(::http::header::COOKIE)
.iter()
.filter_map(|h| h.to_str().ok().map(str::to_owned))
.collect();
values.extend(admin.iter().cloned());
headers.remove(http::header::COOKIE);
headers.remove(::http::header::COOKIE);
if let Ok(value) = values.join("; ").parse() {
headers.insert(http::header::COOKIE, value);
headers.insert(::http::header::COOKIE, value);
}
}
pub fn strip_admin_set_cookie(headers: &mut HeaderMap) {
if !headers.contains_key(http::header::SET_COOKIE) {
if !headers.contains_key(::http::header::SET_COOKIE) {
return;
}
let retained: Vec<_> = headers
.get_all(http::header::SET_COOKIE)
.get_all(::http::header::SET_COOKIE)
.iter()
.filter(|h| {
h.to_str()
@@ -127,8 +129,8 @@ pub fn strip_admin_set_cookie(headers: &mut HeaderMap) {
})
.cloned()
.collect();
headers.remove(http::header::SET_COOKIE);
headers.remove(::http::header::SET_COOKIE);
for value in retained {
headers.append(http::header::SET_COOKIE, value);
headers.append(::http::header::SET_COOKIE, value);
}
}
+3 -3
View File
@@ -46,7 +46,7 @@ fn canonical_host_redirect(
);
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
Some(response)
}
@@ -62,9 +62,9 @@ fn force_ssl_redirect(
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
response
.headers_mut()
.insert(http::header::LOCATION, location);
.insert(::http::header::LOCATION, location);
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
Some(response)
}
+21 -13
View File
@@ -9,9 +9,8 @@ use crate::{
use axum::{
body::Body,
extract::{Request, State},
middleware::{self, Next},
response::{IntoResponse, Response},
Extension, Router,
Router,
};
use body::{Direction, Exchange, ExchangeSource, MeteredBody};
use bytes::Bytes;
@@ -49,14 +48,23 @@ use std::{
};
pub fn router(state: Arc<App>) -> Router {
Router::new()
.fallback(forward)
.layer(middleware::from_fn_with_state(state.clone(), observe))
.layer(middleware::from_fn_with_state(
state.clone(),
crate::acme::challenge,
))
.with_state(state)
Router::new().fallback(entry).with_state(state)
}
async fn entry(State(state): State<Arc<App>>, request: Request) -> Response {
if crate::acme::is_challenge_path(request.uri().path()) {
return crate::acme::challenge(state, request).await;
}
observe(state, request).await
}
async fn dispatch(state: Arc<App>, request: Request) -> Response {
let peer = request.extensions().get::<PeerAddr>().copied();
let local = request.extensions().get::<LocalAddr>().copied();
let (Some(peer), Some(local)) = (peer, local) else {
return (StatusCode::INTERNAL_SERVER_ERROR, "Missing request extension").into_response();
};
forward(state, peer, local, request).await
}
const MAX_UPSTREAM_ERROR_DETAIL: usize = 2048;
@@ -320,7 +328,7 @@ async fn refresh_cache_once(
.as_ref()
.is_some_and(|settings| settings.enabled)
{
refresh_headers.remove("accept-encoding");
refresh_headers.remove(::http::header::ACCEPT_ENCODING);
}
let mut refresh = Request::new(Body::empty());
*refresh.method_mut() = Method::GET;
@@ -382,8 +390,8 @@ async fn refresh_cache_once(
let origin_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(),
etag: origin.headers().get(::http::header::ETAG).cloned(),
last_modified: origin.headers().get(::http::header::LAST_MODIFIED).cloned(),
};
let (mut parts, body) = origin.into_parts();
parts.extensions.insert(origin_validators);
+12 -8
View File
@@ -41,7 +41,7 @@ fn should_replace_error_page(response: &Response, minimal_welcome: bool) -> bool
.is_none()
}
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
async fn observe(state: Arc<App>, mut request: Request) -> Response {
let snapshot = state.config.load_full();
let selected = response_context(&snapshot, &request);
let frontend_context = selected.frontend.clone();
@@ -65,7 +65,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
let is_head = method == Method::HEAD;
let ws_attempt = request
.headers()
.get("upgrade")
.get(::http::header::UPGRADE)
.is_some_and(|v| v.as_bytes().eq_ignore_ascii_case(b"websocket"))
|| (request.version() == Version::HTTP_2
&& request.method() == Method::CONNECT
@@ -148,10 +148,14 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
.unwrap_or_else(|| request.uri().authority().map(|v| v.as_str()).unwrap_or("")),
253,
);
let initial_client_ip = request
.extensions()
.get::<PeerAddr>()
.map(|peer| peer.0.ip().to_string());
let initial_client_ip = request.extensions().get::<PeerAddr>().map(|peer| {
request
.extensions()
.get::<Arc<crate::forwarding::PeerFormatting>>()
.and_then(|formatting| formatting.raw_peer_text())
.map(str::to_owned)
.unwrap_or_else(|| peer.0.ip().to_string())
});
let user_agent = state
.logs
.capture_user_agent()
@@ -221,7 +225,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
.and_then(|frontend| frontend.proxy_timeouts.send_timeout())
.unwrap_or(state.body_idle_timeout);
let request = request.map(|body| wrap_observed_request(body, &exchange, request_send_timeout));
let mut response = next.run(request).await;
let mut response = dispatch(state.clone(), request).await;
let final_route_context = response
.extensions()
.get::<FinalRouteContext>()
@@ -277,7 +281,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
}
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
} else {
if response
.extensions()
+10 -7
View File
@@ -17,8 +17,8 @@ pub(super) fn web_origin_request_headers(
let Some(cached) = cached else {
return (headers, false);
};
headers.remove("if-none-match");
headers.remove("if-modified-since");
headers.remove(::http::header::IF_NONE_MATCH);
headers.remove(::http::header::IF_MODIFIED_SINCE);
let sent = cached.apply_validators(&mut headers);
(headers, sent)
}
@@ -62,17 +62,20 @@ pub(super) fn finish_web_revalidation(
let mut merged = origin_merged;
apply_rules(&mut merged);
if force_cache {
if client.contains_key("cookie") {
merged.append("vary", HeaderValue::from_static("Cookie"));
if client.contains_key(::http::header::COOKIE) {
merged.append(::http::header::VARY, HeaderValue::from_static("Cookie"));
}
if client.contains_key("authorization") {
merged.append("vary", HeaderValue::from_static("Authorization"));
if client.contains_key(::http::header::AUTHORIZATION) {
merged.append(
::http::header::VARY,
HeaderValue::from_static("Authorization"),
);
}
}
let can_store = origin_safe && !(ignore_cookie && crate::cache::varies_on(&merged, "cookie"));
let client_not_modified = crate::cache::conditional_not_modified(client, &merged);
let range = crate::cache::request_range(method, client);
let if_range = client.get("if-range");
let if_range = client.get(::http::header::IF_RANGE);
let published = if can_store {
cache.publish_revalidated(
ticket.clone(),
+13 -13
View File
@@ -81,7 +81,7 @@ impl Handshake {
fn protocols(headers: &HeaderMap) -> Result<Vec<String>> {
let protocols = headers
.get_all("sec-websocket-protocol")
.get_all(::http::header::SEC_WEBSOCKET_PROTOCOL)
.iter()
.map(|v| v.to_str())
.collect::<std::result::Result<Vec<_>, _>>()?
@@ -123,18 +123,18 @@ pub fn validate(request: &Request) -> Result<Handshake> {
"invalid WebSocket key"
);
ensure!(
!request.headers().contains_key("transfer-encoding"),
!request.headers().contains_key(::http::header::TRANSFER_ENCODING),
"WebSocket handshake cannot contain a transfer body"
);
ensure!(
request.headers().get_all("content-length").iter().count() <= 1
request.headers().get_all(::http::header::CONTENT_LENGTH).iter().count() <= 1
&& request
.headers()
.get("content-length")
.get(::http::header::CONTENT_LENGTH)
.is_none_or(|v| v == "0"),
"WebSocket handshake cannot contain a body"
);
for value in request.headers().get_all("connection").iter() {
for value in request.headers().get_all(::http::header::CONNECTION).iter() {
for field in value.to_str()?.split(',') {
ensure!(
!field
@@ -256,8 +256,8 @@ pub fn validate_response(
has_token(headers, "upgrade", "websocket"),
"invalid legacy upstream upgrade"
);
if headers.contains_key("content-length") {
for value in headers.get_all("content-length").iter() {
if headers.contains_key(::http::header::CONTENT_LENGTH) {
for value in headers.get_all(::http::header::CONTENT_LENGTH).iter() {
ensure!(
value.to_str()?.trim() == "0",
"legacy 101 response declared a non-zero body"
@@ -271,17 +271,17 @@ pub fn validate_response(
"invalid upstream upgrade"
);
ensure!(
!headers.contains_key("transfer-encoding"),
!headers.contains_key(::http::header::TRANSFER_ENCODING),
"101 response must not use Transfer-Encoding"
);
ensure!(
headers.get_all("content-length").iter().count() <= 1
headers.get_all(::http::header::CONTENT_LENGTH).iter().count() <= 1
&& headers
.get("content-length")
.get(::http::header::CONTENT_LENGTH)
.is_none_or(|value| value.to_str().is_ok_and(|value| value.trim() == "0")),
"101 response Content-Length must be 0 when present"
);
for value in headers.get_all("connection").iter() {
for value in headers.get_all(::http::header::CONNECTION).iter() {
for field in value.to_str()?.split(',') {
ensure!(
!field
@@ -297,7 +297,7 @@ pub fn validate_response(
single(headers, "sec-websocket-accept")? == expected.accept,
"upstream WebSocket accept mismatch"
);
if headers.contains_key("sec-websocket-protocol") {
if headers.contains_key(::http::header::SEC_WEBSOCKET_PROTOCOL) {
let selected = single(headers, "sec-websocket-protocol")?;
ensure!(
expected.protocols.iter().any(|v| v == selected),
@@ -321,7 +321,7 @@ pub fn validate_h2_response(headers: &HeaderMap, expected: &Handshake) -> Result
"HTTP/1.1-only WebSocket field on HTTP/2 response"
);
}
if headers.contains_key("sec-websocket-protocol") {
if headers.contains_key(::http::header::SEC_WEBSOCKET_PROTOCOL) {
let selected = single(headers, "sec-websocket-protocol")?;
ensure!(
expected.protocols.iter().any(|value| value == selected),