v0.32.4
This commit is contained in:
1 parent
b0f211555a
commit
8b9ca4db44
39 files changed
+1481
-303
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.32.3",
|
||||
"version": "0.32.4",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
@@ -1981,7 +1981,8 @@
|
||||
},
|
||||
"preserve_host": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Keep the original client Host for HTTP/1.1 or HTTP/2 :authority, independently of origin URL, TCP destination and TLS SNI. Supported with http1, auto, http2 and h2."
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
|
||||
+695
-183
File diff suppressed because it is too large.
Load diff
@@ -207,7 +207,8 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
const connection = el("div", undefined, "vh-wide vh-backend-switches vh-backend-connection"); main.append(connection);
|
||||
controls["member-private"] = field(connection, "member-private", "vhPrivate", value.allow_private, "checkbox");
|
||||
const preserve = field(connection, "member-host", "vhPreserveHost", value.preserve_host === true, "checkbox");
|
||||
const syncProtocol = () => { preserve.disabled = protocol.value !== "http1"; if (preserve.disabled) preserve.checked = false; }; protocol.addEventListener("change", syncProtocol); syncProtocol();
|
||||
// Preserve Host is a request-authority choice, not an HTTP/1.1-only option.
|
||||
// The Rust forwarder keeps HTTP/2 :authority consistent with this value.
|
||||
|
||||
const advanced = el("details", undefined, "vh-wide vh-backend-advanced"), extra = el("div", undefined, "vh-grid"); advanced.append(text("summary", "advanced"), extra);
|
||||
field(extra, "member-allowlist", "vhAddressAllowlist", (value.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
||||
@@ -258,7 +259,7 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
if (!(await appConfirm(t("vhDebugOriginTlsWarning"), { danger: true }))) return;
|
||||
if (!url.value.trim()) url.value = "https://127.0.0.1:9000";
|
||||
controls["member-pool"].checked = true; controls["member-private"].checked = true;
|
||||
protocol.value = "http1"; preserve.checked = true; tlsVerify.checked = false; syncProtocol(); syncRisk(); changed();
|
||||
protocol.value = "http1"; preserve.checked = true; tlsVerify.checked = false; protocol.dispatchEvent(new Event("change")); syncRisk(); changed();
|
||||
}));
|
||||
actions.append(text("span", "vhOriginPresetHelp", "fine")); row.append(actions);
|
||||
|
||||
@@ -296,7 +297,10 @@ function debugOriginIssues(members) {
|
||||
let parsed; try { parsed = new URL(member.url); } catch { return false; }
|
||||
const host = parsed.hostname.replace(/^\[|\]$/g, ""), ipHost = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
||||
if (parsed.protocol !== "https:" || !ipHost) return false;
|
||||
return !member.pool_eligible || !member.allow_private || member.protocol !== "http1" || !member.preserve_host || member.tls_verify;
|
||||
// The troubleshooting preset is OPTIONAL, not a protocol compatibility rule.
|
||||
// Warn only when IP-host TLS verification may need an explicit identity;
|
||||
// HTTP/1.1, Auto and H2 plus preserve_host are all valid configurations.
|
||||
return member.tls_verify === true && !String(member.tls_server_name || "").trim();
|
||||
});
|
||||
}
|
||||
function aclEditor(parent, initial, onChange = () => { }) {
|
||||
|
||||
@@ -12,8 +12,6 @@ pub(crate) fn compile_backend_with_timeout(
|
||||
matches!(url.scheme(), "http" | "https"),
|
||||
"backend scheme must be http or https"
|
||||
);
|
||||
ensure!(backend.protocol == UpstreamProtocol::Http1 || !backend.preserve_host,
|
||||
"HTTP/2 or auto upstream requires preserve_host=false; use http1 for original Host preservation");
|
||||
ensure!(
|
||||
backend.tls_ca_pem.is_none() || url.scheme() == "https",
|
||||
"backend additional CA can only be used with an https:// URL; remove the CA or change the backend URL to HTTPS"
|
||||
|
||||
@@ -348,6 +348,25 @@ fn force_ssl_requires_plain_http_and_chooses_https_target() {
|
||||
let error = compile(c, 4, &policy()).unwrap_err().to_string();
|
||||
assert!(error.contains("plain HTTP listener"), "{error}");
|
||||
}
|
||||
#[test]
|
||||
fn preserve_host_is_valid_with_every_upstream_protocol() {
|
||||
for protocol in [
|
||||
UpstreamProtocol::Http1,
|
||||
UpstreamProtocol::Auto,
|
||||
UpstreamProtocol::Http2,
|
||||
UpstreamProtocol::H2,
|
||||
] {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
let member = &mut config.vhosts[0].backends[0];
|
||||
member.url = "https://10.87.20.2:443".into();
|
||||
member.allow_private = true;
|
||||
member.preserve_host = true;
|
||||
member.protocol = protocol;
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
assert!(snapshot.pools[0].members[0].backend.preserve_host);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_tls_server_name_keeps_network_origin_and_changes_verified_request_name() {
|
||||
let mut c = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -196,6 +196,9 @@ pub struct TimedConnector {
|
||||
inner: InnerConnector,
|
||||
policy: AddressPolicy,
|
||||
timeout: Duration,
|
||||
/// The configured backend/TLS identity, never the forwarded HTTP Host.
|
||||
/// The resolver separately pins DNS to the configured backend URL.
|
||||
destination: Uri,
|
||||
}
|
||||
impl Service<Uri> for TimedConnector {
|
||||
type Response = <InnerConnector as Service<Uri>>::Response;
|
||||
@@ -205,7 +208,10 @@ impl Service<Uri> for TimedConnector {
|
||||
fn poll_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll<std::result::Result<(), BoxError>> {
|
||||
self.inner.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
fn call(&mut self, uri: Uri) -> Self::Future {
|
||||
fn call(&mut self, _request_uri: Uri) -> Self::Future {
|
||||
// A preserved HTTP/2 :authority can be a public vhost name. Never
|
||||
// resolve or dial it: all sockets must target the configured origin.
|
||||
let uri = self.destination.clone();
|
||||
if let Some(ip) = uri.host().and_then(|host| {
|
||||
host.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
|
||||
@@ -364,10 +364,27 @@ fn build_raw_client(factory: &ClientFactory) -> Result<RawProxyClient> {
|
||||
)
|
||||
.http2_keep_alive_timeout(Duration::from_secs(10))
|
||||
.http2_keep_alive_while_idle(false);
|
||||
// The request URI is free to carry the preserved public H2 :authority,
|
||||
// while TLS SNI/verification uses tls_server_name (or backend URL) and
|
||||
// DNS/TCP remains restricted to backend.url by GuardedResolver.
|
||||
let origin = url::Url::parse(&backend.url)?;
|
||||
let network_host = match origin.host().context("backend host missing")? {
|
||||
url::Host::Ipv6(ip) => format!("[{ip}]"),
|
||||
host => host.to_string(),
|
||||
};
|
||||
let port = origin.port_or_known_default().context("backend port missing")?;
|
||||
let tls_name = backend.tls_server_name
|
||||
.as_deref()
|
||||
.map(crate::config::normalize_host)
|
||||
.transpose()?
|
||||
.unwrap_or(network_host);
|
||||
let destination: http::Uri = format!("{}://{tls_name}:{port}", origin.scheme())
|
||||
.parse().context("invalid configured backend transport authority")?;
|
||||
Ok(builder.build(TimedConnector {
|
||||
inner,
|
||||
policy: policy.clone(),
|
||||
timeout,
|
||||
destination,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -221,3 +221,154 @@ fn default_protocol_and_serialization_are_backward_compatible() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise real TLS ALPN and the actual Hyper connector rather than only the
|
||||
// local authority helper. The public Host must never become a DNS/TCP target.
|
||||
async fn tls_authority_origin(
|
||||
offer_h2: bool,
|
||||
) -> (
|
||||
std::net::SocketAddr,
|
||||
tokio::sync::oneshot::Receiver<(Version, Option<String>, Option<String>, Option<String>)>,
|
||||
tokio::task::JoinHandle<()>,
|
||||
) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let pair = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let key = rustls::pki_types::PrivatePkcs8KeyDer::from(pair.signing_key.serialize_der());
|
||||
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
||||
let mut config = rustls::ServerConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions().unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(vec![pair.cert.der().clone()], key.into()).unwrap();
|
||||
config.alpn_protocols = if offer_h2 {
|
||||
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
||||
} else {
|
||||
vec![b"http/1.1".to_vec()]
|
||||
};
|
||||
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
|
||||
let (observed_tx, observed_rx) = tokio::sync::oneshot::channel();
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.unwrap();
|
||||
let tls = acceptor.accept(stream).await.unwrap();
|
||||
let sni = tls.get_ref().1.server_name().map(str::to_owned);
|
||||
let observed = Arc::new(std::sync::Mutex::new(Some(observed_tx)));
|
||||
let service = service_fn(move |request: Request<hyper::body::Incoming>| {
|
||||
let observed = observed.clone();
|
||||
let sni = sni.clone();
|
||||
async move {
|
||||
if let Some(tx) = observed.lock().unwrap().take() {
|
||||
let authority = request.uri().authority().map(|v| v.as_str().to_owned());
|
||||
let host = request.headers().get("host")
|
||||
.map(|v| v.to_str().unwrap().to_owned());
|
||||
let _ = tx.send((request.version(), authority, host, sni));
|
||||
}
|
||||
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
|
||||
}
|
||||
});
|
||||
if offer_h2 {
|
||||
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
|
||||
.serve_connection(TokioIo::new(tls), service).await;
|
||||
} else {
|
||||
let _ = hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(TokioIo::new(tls), service).await;
|
||||
}
|
||||
});
|
||||
(address, observed_rx, server)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn strict_h2_preserves_public_authority_while_tcp_and_sni_use_private_origin() {
|
||||
let (address, seen, server) = tls_authority_origin(true).await;
|
||||
let mut backend = protocol_backend(address, "https", "h2");
|
||||
backend.tls_server_name = Some("localhost".into());
|
||||
backend.preserve_host = true;
|
||||
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
|
||||
let request = Request::builder().version(Version::HTTP_2)
|
||||
.uri("https://unitraklub.pl/block_refresh/block/10")
|
||||
.body(Body::empty()).unwrap();
|
||||
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
|
||||
.await.unwrap().unwrap();
|
||||
assert_eq!(response.version(), Version::HTTP_2);
|
||||
let (version, authority, host, sni) = seen.await.unwrap();
|
||||
assert_eq!(version, Version::HTTP_2);
|
||||
assert_eq!(authority.as_deref(), Some("unitraklub.pl"));
|
||||
assert!(host.is_none(), "Strict H2 must not duplicate :authority with Host");
|
||||
assert_eq!(sni.as_deref(), Some("localhost"));
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_h1_fallback_preserves_public_host_without_resolving_it() {
|
||||
let (address, seen, server) = tls_authority_origin(false).await;
|
||||
let mut backend = protocol_backend(address, "https", "auto");
|
||||
backend.tls_server_name = Some("localhost".into());
|
||||
backend.preserve_host = true;
|
||||
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
|
||||
let request = Request::builder().version(Version::HTTP_11)
|
||||
.uri("https://unitraklub.pl/block_refresh/block/10")
|
||||
.header("host", "unitraklub.pl")
|
||||
.body(Body::empty()).unwrap();
|
||||
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
|
||||
.await.unwrap().unwrap();
|
||||
assert_eq!(response.version(), Version::HTTP_11);
|
||||
let (version, _authority, host, sni) = seen.await.unwrap();
|
||||
assert_eq!(version, Version::HTTP_11);
|
||||
assert_eq!(host.as_deref(), Some("unitraklub.pl"));
|
||||
assert_eq!(sni.as_deref(), Some("localhost"));
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_h2_alpn_retains_same_public_host_and_pinned_tls_origin() {
|
||||
let (address, seen, server) = tls_authority_origin(true).await;
|
||||
let mut backend = protocol_backend(address, "https", "auto");
|
||||
backend.tls_server_name = Some("localhost".into());
|
||||
backend.preserve_host = true;
|
||||
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
|
||||
let request = Request::builder().version(Version::HTTP_11)
|
||||
.uri("https://unitraklub.pl/block_refresh/block/10")
|
||||
.header("host", "unitraklub.pl")
|
||||
.body(Body::empty()).unwrap();
|
||||
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
|
||||
.await.unwrap().unwrap();
|
||||
assert_eq!(response.version(), Version::HTTP_2);
|
||||
let (version, authority, host, sni) = seen.await.unwrap();
|
||||
assert_eq!(version, Version::HTTP_2);
|
||||
// Legacy Hyper may preserve H1-compatible Host on ALPN H2; neither value
|
||||
// may select a different origin or differ from the intended public name.
|
||||
assert_eq!(authority.or(host).as_deref(), Some("unitraklub.pl"));
|
||||
assert_eq!(sni.as_deref(), Some("localhost"));
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cleartext_h2c_preserves_public_authority_and_pins_socket() {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let (tx, observed) = tokio::sync::oneshot::channel();
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.unwrap();
|
||||
let result = Arc::new(std::sync::Mutex::new(Some(tx)));
|
||||
let service = service_fn(move |request: Request<hyper::body::Incoming>| {
|
||||
let result = result.clone();
|
||||
async move {
|
||||
if let Some(tx) = result.lock().unwrap().take() {
|
||||
let _ = tx.send(request.uri().authority().map(|v| v.as_str().to_owned()));
|
||||
}
|
||||
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
|
||||
}
|
||||
});
|
||||
let _ = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
|
||||
.serve_connection(TokioIo::new(stream), service).await;
|
||||
});
|
||||
let backend = protocol_backend(address, "http", "h2");
|
||||
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
|
||||
let request = Request::builder().version(Version::HTTP_2)
|
||||
.uri("http://unitraklub.pl/test")
|
||||
.body(Body::empty()).unwrap();
|
||||
let response = tokio::time::timeout(Duration::from_secs(5), client.request(request))
|
||||
.await.unwrap().unwrap();
|
||||
assert_eq!(response.version(), Version::HTTP_2);
|
||||
assert_eq!(observed.await.unwrap().as_deref(), Some("unitraklub.pl"));
|
||||
server.abort();
|
||||
}
|
||||
Reference in new issue
Block a user