v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,380 @@
|
||||
{
|
||||
let origin_validation_headers = response.headers().clone();
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange
|
||||
.upstream_status
|
||||
.store(response.status().as_u16(), Ordering::Relaxed);
|
||||
}
|
||||
if let Some(lease) = &active_lease {
|
||||
lease.response(
|
||||
response.status().as_u16(),
|
||||
final_attempt_elapsed,
|
||||
request_body_empty,
|
||||
);
|
||||
if retries > 0 && response.status().as_u16() < 500 {
|
||||
lease.retry_succeeded();
|
||||
}
|
||||
}
|
||||
if cache_settings.allows_stale_error_status(response.status()) {
|
||||
if let Some(stale) = serve_stale(
|
||||
"cache_stale_if_error",
|
||||
upstream_started.elapsed(),
|
||||
&active_backend.id,
|
||||
) {
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
let h2_direct_tunnel = websocket_upstream_h2 && response.status().is_success();
|
||||
if websocket_upstream_h2 && response.status() == StatusCode::SWITCHING_PROTOCOLS {
|
||||
if let Some(session) = h2_direct_session.take() {
|
||||
session.cancellation().cancel();
|
||||
}
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_rejected", "unexpected_h2_101");
|
||||
}
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: HTTP/2 WebSocket upstream returned HTTP/1.1 switching protocols\n",
|
||||
);
|
||||
}
|
||||
if h2_direct_tunnel {
|
||||
let Some(expected) = &handshake else {
|
||||
unreachable!("direct H2 WebSocket requires a handshake")
|
||||
};
|
||||
if websocket::validate_h2_response(response.headers(), expected).is_err() {
|
||||
if let Some(session) = h2_direct_session.take() {
|
||||
session.cancellation().cancel();
|
||||
}
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_rejected", "invalid_h2_handshake");
|
||||
}
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: invalid upstream HTTP/2 WebSocket handshake\n",
|
||||
);
|
||||
}
|
||||
if let Some(session) = h2_direct_session.as_ref() {
|
||||
session.establish();
|
||||
}
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_phase("upstream_2xx_received");
|
||||
}
|
||||
} else if websocket_upstream_h2 {
|
||||
if let Some(session) = h2_direct_session.take() {
|
||||
session.cancellation().cancel();
|
||||
}
|
||||
}
|
||||
let mut upgraded = if response.status() == StatusCode::SWITCHING_PROTOCOLS {
|
||||
let Some(expected) = &handshake else {
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: unsolicited upstream upgrade\n",
|
||||
);
|
||||
};
|
||||
if websocket::validate_response(
|
||||
response.headers(),
|
||||
expected,
|
||||
websocket_legacy_support,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_rejected", "invalid_101_handshake");
|
||||
}
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: invalid upstream WebSocket handshake\n",
|
||||
);
|
||||
}
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_phase("upstream_101_received");
|
||||
}
|
||||
Some(hyper::upgrade::on(&mut response))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if handshake.as_ref().is_some_and(|handshake| {
|
||||
handshake.h3() || (handshake.h2() && !websocket_upstream_h2)
|
||||
})
|
||||
&& response.status().is_success()
|
||||
&& response.status() != StatusCode::SWITCHING_PROTOCOLS
|
||||
{
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_error("upstream_rejected", "upstream_2xx_without_101");
|
||||
}
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: HTTP/1.1 WebSocket upstream returned 2xx without switching protocols\n",
|
||||
);
|
||||
}
|
||||
// Varnish built-in behavior treats non-GET/HEAD methods as pass: the
|
||||
// response is never stored, but existing cached GET representations are
|
||||
// not purged implicitly. Explicit cache purge remains an admin action.
|
||||
let origin_plan = if cache_ticket.is_some()
|
||||
&& !(ignore_static_cookie && crate::cache::varies_on(response.headers(), "cookie"))
|
||||
{
|
||||
crate::cache::plan(
|
||||
response.status(),
|
||||
response.headers(),
|
||||
cache_settings,
|
||||
upstream_started.elapsed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let origin_reason = (cache_ticket.is_some() && origin_plan.is_none()).then(|| {
|
||||
if ignore_static_cookie && crate::cache::varies_on(response.headers(), "cookie") {
|
||||
"response_vary_cookie"
|
||||
} else {
|
||||
crate::cache::response_reason(response.status(), response.headers())
|
||||
}
|
||||
});
|
||||
let origin_no_transform = crate::transform::no_transform(response.headers());
|
||||
let skip_compression = crate::compression::private_or_no_transform(response.headers());
|
||||
let origin_validators = crate::transform::OriginValidators {
|
||||
etag: response.headers().get("etag").cloned(),
|
||||
last_modified: response.headers().get("last-modified").cloned(),
|
||||
};
|
||||
let (mut parts, body) = response.into_parts();
|
||||
parts.extensions.insert(origin_validators);
|
||||
if skip_compression || range_requested {
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
}
|
||||
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");
|
||||
}
|
||||
apply_response_rules(
|
||||
&mut parts.headers,
|
||||
&snapshot,
|
||||
route.frontend.as_deref(),
|
||||
Some(route),
|
||||
admin_control_plane,
|
||||
);
|
||||
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);
|
||||
}
|
||||
let h3_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h3())
|
||||
&& upgraded.is_some();
|
||||
let h2_bridge_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h2())
|
||||
&& !websocket_upstream_h2
|
||||
&& upgraded.is_some();
|
||||
let mut response = if h3_tunnel {
|
||||
let Some(upstream) = upgraded.take() else {
|
||||
unreachable!("checked above")
|
||||
};
|
||||
let Some(exchange) = exchange.clone() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: WebSocket exchange state unavailable\n",
|
||||
);
|
||||
};
|
||||
// Hyper cannot complete the client-side upgrade while the 101 body
|
||||
// remains owned here. Drop it, then prove that OnUpgrade is ready
|
||||
// BEFORE returning a successful RFC 9220 response to the H3 layer.
|
||||
drop(body);
|
||||
let stop_token = stop
|
||||
.as_ref()
|
||||
.map(|value| value.0.clone())
|
||||
.unwrap_or_else(|| state.shutdown.clone());
|
||||
let upstream = tokio::select! {
|
||||
_ = stop_token.cancelled() => {
|
||||
exchange.failed.store(true, Ordering::Relaxed);
|
||||
exchange.websocket_error("upgrade_cancelled", "shutdown");
|
||||
state.metrics.websocket_errors.fetch_add(1, Ordering::Relaxed);
|
||||
return error(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"RustProxy: WebSocket upgrade cancelled\n",
|
||||
);
|
||||
}
|
||||
result = tokio::time::timeout(Duration::from_secs(10), upstream) => match result {
|
||||
Ok(Ok(upstream)) => upstream,
|
||||
_ => {
|
||||
exchange.failed.store(true, Ordering::Relaxed);
|
||||
exchange.websocket_error("upgrade_failed", "backend_on_upgrade");
|
||||
state.metrics.websocket_errors.fetch_add(1, Ordering::Relaxed);
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: upstream WebSocket upgrade failed\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
exchange.websocket_phase("tunnel_ready");
|
||||
// RFC 8441/9220 uses a successful 2xx response, never 101, and
|
||||
// Sec-WebSocket-Accept is not part of the downstream handshake.
|
||||
parts.status = StatusCode::OK;
|
||||
parts.headers.remove("sec-websocket-accept");
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove("transfer-encoding");
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
parts
|
||||
.extensions
|
||||
.insert(websocket::H3Tunnel::new(
|
||||
upstream,
|
||||
exchange,
|
||||
active_backend.origin.clone(),
|
||||
lease,
|
||||
stop,
|
||||
));
|
||||
Response::from_parts(parts, Body::empty())
|
||||
} else if h2_bridge_tunnel {
|
||||
let Some(upstream) = upgraded.take() else {
|
||||
unreachable!("checked above")
|
||||
};
|
||||
let Some(exchange) = exchange.clone() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: WebSocket exchange state unavailable\n",
|
||||
);
|
||||
};
|
||||
let Some(downstream_body) = h2_downstream.take() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: HTTP/2 WebSocket request stream unavailable\n",
|
||||
);
|
||||
};
|
||||
drop(body);
|
||||
let stop_token = stop
|
||||
.as_ref()
|
||||
.map(|value| value.0.clone())
|
||||
.unwrap_or_else(|| state.shutdown.clone());
|
||||
let upstream = tokio::select! {
|
||||
_ = stop_token.cancelled() => {
|
||||
exchange.failed.store(true, Ordering::Relaxed);
|
||||
exchange.websocket_error("upgrade_cancelled", "shutdown");
|
||||
state.metrics.websocket_errors.fetch_add(1, Ordering::Relaxed);
|
||||
return error(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"RustProxy: WebSocket upgrade cancelled\n",
|
||||
);
|
||||
}
|
||||
result = tokio::time::timeout(Duration::from_secs(10), upstream) => match result {
|
||||
Ok(Ok(upstream)) => upstream,
|
||||
_ => {
|
||||
exchange.failed.store(true, Ordering::Relaxed);
|
||||
exchange.websocket_error("upgrade_failed", "backend_on_upgrade");
|
||||
state.metrics.websocket_errors.fetch_add(1, Ordering::Relaxed);
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: upstream WebSocket upgrade failed\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
exchange.websocket_phase("tunnel_ready");
|
||||
parts.status = StatusCode::OK;
|
||||
for name in [
|
||||
"connection",
|
||||
"upgrade",
|
||||
"sec-websocket-accept",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
] {
|
||||
parts.headers.remove(name);
|
||||
}
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
let body = websocket::bridge_h2_to_h1(
|
||||
state.clone(),
|
||||
downstream_body,
|
||||
upstream,
|
||||
exchange,
|
||||
active_backend.origin.clone(),
|
||||
lease,
|
||||
stop,
|
||||
);
|
||||
Response::from_parts(parts, body)
|
||||
} else if h2_direct_tunnel {
|
||||
let Some(exchange) = exchange.clone() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: WebSocket exchange state unavailable\n",
|
||||
);
|
||||
};
|
||||
let Some(session) = h2_direct_session.take() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: HTTP/2 WebSocket tunnel state unavailable\n",
|
||||
);
|
||||
};
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove("transfer-encoding");
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
Response::from_parts(
|
||||
parts,
|
||||
websocket::wrap_h2_response_body(body, session, exchange),
|
||||
)
|
||||
} else {
|
||||
if let (Some(upstream), Some(downstream), Some(exchange)) =
|
||||
(upgraded.take(), downstream, exchange.clone())
|
||||
{
|
||||
parts
|
||||
.headers
|
||||
.insert("connection", HeaderValue::from_static("Upgrade"));
|
||||
parts
|
||||
.headers
|
||||
.insert("upgrade", HeaderValue::from_static("websocket"));
|
||||
websocket::spawn(
|
||||
state.clone(),
|
||||
downstream,
|
||||
upstream,
|
||||
exchange,
|
||||
active_backend.origin.clone(),
|
||||
lease,
|
||||
stop,
|
||||
);
|
||||
}
|
||||
Response::from_parts(parts, Body::new(body))
|
||||
};
|
||||
response.extensions_mut().insert(ResponseRulesApplied);
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(OriginTiming(upstream_started.elapsed()));
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(OriginBackend(active_backend.id.clone()));
|
||||
if !range_requested && handshake.is_none() {
|
||||
if let Some(settings) = transform_settings.filter(|settings| settings.enabled) {
|
||||
let transform_started = Instant::now();
|
||||
response = match crate::transform::apply(
|
||||
response,
|
||||
&public_suffix,
|
||||
settings,
|
||||
origin_no_transform,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
state.transform_metrics.record_response(
|
||||
scope,
|
||||
&response,
|
||||
transform_started.elapsed(),
|
||||
);
|
||||
response
|
||||
}
|
||||
Err(cause) => {
|
||||
state
|
||||
.transform_metrics
|
||||
.record_error(scope, transform_started.elapsed());
|
||||
state.cache.record_reason(scope, &host, "transform_error");
|
||||
if let Some(exchange) = &exchange {
|
||||
*exchange
|
||||
.cache_reason
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = "transform_error";
|
||||
}
|
||||
tracing::debug!(error = %cause, "static response transformation failed");
|
||||
return error(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"RustProxy: upstream response body could not be transformed\n",
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
(origin_validation_headers, origin_plan, origin_reason, response)
|
||||
}
|
||||
Reference in new issue
Block a user