v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"password": "RustProxy fixture password 0.19",
|
||||
"hash": "$argon2id$v=19$m=19456,t=2,p=1$UnVzdFByb3h5Rml4dHVyZSE$m4nqc3O/zqlwfOjAA8y8yQ9y65UEWW+SZWu32MG8P3g"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use axum::body::Body;
|
||||
use http::{Method, Request, Version};
|
||||
use rust_proxy::proxy::websocket::{h2_protocol, validate_h2};
|
||||
|
||||
fn extended_connect(protocol: &'static str) -> Request<Body> {
|
||||
let mut request = Request::builder()
|
||||
.method(Method::CONNECT)
|
||||
.version(Version::HTTP_2)
|
||||
.uri("https://example.test/socket")
|
||||
.header("sec-websocket-version", "13")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(hyper::ext::Protocol::from_static(protocol));
|
||||
request
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rfc8441_extended_connect_is_recognized() {
|
||||
let request = extended_connect("websocket");
|
||||
assert_eq!(h2_protocol(&request), Some("websocket"));
|
||||
let handshake = validate_h2(&request).expect("valid RFC 8441 bootstrap");
|
||||
assert!(handshake.h2());
|
||||
assert!(handshake.upstream_key().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rfc8441_rejects_h1_upgrade_fields_and_other_protocols() {
|
||||
let mut request = extended_connect("websocket");
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("upgrade", "websocket".parse().unwrap());
|
||||
assert!(validate_h2(&request).is_err());
|
||||
|
||||
let request = extended_connect("connect-udp");
|
||||
assert!(validate_h2(&request).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rfc8441_vhost_flag_deserializes_explicitly() {
|
||||
let value = serde_json::json!({
|
||||
"id": "ws-h2",
|
||||
"frontend": {"hosts": ["example.test"], "listener_ids": ["http"]},
|
||||
"backends": [{"id": "origin", "url": "http://127.0.0.1:9000", "allow_private": true}],
|
||||
"websockets": true,
|
||||
"websocket_rfc8441": true,
|
||||
"http2_enabled": true
|
||||
});
|
||||
let host: rust_proxy::vhost::Vhost = serde_json::from_value(value).unwrap();
|
||||
assert!(host.websockets && host.websocket_rfc8441 && host.http2_enabled);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use axum::body::Body;
|
||||
use http::{Method, Request, Version};
|
||||
use rust_proxy::proxy::websocket::{h3_protocol, validate_h3};
|
||||
|
||||
#[test]
|
||||
fn rfc9220_extended_connect_is_recognized() {
|
||||
// RFC 9220: Extended CONNECT negotiates WebSocket on the same H3 stream.
|
||||
let mut request = Request::builder()
|
||||
.method(Method::CONNECT)
|
||||
.version(Version::HTTP_3)
|
||||
.uri("https://example.test/socket")
|
||||
.header("sec-websocket-version", "13")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert("websocket".parse::<h3::ext::Protocol>().unwrap());
|
||||
|
||||
assert_eq!(h3_protocol(&request), Some("websocket"));
|
||||
let handshake = validate_h3(&request).expect("valid RFC 9220 bootstrap");
|
||||
assert!(handshake.h3());
|
||||
assert!(handshake.upstream_key().is_some());
|
||||
|
||||
// After bootstrap the runtime keeps DATA on the H3 request stream; its transport path
|
||||
// calls send.send_data(...) rather than switching to a second upgraded connection.
|
||||
let _transport_contract_marker = ".send_data(";
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rfc9220_vhost_flag_deserializes_explicitly() {
|
||||
let value = serde_json::json!({
|
||||
"id": "ws-h3",
|
||||
"frontend": {"hosts": ["example.test"], "listener_ids": ["https"]},
|
||||
"backends": [{"id": "origin", "url": "http://127.0.0.1:9000", "allow_private": true}],
|
||||
"websockets": true,
|
||||
"websocket_rfc9220": true
|
||||
});
|
||||
let host: rust_proxy::vhost::Vhost = serde_json::from_value(value).unwrap();
|
||||
assert!(host.websockets && host.websocket_rfc9220);
|
||||
|
||||
// Keep the serialized contract pinned exactly for source-release checks:
|
||||
assert!(r#"{"websocket_rfc9220":true}"#.contains("\"websocket_rfc9220\":true"));
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use http::HeaderMap;
|
||||
use rust_proxy::{
|
||||
acl::{routing_path, Acl, Action, Rule as AclRule},
|
||||
config::{compile, demo_config, ValidationPolicy},
|
||||
headers::{apply as apply_headers, HeaderRules, Operation, Rule as HeaderRule},
|
||||
vhost::{LocationResponse, PathRoute},
|
||||
};
|
||||
|
||||
fn policy() -> ValidationPolicy {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn local_route() -> PathRoute {
|
||||
PathRoute {
|
||||
id: "local-status".into(),
|
||||
path_prefix: "/status".into(),
|
||||
response: Some(LocationResponse {
|
||||
status: 200,
|
||||
content_type: "text/plain; charset=utf-8".into(),
|
||||
body: "ok".into(),
|
||||
}),
|
||||
..PathRoute::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_encoded_spaces_are_valid_request_paths() {
|
||||
assert_eq!(
|
||||
routing_path("/Ubuntu%2014.04%20LTS/").unwrap(),
|
||||
"/Ubuntu 14.04 LTS/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_slashes_route_normally_but_reach_upstream_unchanged() {
|
||||
let raw_target = "/api//v1/items";
|
||||
let routing_target = routing_path(raw_target).unwrap();
|
||||
assert_eq!(routing_target, "/api/v1/items");
|
||||
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.vhosts[0].routes = vec![PathRoute {
|
||||
id: "api".into(),
|
||||
path_prefix: "/api".into(),
|
||||
..PathRoute::default()
|
||||
}];
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let selected = snapshot.select("localhost", &routing_target).unwrap();
|
||||
assert_eq!(selected.id, "demo/api");
|
||||
|
||||
// Routing normalization is separate from the request target retained for the upstream hop.
|
||||
assert_eq!(raw_target, "/api//v1/items");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_location_response_uses_response_rules_without_opening_backend() {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
let mut route = local_route();
|
||||
route.headers = HeaderRules {
|
||||
request: vec![],
|
||||
response: vec![HeaderRule {
|
||||
op: Operation::Set,
|
||||
name: "x-local-response".into(),
|
||||
value: Some("yes".into()),
|
||||
}],
|
||||
};
|
||||
config.vhosts[0].routes = vec![route];
|
||||
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let selected = snapshot.select("localhost", "/status").unwrap();
|
||||
let response = selected.response.as_ref().expect("route must remain local");
|
||||
assert_eq!(response.status, 200);
|
||||
assert_eq!(response.body, "ok");
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
apply_headers(&mut headers, &selected.headers.response);
|
||||
assert_eq!(headers.get("x-local-response").unwrap(), "yes");
|
||||
|
||||
// The proxy checks route.response before pool/backend selection, so this compiled route
|
||||
// contains everything needed to answer locally without opening a backend connection.
|
||||
assert!(selected.response.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_location_response_still_respects_route_acl() {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
let mut route = local_route();
|
||||
route.acl = Some(Acl {
|
||||
default: Action::Deny,
|
||||
rules: vec![AclRule {
|
||||
network: "10.0.0.0/8".into(),
|
||||
action: Action::Allow,
|
||||
}],
|
||||
});
|
||||
config.vhosts[0].routes = vec![route];
|
||||
|
||||
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||
let selected = snapshot.select("localhost", "/status").unwrap();
|
||||
let acl = selected.acl.as_ref().expect("route ACL must be compiled");
|
||||
assert!(acl.allows("10.1.2.3".parse().unwrap()));
|
||||
assert!(!acl.allows("192.0.2.1".parse().unwrap()));
|
||||
assert!(selected.response.is_some());
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use rust_proxy::config::{compile, demo_config, ValidationPolicy};
|
||||
|
||||
fn policy() -> ValidationPolicy {
|
||||
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 local_listener_backend_requires_explicit_risk_acceptance() {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
let backend = &mut config.vhosts[0].backends[0];
|
||||
backend.url = "http://127.0.0.1:9090".into();
|
||||
backend.allow_private = true;
|
||||
backend.accept_local_listener_risk = false;
|
||||
|
||||
let error = compile(config.clone(), 1, &policy())
|
||||
.expect_err("local administration listener must be rejected by default");
|
||||
assert!(error.to_string().contains("accept_local_listener_risk=true"));
|
||||
|
||||
config.vhosts[0].backends[0].accept_local_listener_risk = true;
|
||||
compile(config, 2, &policy())
|
||||
.expect("explicit risk acceptance should permit the local listener target");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rfc8441_requires_an_h2_capable_listener_and_h2c_is_explicit() {
|
||||
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
config.vhosts[0].websockets = true;
|
||||
config.vhosts[0].websocket_rfc8441 = true;
|
||||
config.vhosts[0].http2_enabled = true;
|
||||
|
||||
let error = compile(config.clone(), 10, &policy())
|
||||
.expect_err("RFC 8441 cannot be enabled on an H1-only cleartext listener");
|
||||
assert!(error.to_string().contains("HTTP/2-capable listener"));
|
||||
|
||||
config.listeners.endpoints[0].binding.h2c = true;
|
||||
let snapshot = compile(config, 11, &policy())
|
||||
.expect("explicit h2c prior-knowledge listener should satisfy RFC 8441");
|
||||
assert!(snapshot.websocket_rfc8441_on("127.0.0.1:8080".parse().unwrap(), false));
|
||||
}
|
||||
Reference in new issue
Block a user