This commit is contained in:
Alexey
2026-08-23 10:56:00 +03:00
parent 596149cab8
commit 840b6f563a
31 changed files with 232 additions and 346 deletions
+1 -4
View File
@@ -37,10 +37,7 @@ pub(super) struct ActivityBody {
impl ActivityBody {
/// Binds one response body to its request activity guard.
pub(super) fn new(inner: HttpBody, activity: RequestActivity) -> Self {
Self {
inner,
activity,
}
Self { inner, activity }
}
}
+3 -9
View File
@@ -2,8 +2,8 @@ use std::time::Duration;
use bytes::Bytes;
use http_body_util::{BodyExt, Empty, Limited};
use hyper::body::{Body as _, Incoming};
use hyper::Request;
use hyper::body::{Body as _, Incoming};
use crate::web::manager::WebProcessRuntime;
@@ -50,14 +50,8 @@ pub(super) async fn collect_body(
let Some((reader_budget, body_budget)) = runtime.try_body_budget(limit) else {
return Err(CollectBodyError::Limit);
};
let body_timeout = Duration::from_secs(
runtime
.active_generation()
.config()
.web
.timeouts
.body_secs,
);
let body_timeout =
Duration::from_secs(runtime.active_generation().config().web.timeouts.body_secs);
let body = match tokio::time::timeout(body_timeout, Limited::new(body, limit).collect()).await {
Ok(Ok(body)) => body.to_bytes(),
_ => {
+19 -22
View File
@@ -11,8 +11,7 @@ use hyper_util::rt::TokioIo;
use tokio::net::TcpStream;
use super::{
BoxError, HttpBody, HttpResponse, bad_gateway, full_response, generic_not_found,
insert_header,
BoxError, HttpBody, HttpResponse, bad_gateway, full_response, generic_not_found, insert_header,
};
use crate::config::{WebRuntimeDecoy, WebRuntimeVhost};
use crate::web::manager::WebProcessRuntime;
@@ -128,14 +127,13 @@ fn static_entry<B>(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
);
response.headers_mut().insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
response
.headers_mut()
.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
response
}
fn resolve_static_path<'a>(
path: &str,
site: &'a crate::config::WebStaticSite,
) -> Option<&'a str> {
fn resolve_static_path<'a>(path: &str, site: &'a crate::config::WebStaticSite) -> Option<&'a str> {
if !path.starts_with('/')
|| path.contains('\\')
|| path.contains("//")
@@ -151,7 +149,10 @@ fn resolve_static_path<'a>(
path
};
if site.assets.contains_key(route) {
return site.assets.get_key_value(route).map(|(key, _)| key.as_str());
return site
.assets
.get_key_value(route)
.map(|(key, _)| key.as_str());
}
if route == "/favicon.ico" && site.assets.contains_key("/favicon.svg") {
return Some("/favicon.svg");
@@ -198,23 +199,19 @@ async fn proxy_to_upstream(
.max_header_bytes;
let mut builder = hyper::client::conn::http1::Builder::new();
builder.max_buf_size(max_header_bytes);
let (mut sender, connection) = match tokio::time::timeout(
header_timeout,
builder.handshake(TokioIo::new(stream)),
)
.await
{
Ok(Ok(parts)) => parts,
_ => return bad_gateway(),
};
let (mut sender, connection) =
match tokio::time::timeout(header_timeout, builder.handshake(TokioIo::new(stream))).await {
Ok(Ok(parts)) => parts,
_ => return bad_gateway(),
};
runtime.spawn_auxiliary(async move {
let _ = connection.await;
});
let mut response = match tokio::time::timeout(header_timeout, sender.send_request(request)).await
{
Ok(Ok(response)) => response,
_ => return bad_gateway(),
};
let mut response =
match tokio::time::timeout(header_timeout, sender.send_request(request)).await {
Ok(Ok(response)) => response,
_ => return bad_gateway(),
};
remove_hop_by_hop(response.headers_mut());
response.map(|body| {
body.map_err(|error| -> BoxError { Box::new(error) })
+11 -21
View File
@@ -2,15 +2,13 @@ use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use base64::Engine as _;
use hyper::header;
use hyper::Request;
use hyper::header;
use ipnetwork::IpNetwork;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use crate::config::{
WebClientIpSource, WebRuntimeProfile, WebRuntimeVhost,
};
use crate::config::{WebClientIpSource, WebRuntimeProfile, WebRuntimeVhost};
use crate::web::manager::TokenHash;
/// Parses one lowercase canonical Host value restricted to the public HTTPS port.
@@ -26,8 +24,7 @@ pub(super) fn canonical_request_host<B>(request: &Request<B>) -> Option<&str> {
return None;
}
let host = value.strip_suffix(":443").unwrap_or(value);
if authority.host() != host || host.bytes().any(|byte| byte.is_ascii_uppercase())
{
if authority.host() != host || host.bytes().any(|byte| byte.is_ascii_uppercase()) {
return None;
}
Some(host)
@@ -74,14 +71,14 @@ pub(super) fn bridge_candidate(query: Option<&str>) -> ([u8; 32], bool) {
return (candidate, false);
}
let mut decoded = [0u8; 32];
let Ok(decoded_len) = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode_slice(value, &mut decoded)
let Ok(decoded_len) =
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode_slice(value, &mut decoded)
else {
return (candidate, false);
};
let mut canonical = [0u8; 43];
let Ok(encoded_len) = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode_slice(decoded, &mut canonical)
let Ok(encoded_len) =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode_slice(decoded, &mut canonical)
else {
return (candidate, false);
};
@@ -114,8 +111,7 @@ pub(super) fn bearer_token_hash<B>(request: &Request<B>) -> Option<TokenHash> {
let values = request.headers().get_all(header::AUTHORIZATION);
let mut values = values.iter();
let value = values.next()?.to_str().ok()?;
if values.next().is_some() || !value.starts_with("Bearer ") || value.matches(' ').count() != 1
{
if values.next().is_some() || !value.starts_with("Bearer ") || value.matches(' ').count() != 1 {
return None;
}
let token = value.strip_prefix("Bearer ")?;
@@ -133,7 +129,7 @@ pub(super) fn bearer_token_hash<B>(request: &Request<B>) -> Option<TokenHash> {
(decoded_len == decoded.len()
&& encoded_len == canonical.len()
&& bool::from(canonical.ct_eq(token.as_bytes())))
.then(|| Sha256::digest(decoded).into())
.then(|| Sha256::digest(decoded).into())
}
/// Checks the exact carrier media type without accepting duplicate headers.
@@ -146,10 +142,7 @@ pub(super) fn binary_content_type<B>(request: &Request<B>) -> bool {
}
/// Parses one canonical unsigned decimal carrier sequence header.
pub(super) fn canonical_u64_header<B>(
request: &Request<B>,
name: &'static str,
) -> Option<u64> {
pub(super) fn canonical_u64_header<B>(request: &Request<B>, name: &'static str) -> Option<u64> {
let values = request.headers().get_all(name);
let mut values = values.iter();
let value = values.next()?.to_str().ok()?;
@@ -183,10 +176,7 @@ mod tests {
.header("x-forwarded-for", "192.0.2.10")
.body(())
.unwrap();
assert_eq!(
canonical_request_host(&request),
Some("proxy.example.com")
);
assert_eq!(canonical_request_host(&request), Some("proxy.example.com"));
let trusted: [IpNetwork; 1] = ["127.0.0.1/32".parse().unwrap()];
assert_eq!(
client_ip(
+26 -25
View File
@@ -164,10 +164,8 @@ async fn https_carrier_bootstraps_and_closes_one_session() {
let session = response_header(create_headers, "x-session-token");
assert_eq!(session.len(), 43);
let replacement = test_runtime_generation(
2,
runtime_config(capability, WebCarrier::HttpsLanes),
);
let replacement =
test_runtime_generation(2, runtime_config(capability, WebCarrier::HttpsLanes));
active_runtime.store(Arc::clone(&replacement));
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let retry_response = request(&listener, &runtime, create_retry).await;
@@ -183,10 +181,16 @@ async fn https_carrier_bootstraps_and_closes_one_session() {
.into_bytes();
let next_root_response = request(&listener, &runtime, next_root).await;
let (_, next_root_body) = split_response(&next_root_response);
assert!(next_root_body.windows(11).any(|value| value == b"bootstrap='"));
assert!(next_root_body
.windows(21)
.any(|value| value == b"carrier='https-lanes'"));
assert!(
next_root_body
.windows(11)
.any(|value| value == b"bootstrap='")
);
assert!(
next_root_body
.windows(21)
.any(|value| value == b"carrier='https-lanes'")
);
let close = format!(
"DELETE /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
@@ -262,10 +266,7 @@ async fn unused_bootstrap_survives_equivalent_runtime_generation_swap() {
.map(|(token, _)| token)
.unwrap();
let replacement = test_runtime_generation(
2,
runtime_config(capability, WebCarrier::Https),
);
let replacement = test_runtime_generation(2, runtime_config(capability, WebCarrier::Https));
active_runtime.store(Arc::clone(&replacement));
let hello = frame::encode(FrameType::Hello, 0, &[1]);
let mut create = format!(
@@ -305,10 +306,8 @@ async fn unused_bootstrap_is_rejected_after_profile_identity_change() {
.map(|(token, _)| token)
.unwrap();
let replacement = test_runtime_generation(
2,
runtime_config(capability, WebCarrier::HttpsLanes),
);
let replacement =
test_runtime_generation(2, runtime_config(capability, WebCarrier::HttpsLanes));
active_runtime.store(Arc::clone(&replacement));
let hello = frame::encode(FrameType::Hello, 0, &[1]);
let mut create = format!(
@@ -330,10 +329,7 @@ async fn unused_bootstrap_is_rejected_after_profile_identity_change() {
#[tokio::test]
async fn https_lanes_is_advertised_and_requires_canonical_lane_headers() {
let capability = [9u8; 32];
let generation = test_runtime_generation(
1,
runtime_config(capability, WebCarrier::HttpsLanes),
);
let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::HttpsLanes));
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
let runtime = WebProcessRuntime::start(active_runtime);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
@@ -361,7 +357,10 @@ async fn https_lanes_is_advertised_and_requires_canonical_lane_headers() {
create.extend_from_slice(&hello);
let create_response = request(&listener, &runtime, create).await;
let (create_headers, _) = split_response(&create_response);
assert_eq!(response_header(create_headers, "x-carrier-mode"), "https-lanes");
assert_eq!(
response_header(create_headers, "x-carrier-mode"),
"https-lanes"
);
let session = response_header(create_headers, "x-session-token").to_string();
let pong = frame::encode(FrameType::Pong, 0, &[]);
@@ -375,10 +374,12 @@ async fn https_lanes_is_advertised_and_requires_canonical_lane_headers() {
let (uplink_headers, _) = split_response(&uplink_response);
assert!(uplink_headers.starts_with(b"HTTP/1.1 204"));
assert_eq!(response_header(uplink_headers, "x-up-ack"), "1");
assert!(!std::str::from_utf8(uplink_headers)
.unwrap()
.lines()
.any(|line| line.to_ascii_lowercase().starts_with("content-length:")));
assert!(
!std::str::from_utf8(uplink_headers)
.unwrap()
.lines()
.any(|line| line.to_ascii_lowercase().starts_with("content-length:"))
);
let mut missing_lane = format!(
"POST /api/v1/up HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Type: application/octet-stream\r\nX-Up-Seq: 2\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",