v0.32.4
This commit is contained in:
1 parent
b0f211555a
commit
8b9ca4db44
39 files changed
+1481
-303
No files matched your search
@@ -60,7 +60,23 @@ if pending_revalidation {
|
||||
if request.method() == Method::HEAD && cache_ticket.is_some() {
|
||||
*request.method_mut() = Method::GET;
|
||||
}
|
||||
*request.uri_mut() = uri.clone();
|
||||
// Preserve a pre-normalization Host snapshot for pool retries. Each next
|
||||
// backend gets its OWN target and Host/:authority, never the previous peer's.
|
||||
let retry_headers = request.headers().clone();
|
||||
let expected_backend_host = if backend.preserve_host {
|
||||
&original_host
|
||||
} else {
|
||||
&backend.host_header
|
||||
};
|
||||
let retry_host_follows_backend = retry_headers.get(HOST)
|
||||
.is_some_and(|value| value == expected_backend_host);
|
||||
let Some(upstream_target) = upstream_request_target(
|
||||
&uri, request.headers_mut(), &backend,
|
||||
websocket_legacy_support && !websocket_upstream_h2, websocket_upstream_h2,
|
||||
) else {
|
||||
return error(StatusCode::BAD_GATEWAY, "RustProxy: invalid upstream authority\n");
|
||||
};
|
||||
*request.uri_mut() = upstream_target.clone();
|
||||
let upstream_version = if websocket_legacy_support && !websocket_upstream_h2 {
|
||||
Version::HTTP_11
|
||||
} else {
|
||||
@@ -107,17 +123,7 @@ let proactive_base_headers = request.headers().clone();
|
||||
let response_header_timeout = frontend
|
||||
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
|
||||
.unwrap_or(state.header_timeout);
|
||||
let retry_headers = request.headers().clone();
|
||||
let retry_method = request.method().clone();
|
||||
let expected_backend_host = if backend.preserve_host {
|
||||
&original_host
|
||||
} else {
|
||||
&backend.host_header
|
||||
};
|
||||
let retry_host_follows_backend = request
|
||||
.headers()
|
||||
.get(HOST)
|
||||
.is_some_and(|value| value == expected_backend_host);
|
||||
let retry_policy = route.pool.as_ref().map(|pool| pool.settings.retry.clone());
|
||||
let retry_enabled = route.backend_id.is_none()
|
||||
&& !admin_control_plane
|
||||
@@ -131,7 +137,7 @@ let mut active_backend = backend.clone();
|
||||
let mut active_lease = backend_lease.clone();
|
||||
drop(backend_lease);
|
||||
let mut active_affinity_cookie = affinity_cookie.clone();
|
||||
let mut active_uri = uri.clone();
|
||||
let mut active_uri = upstream_target;
|
||||
let mut request_attempt = Some(request);
|
||||
let mut attempted_backends = Vec::<String>::new();
|
||||
let mut retries = 0u8;
|
||||
|
||||
@@ -32,6 +32,68 @@ mod authority_tests {
|
||||
Version::HTTP_2
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn preserve_host_sets_http2_authority_without_changing_network_origin() {
|
||||
use crate::config::UpstreamProtocol;
|
||||
for (protocol, scheme, expected_authority, remove_host) in [
|
||||
(UpstreamProtocol::Http1, "https", "backend.internal:443", false),
|
||||
(UpstreamProtocol::Auto, "https", "unitraklub.pl", false),
|
||||
(UpstreamProtocol::Http2, "https", "unitraklub.pl", false),
|
||||
(UpstreamProtocol::H2, "https", "unitraklub.pl", true),
|
||||
(UpstreamProtocol::Auto, "http", "10.87.20.2:443", false),
|
||||
(UpstreamProtocol::Http2, "http", "unitraklub.pl", true),
|
||||
(UpstreamProtocol::H2, "http", "unitraklub.pl", true),
|
||||
] {
|
||||
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
let member = &mut config.vhosts[0].backends[0];
|
||||
member.url = format!("{scheme}://10.87.20.2:443");
|
||||
member.allow_private = true;
|
||||
member.protocol = protocol;
|
||||
member.preserve_host = true;
|
||||
if scheme == "https" {
|
||||
member.tls_server_name = Some("backend.internal".into());
|
||||
}
|
||||
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
||||
let backend = &snapshot.pools[0].members[0].backend;
|
||||
let uri = upstream_uri(backend, "/block_refresh/block/10").unwrap();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(HOST, HeaderValue::from_static("unitraklub.pl"));
|
||||
let target = upstream_request_target(&uri, &mut headers, backend, false, false).unwrap();
|
||||
assert_eq!(target.authority().unwrap().as_str(), expected_authority, "{protocol:?}");
|
||||
assert_eq!(target.path(), "/block_refresh/block/10");
|
||||
assert_eq!(headers.contains_key(HOST), !remove_host);
|
||||
assert_eq!(backend.origin, format!("{scheme}://10.87.20.2:443"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_outgoing_host_is_rejected_instead_of_used_for_dialing() {
|
||||
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.vhosts[0].backends[0].protocol = crate::config::UpstreamProtocol::H2;
|
||||
config.vhosts[0].backends[0].preserve_host = true;
|
||||
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
||||
let backend = &snapshot.pools[0].members[0].backend;
|
||||
let uri = upstream_uri(backend, "/").unwrap();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(HOST, HeaderValue::from_static("evil@example.invalid"));
|
||||
assert!(upstream_request_target(&uri, &mut headers, backend, false, false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_prepares_authority_for_the_selected_backend_not_the_previous_one() {
|
||||
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.vhosts[0].backends[0].protocol = crate::config::UpstreamProtocol::H2;
|
||||
config.vhosts[0].backends[0].preserve_host = true;
|
||||
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
||||
let backend = &snapshot.pools[0].members[0].backend;
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(HOST, HeaderValue::from_static("public.example"));
|
||||
let req = retry_request(&Method::GET, Version::HTTP_2, &headers, backend,
|
||||
&HeaderValue::from_static("public.example"), "/retry", true).unwrap();
|
||||
assert_eq!(req.uri().authority().unwrap().as_str(), "public.example");
|
||||
assert!(req.headers().get(HOST).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_https_http2_version_is_h1_safe_and_strict_h2_is_required() {
|
||||
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
|
||||
+44
-1
@@ -158,6 +158,46 @@ fn legacy_websocket_origin(
|
||||
HeaderValue::from_str(&format!("{scheme}://{authority}")).ok()
|
||||
}
|
||||
|
||||
/// Align HTTP request authority with the selected public/backend Host.
|
||||
///
|
||||
/// TCP/DNS/TLS still target the configured origin in TimedConnector. Strict
|
||||
/// HTTP/2 sends :authority without a second Host field. HTTPS ALPN mixed
|
||||
/// mode must retain the Host field: Hyper's legacy H1-compatible request may
|
||||
/// be sent over an H2-negotiated connection, and H1 fallback needs Host.
|
||||
fn upstream_request_target(
|
||||
uri: &Uri,
|
||||
headers: &mut HeaderMap,
|
||||
backend: &crate::config::CompiledBackend,
|
||||
websocket_h1: bool,
|
||||
websocket_h2: bool,
|
||||
) -> Option<Uri> {
|
||||
let h2_capable = !websocket_h1 && match backend.protocol {
|
||||
crate::config::UpstreamProtocol::Http1 => false,
|
||||
crate::config::UpstreamProtocol::Auto => backend.origin.starts_with("https://"),
|
||||
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2 => true,
|
||||
};
|
||||
if !h2_capable {
|
||||
return Some(uri.clone());
|
||||
}
|
||||
let selected = headers.get(HOST).unwrap_or(&backend.host_header);
|
||||
let authority: http::uri::Authority = selected.to_str().ok()?.parse().ok()?;
|
||||
// Never treat a userinfo URI or a malformed Host as a valid HTTP authority.
|
||||
if authority.host().is_empty() || authority.as_str().contains('@') {
|
||||
return None;
|
||||
}
|
||||
let mut parts = uri.clone().into_parts();
|
||||
parts.authority = Some(authority);
|
||||
let target = Uri::from_parts(parts).ok()?;
|
||||
let strict_h2_request = websocket_h2
|
||||
|| backend.protocol == crate::config::UpstreamProtocol::H2
|
||||
|| (backend.protocol == crate::config::UpstreamProtocol::Http2
|
||||
&& backend.origin.starts_with("http://"));
|
||||
if strict_h2_request {
|
||||
headers.remove(HOST);
|
||||
}
|
||||
Some(target)
|
||||
}
|
||||
|
||||
fn upstream_request_version(
|
||||
backend: &crate::config::CompiledBackend,
|
||||
websocket_h2: bool,
|
||||
@@ -188,7 +228,8 @@ fn retry_request(
|
||||
let mut request = Request::builder()
|
||||
.method(method.clone())
|
||||
.version(version)
|
||||
.uri(uri)
|
||||
// Keep the original URI for authority adjustment after building the request.
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.ok()?;
|
||||
*request.headers_mut() = headers.clone();
|
||||
@@ -202,6 +243,8 @@ fn retry_request(
|
||||
},
|
||||
);
|
||||
}
|
||||
let target = upstream_request_target(&uri, request.headers_mut(), backend, false, false)?;
|
||||
*request.uri_mut() = target;
|
||||
Some(request)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user