This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+278
View File
@@ -0,0 +1,278 @@
mod authority_tests {
use super::*;
fn validation_policy() -> crate::config::ValidationPolicy {
crate::config::ValidationPolicy {
tls_addr: None,
cert_dir: None,
data_dir: None,
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
admin_addr: "127.0.0.1:9090".parse().unwrap(),
}
}
#[test]
fn retry_is_limited_to_safe_methods() {
for method in [Method::GET, Method::HEAD, Method::OPTIONS] {
assert!(retry_safe_method(&method));
}
for method in [Method::POST, Method::PUT, Method::PATCH, Method::DELETE] {
assert!(!retry_safe_method(&method));
}
}
#[test]
fn explicit_http2_backend_uses_http2_request_version() {
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
config.vhosts[0].backends[0].protocol = crate::config::UpstreamProtocol::Http2;
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
let route = snapshot.select("localhost", "/").unwrap();
assert_eq!(
upstream_request_version(route.backend.as_ref().unwrap(), false),
Version::HTTP_2
);
}
#[tokio::test]
async fn h3_body_capable_methods_without_length_become_known_empty_for_h1() {
use bytes::Bytes;
for method in [Method::POST, Method::PUT, Method::PATCH, Method::DELETE] {
let stream = futures_util::stream::empty::<Result<Bytes, std::io::Error>>();
let mut request = Request::builder()
.method(method.clone())
.version(Version::HTTP_3)
.uri("https://example.test/api2/json/item")
.body(Body::from_stream(stream))
.unwrap();
assert_eq!(request.body().size_hint().exact(), None, "{method}");
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11)
.await
.unwrap();
assert!(request.body().is_end_stream(), "{method}");
assert_eq!(request.body().size_hint().exact(), Some(0), "{method}");
}
}
#[tokio::test]
async fn h3_delete_unknown_nonempty_body_is_replayed_not_buffered_away() {
use bytes::Bytes;
let stream = futures_util::stream::iter([Ok::<_, std::io::Error>(
Bytes::from_static(b"payload"),
)]);
let mut request = Request::builder()
.method(Method::DELETE)
.version(Version::HTTP_3)
.uri("https://example.test/api2/json/item")
.body(Body::from_stream(stream))
.unwrap();
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11)
.await
.unwrap();
let body = request.into_body().collect().await.unwrap().to_bytes();
assert_eq!(body, Bytes::from_static(b"payload"));
}
#[tokio::test]
async fn h3_unknown_body_is_untouched_for_http2_upstream() {
use bytes::Bytes;
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
let mut request = Request::builder()
.method(Method::DELETE)
.version(Version::HTTP_3)
.uri("https://example.test/api2/json/item")
.body(Body::from_stream(stream))
.unwrap();
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_2)
.await
.unwrap();
assert_eq!(request.body().size_hint().exact(), None);
}
#[tokio::test]
async fn non_h3_unknown_body_is_untouched_for_http1_upstream() {
use bytes::Bytes;
for version in [Version::HTTP_11, Version::HTTP_2] {
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
let mut request = Request::builder()
.method(Method::DELETE)
.version(version)
.uri("https://example.test/api2/json/item")
.body(Body::from_stream(stream))
.unwrap();
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11)
.await
.unwrap();
assert_eq!(request.body().size_hint().exact(), None);
}
}
#[tokio::test]
async fn h3_declared_delete_body_is_not_preconsumed() {
use bytes::Bytes;
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
let mut request = Request::builder()
.method(Method::DELETE)
.version(Version::HTTP_3)
.uri("https://example.test/api2/json/item")
.header(http::header::CONTENT_LENGTH, "7")
.body(Body::from_stream(stream))
.unwrap();
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11)
.await
.unwrap();
assert_eq!(request.headers()[http::header::CONTENT_LENGTH], "7");
assert_eq!(request.body().size_hint().exact(), None);
}
#[test]
fn generated_policy_and_gateway_errors_use_html_pages() {
for status in [
StatusCode::FORBIDDEN,
StatusCode::TOO_MANY_REQUESTS,
StatusCode::SERVICE_UNAVAILABLE,
StatusCode::GATEWAY_TIMEOUT,
] {
let response = error(status, "internal detail\n");
assert_eq!(response.status(), status);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/html; charset=utf-8"
);
assert_eq!(response.headers().get("cache-control").unwrap(), "no-store");
}
let response = error(StatusCode::BAD_GATEWAY, "internal detail\n");
assert_ne!(
response.headers().get("content-type").unwrap(),
"text/html; charset=utf-8"
);
}
#[test]
fn locally_generated_pages_remove_cache_metadata() {
let mut response = error(StatusCode::BAD_GATEWAY, "internal detail\n");
for (name, value) in [
("etag", "\"old\""),
("x-cache", "HIT"),
("x-cache-reason", "origin"),
("cache-status", "Origin; hit"),
] {
response.headers_mut().insert(name, value.parse().unwrap());
}
enforce_local_page_no_cache(&mut response);
assert_eq!(response.headers().get("cache-control").unwrap(), "no-store");
for name in ["etag", "x-cache", "x-cache-reason", "cache-status"] {
assert!(!response.headers().contains_key(name), "{name}");
}
assert!(response.extensions().get::<LocalGeneratedResponse>().is_some());
}
#[test]
fn http2_uses_authority_and_rejects_conflicting_host() {
let mut request = Request::builder()
.version(Version::HTTP_2)
.uri("https://app.localhost/test")
.body(Body::empty())
.unwrap();
assert_eq!(effective_host(&request, true).unwrap(), "app.localhost");
request
.headers_mut()
.insert(HOST, "different.localhost".parse().unwrap());
assert!(effective_host(&request, true).is_err());
}
#[test]
fn legacy_websocket_origin_matches_upstream_scheme_and_host() {
let origin = legacy_websocket_origin(
"https://ilo.internal:443",
&"10.0.0.5:443".parse().unwrap(),
)
.unwrap();
assert_eq!(origin, "https://10.0.0.5:443");
assert!(legacy_websocket_origin("not-an-origin", &"10.0.0.5".parse().unwrap()).is_none());
}
#[test]
fn force_ssl_location_preserves_path_query_and_https_port() {
let uri: Uri = "/api/items?x=1&next=%2Fhome".parse().unwrap();
assert_eq!(
force_ssl_location("app.example.test", &uri, 443).unwrap(),
"https://app.example.test/api/items?x=1&next=%2Fhome"
);
assert_eq!(
force_ssl_location("app.example.test", &uri, 8443).unwrap(),
"https://app.example.test:8443/api/items?x=1&next=%2Fhome"
);
}
#[test]
fn http1_absolute_form_and_h2_wrong_scheme_are_rejected() {
let request = Request::builder()
.uri("http://app.localhost/")
.header(HOST, "app.localhost")
.body(Body::empty())
.unwrap();
assert!(effective_host(&request, false).is_err());
let request = Request::builder()
.version(Version::HTTP_2)
.uri("http://app.localhost/")
.body(Body::empty())
.unwrap();
assert!(effective_host(&request, true).is_err());
}
#[derive(Debug)]
struct NestedDiagnosticError {
source: std::io::Error,
}
impl std::fmt::Display for NestedDiagnosticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("connector wrapper")
}
}
impl std::error::Error for NestedDiagnosticError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[test]
fn upstream_error_chain_keeps_nested_connector_cause() {
let error = NestedDiagnosticError {
source: std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"connection refused by origin",
),
};
let detail = format_error_chain(&error);
assert!(detail.contains("connector wrapper"));
assert!(detail.contains("connection refused by origin"));
}
#[test]
fn upstream_error_detail_truncation_preserves_utf8_boundaries() {
let mut detail = "ą".repeat(MAX_UPSTREAM_ERROR_DETAIL);
truncate_utf8(&mut detail, MAX_UPSTREAM_ERROR_DETAIL);
assert!(detail.len() <= MAX_UPSTREAM_ERROR_DETAIL);
assert!(std::str::from_utf8(detail.as_bytes()).is_ok());
}
#[test]
fn backend_diagnostics_are_opt_in_and_strip_spoofed_values() {
let mut response = Response::new(Body::empty());
response.headers_mut().insert("x-rust-proxy-backend", HeaderValue::from_static("spoofed"));
response.headers_mut().insert("x-rust-proxy-backend-time", HeaderValue::from_static("legacy-spoof"));
response.headers_mut().insert("x-rustproxy-backend-time", HeaderValue::from_static("999ms"));
apply_backend_diagnostic_values(&mut response, false, false);
assert!(response.headers().get("x-rust-proxy-backend").is_none());
assert!(response.headers().get("x-rust-proxy-backend-time").is_none());
assert!(response.headers().get("x-rustproxy-backend-time").is_none());
response.extensions_mut().insert(OriginBackend("origin-a".into()));
response.extensions_mut().insert(OriginTiming(Duration::from_millis(12)));
apply_backend_diagnostic_values(&mut response, true, true);
assert_eq!(response.headers().get("x-rust-proxy-backend").unwrap(), "origin-a");
assert_eq!(response.headers().get("x-rustproxy-backend-time").unwrap(), "12.00ms");
}
}