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:
1 parent
d3b492874b
commit
eba9f89283
135 files changed
+1425
-1064
No files matched your search
+22
-21
@@ -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(());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user