599 lines
25 KiB
Rust
599 lines
25 KiB
Rust
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
|
|
);
|
|
}
|
|
#[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());
|
|
config.vhosts[0].backends[0].url = "https://127.0.0.1:9000".to_owned();
|
|
config.vhosts[0].backends[0].protocol = crate::config::UpstreamProtocol::Http2;
|
|
let snapshot = crate::config::compile(config.clone(), 1, &validation_policy()).unwrap();
|
|
let route = snapshot.select("localhost", "/").unwrap();
|
|
let origin = route.backend.as_ref().unwrap();
|
|
assert_eq!(upstream_request_version(origin, false), Version::HTTP_11);
|
|
// Extended CONNECT still uses H2 only in its existing WebSocket path.
|
|
assert_eq!(upstream_request_version(origin, true), Version::HTTP_2);
|
|
|
|
config.vhosts[0].backends[0].protocol = crate::config::UpstreamProtocol::H2;
|
|
let snapshot = crate::config::compile(config, 2, &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::BAD_GATEWAY,
|
|
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");
|
|
}
|
|
}
|
|
#[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-rustproxy-backend", HeaderValue::from_static("canonical-spoof"));
|
|
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-rustproxy-backend").is_none());
|
|
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)));
|
|
response.headers_mut().insert("x-rustproxy-backend", HeaderValue::from_static("canonical-spoof"));
|
|
apply_backend_diagnostic_values(&mut response, true, true);
|
|
assert!(response.headers().get("x-rustproxy-backend").is_none());
|
|
assert_eq!(
|
|
response.headers().get("x-rust-proxy-backend").unwrap(),
|
|
"origin-a"
|
|
);
|
|
assert_eq!(
|
|
response.headers().get("x-rustproxy-backend-time").unwrap(),
|
|
"12.00ms"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn upstream_uri_preserves_nested_url_query_without_rejecting_valid_separators() {
|
|
let config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
|
|
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
|
let backend = snapshot
|
|
.select("localhost", "/")
|
|
.unwrap()
|
|
.backend
|
|
.as_ref()
|
|
.unwrap();
|
|
let suffix = "/convert?url=https%3A//easylist.to/easylist/easylist.txt&ip=0.0.0.0";
|
|
let uri = upstream_uri(backend, suffix).unwrap();
|
|
assert_eq!(uri.path_and_query().unwrap().as_str(), suffix);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_fallback_replays_empty_unknown_length_h2_h3_get_and_head() {
|
|
use bytes::Bytes;
|
|
|
|
for version in [Version::HTTP_2, Version::HTTP_3] {
|
|
for method in [Method::GET, Method::HEAD] {
|
|
let stream = futures_util::stream::empty::<Result<Bytes, std::io::Error>>();
|
|
let mut request = Request::builder()
|
|
.version(version)
|
|
.method(method.clone())
|
|
.uri("https://www.example.test/missing?query=1")
|
|
.body(Body::from_stream(stream))
|
|
.unwrap();
|
|
assert_eq!(request.body().size_hint().exact(), None);
|
|
let result = match prepare_error_fallback_body(&mut request, true).await {
|
|
Ok(result) => result,
|
|
Err(response) => panic!("unexpected status {}", response.status()),
|
|
};
|
|
assert!(
|
|
matches!(result, ErrorFallbackBody::Replayable(ref bytes) if bytes.is_empty()),
|
|
"an empty {method:?} request over {version:?} should allow fallback"
|
|
);
|
|
assert!(request.body().is_end_stream());
|
|
assert_eq!(
|
|
request.uri().path_and_query().unwrap().as_str(),
|
|
"/missing?query=1"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_fallback_preserves_unknown_length_get_payload_without_replaying_it() {
|
|
use bytes::Bytes;
|
|
|
|
let stream = futures_util::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(
|
|
b"unexpected-payload",
|
|
))]);
|
|
let mut request = Request::builder()
|
|
.method(Method::GET)
|
|
.version(Version::HTTP_3)
|
|
.uri("https://www.example.test/missing")
|
|
.body(Body::from_stream(stream))
|
|
.unwrap();
|
|
let result = match prepare_error_fallback_body(&mut request, true).await {
|
|
Ok(result) => result,
|
|
Err(response) => panic!("unexpected status {}", response.status()),
|
|
};
|
|
assert!(matches!(result, ErrorFallbackBody::Unavailable));
|
|
assert_eq!(
|
|
request.into_body().collect().await.unwrap().to_bytes(),
|
|
Bytes::from_static(b"unexpected-payload")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn error_fallback_does_not_poll_unknown_length_post_payload() {
|
|
use bytes::Bytes;
|
|
|
|
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
|
|
let mut request = Request::builder()
|
|
.method(Method::POST)
|
|
.version(Version::HTTP_2)
|
|
.uri("https://www.example.test/form")
|
|
.body(Body::from_stream(stream))
|
|
.unwrap();
|
|
let result = match prepare_error_fallback_body(&mut request, true).await {
|
|
Ok(result) => result,
|
|
Err(response) => panic!("unexpected status {}", response.status()),
|
|
};
|
|
assert!(matches!(result, ErrorFallbackBody::Unavailable));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn fallback_terminal_404_html_takes_precedence_over_builtin_error_page() {
|
|
use bytes::Bytes;
|
|
|
|
let mut response = Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.header("content-type", "text/html; charset=utf-8")
|
|
.body(Body::from("<html><title>ReactPHP 404</title></html>"))
|
|
.unwrap();
|
|
response.extensions_mut().insert(ErrorFallbackResponse);
|
|
let configured_page = crate::pages::Page {
|
|
kind: crate::pages::Kind::Builtin,
|
|
status: Some(404),
|
|
html: String::new(),
|
|
};
|
|
if should_replace_error_page(&response, false) {
|
|
crate::pages::replace_error(&mut response, &configured_page);
|
|
}
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
assert_eq!(
|
|
response.into_body().collect().await.unwrap().to_bytes(),
|
|
Bytes::from_static(b"<html><title>ReactPHP 404</title></html>")
|
|
);
|
|
|
|
// A proxy-generated response is eligible for configured global/vhost
|
|
// pages even when interception of *application* errors is disabled.
|
|
let response = Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
assert!(should_replace_error_page(&response, false));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn application_404_is_not_overwritten_by_global_pages_when_interception_is_off() {
|
|
use http_body_util::BodyExt;
|
|
|
|
for source in ["reverse_proxy", "web_server_php"] {
|
|
let mut response = Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body(Body::from(format!("{source}: origin 404 body")))
|
|
.unwrap();
|
|
// Both paths use intercept_route_origin_error to set this marker
|
|
// once the origin response reaches Response<Body>.
|
|
response
|
|
.extensions_mut()
|
|
.insert(crate::pages::OriginProvidedResponse);
|
|
assert!(!should_replace_error_page(&response, false), "{source}");
|
|
let bytes = response.into_body().collect().await.unwrap().to_bytes();
|
|
assert_eq!(
|
|
bytes.as_ref(),
|
|
format!("{source}: origin 404 body").as_bytes()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn compiled_vhost_origin_interception_controls_application_404() {
|
|
for enabled in [false, true] {
|
|
let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap());
|
|
config.pages.errors.insert(
|
|
404,
|
|
crate::pages::Page {
|
|
kind: crate::pages::Kind::Html,
|
|
status: Some(503),
|
|
html: "RustProxy custom error page".into(),
|
|
},
|
|
);
|
|
if enabled {
|
|
config.vhosts[0].pages.intercept_statuses = vec![404];
|
|
}
|
|
let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap();
|
|
let route = snapshot.select("localhost", "/").unwrap();
|
|
let response = Response::builder()
|
|
.status(StatusCode::NOT_FOUND)
|
|
.body(Body::from("application custom 404"))
|
|
.unwrap();
|
|
let response = intercept_route_origin_error(response, route, &snapshot);
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
assert!(response
|
|
.extensions()
|
|
.get::<crate::pages::OriginProvidedResponse>()
|
|
.is_some());
|
|
assert!(!should_replace_error_page(&response, false));
|
|
assert_eq!(
|
|
response
|
|
.extensions()
|
|
.get::<crate::pages::PageServed>()
|
|
.is_some(),
|
|
enabled
|
|
);
|
|
if enabled {
|
|
assert_eq!(response.headers().get("cache-control").unwrap(), "no-store");
|
|
}
|
|
let bytes = response.into_body().collect().await.unwrap().to_bytes();
|
|
if enabled {
|
|
assert_eq!(bytes, "RustProxy custom error page");
|
|
} else {
|
|
assert_eq!(bytes, "application custom 404");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod graceful_h2_tests {
|
|
use super::*;
|
|
use std::fmt;
|
|
|
|
#[derive(Debug)]
|
|
struct H2DebugError(&'static str);
|
|
impl fmt::Display for H2DebugError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "client error (SendRequest): {}", self.0)
|
|
}
|
|
}
|
|
impl std::error::Error for H2DebugError {}
|
|
|
|
#[test]
|
|
fn only_graceful_remote_h2_goaway_is_neutral() {
|
|
let valid = H2DebugError("GoAway(b\"\", NO_ERROR, Remote)");
|
|
let protocol = H2DebugError("GoAway(b\"\", PROTOCOL_ERROR, Remote)");
|
|
let local = H2DebugError("GoAway(b\"\", NO_ERROR, Local)");
|
|
assert!(is_graceful_h2_goaway(&valid));
|
|
assert!(!is_graceful_h2_goaway(&protocol));
|
|
assert!(!is_graceful_h2_goaway(&local));
|
|
}
|
|
}
|