mirror of
https://github.com/telemt/telemt.git
synced 2026-10-29 03:57:15 +03:00
WEB: Base Path: security + reload coverage fixes&tests
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
use super::*;
|
||||
|
||||
#[path = "base_path_tests/credentials.rs"]
|
||||
mod credentials;
|
||||
#[path = "base_path_tests/reload.rs"]
|
||||
mod reload;
|
||||
#[path = "base_path_tests/routing.rs"]
|
||||
mod routing;
|
||||
|
||||
fn bridge_request(path: &str) -> Vec<u8> {
|
||||
format!(
|
||||
"GET {path} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n"
|
||||
@@ -65,9 +72,11 @@ async fn base_path_routes_only_the_exact_prefixed_contract() {
|
||||
.await;
|
||||
let (headers, body) = split_response(&response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
assert!(std::str::from_utf8(body)
|
||||
.unwrap()
|
||||
.contains("relayBase=relayOrigin+'/dobry-cola'"));
|
||||
assert!(
|
||||
std::str::from_utf8(body)
|
||||
.unwrap()
|
||||
.contains("relayBase=relayOrigin+'/dobry-cola'")
|
||||
);
|
||||
|
||||
for path in [
|
||||
format!("/?bridge={encoded}"),
|
||||
@@ -310,8 +319,7 @@ async fn process_token_provenance_survives_registry_expiry() {
|
||||
#[tokio::test]
|
||||
async fn generation_swap_switches_base_path_and_capability_together() {
|
||||
let initial_capability = [26u8; 32];
|
||||
let mut initial =
|
||||
runtime_config_with_base(initial_capability, WebCarrier::Https, "/old-path/");
|
||||
let mut initial = runtime_config_with_base(initial_capability, WebCarrier::Https, "/old-path/");
|
||||
initial.web.limits.max_bootstraps_per_ip = 2;
|
||||
let generation = test_runtime_generation(1, initial);
|
||||
let active = Arc::new(ArcSwap::from(Arc::clone(&generation)));
|
||||
@@ -331,11 +339,7 @@ async fn generation_swap_switches_base_path_and_capability_together() {
|
||||
let replacement_capability = [27u8; 32];
|
||||
let replacement = test_runtime_generation(
|
||||
2,
|
||||
runtime_config_with_base(
|
||||
replacement_capability,
|
||||
WebCarrier::Https,
|
||||
"/new-path/",
|
||||
),
|
||||
runtime_config_with_base(replacement_capability, WebCarrier::Https, "/new-path/"),
|
||||
);
|
||||
active.store(Arc::clone(&replacement));
|
||||
let stale = request(
|
||||
@@ -359,9 +363,11 @@ async fn generation_swap_switches_base_path_and_capability_together() {
|
||||
.await;
|
||||
let (headers, body) = split_response(¤t);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
assert!(std::str::from_utf8(body)
|
||||
.unwrap()
|
||||
.contains("relayBase=relayOrigin+'/new-path'"));
|
||||
assert!(
|
||||
std::str::from_utf8(body)
|
||||
.unwrap()
|
||||
.contains("relayBase=relayOrigin+'/new-path'")
|
||||
);
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
@@ -414,12 +420,7 @@ async fn prefixed_decoy_request_keeps_its_original_path_and_query() {
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
|
||||
let response = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request("/relay/ordinary?q=1"),
|
||||
)
|
||||
.await;
|
||||
let response = request(&listener, &runtime, bridge_request("/relay/ordinary?q=1")).await;
|
||||
assert!(response.starts_with(b"HTTP/1.1 404"));
|
||||
assert_eq!(split_response(&response).1, b"site");
|
||||
let forwarded = site_task.await.unwrap();
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
use super::*;
|
||||
|
||||
fn assert_private_not_found(response: &[u8]) {
|
||||
let (headers, body) = split_response(response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 404"));
|
||||
assert_eq!(response_header(headers, "cache-control"), "no-store");
|
||||
assert_eq!(body, b"not found\n");
|
||||
}
|
||||
|
||||
fn percent_encode(value: &str) -> String {
|
||||
value
|
||||
.bytes()
|
||||
.enumerate()
|
||||
.map(|(index, byte)| {
|
||||
if index % 2 == 0 {
|
||||
format!("%{byte:02x}")
|
||||
} else {
|
||||
format!("%{byte:02X}")
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn issue_session_credentials(
|
||||
listener: &TcpListener,
|
||||
runtime: &Arc<WebProcessRuntime>,
|
||||
capability: [u8; 32],
|
||||
) -> (String, String) {
|
||||
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
||||
let bridge = request(
|
||||
listener,
|
||||
runtime,
|
||||
bridge_request(&format!("/relay/?bridge={encoded}")),
|
||||
)
|
||||
.await;
|
||||
let bootstrap = bridge_token(split_response(&bridge).1);
|
||||
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
||||
let mut create = format!(
|
||||
"POST /relay/api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
hello.len()
|
||||
)
|
||||
.into_bytes();
|
||||
create.extend_from_slice(&hello);
|
||||
let created = request(listener, runtime, create).await;
|
||||
let (headers, _) = split_response(&created);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
(
|
||||
bootstrap,
|
||||
response_header(headers, "x-session-token").to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn every_authentic_credential_placement_stays_out_of_the_upstream() {
|
||||
let site = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let site_addr = site.local_addr().unwrap();
|
||||
let capability = [91u8; 32];
|
||||
let mut config = runtime_config_with_base(capability, WebCarrier::Https, "/relay/");
|
||||
config.web.limits.max_bootstraps_per_ip = 2;
|
||||
let runtime_config = config.web.runtime.as_ref().unwrap();
|
||||
let profile = runtime_config.profiles[0].clone();
|
||||
let mut vhosts = BTreeMap::new();
|
||||
vhosts.insert(
|
||||
"proxy.example.com".to_string(),
|
||||
Arc::new(WebRuntimeVhost {
|
||||
host: "proxy.example.com".to_string(),
|
||||
base: "/relay/".to_string(),
|
||||
decoy_fasttrack_mode: WebDecoyFastTrackMode::Off,
|
||||
decoy: WebRuntimeDecoy::HttpUpstream {
|
||||
addr: site_addr,
|
||||
authority: "decoy.internal".to_string(),
|
||||
},
|
||||
decoy_header_secs: 1,
|
||||
profiles: vec![profile.clone()],
|
||||
capabilities: vec![capability].into_boxed_slice(),
|
||||
}),
|
||||
);
|
||||
vhosts.insert(
|
||||
"other.example.com".to_string(),
|
||||
Arc::new(WebRuntimeVhost {
|
||||
host: "other.example.com".to_string(),
|
||||
base: "/other/".to_string(),
|
||||
decoy_fasttrack_mode: WebDecoyFastTrackMode::Off,
|
||||
decoy: WebRuntimeDecoy::HttpUpstream {
|
||||
addr: site_addr,
|
||||
authority: "decoy.internal".to_string(),
|
||||
},
|
||||
decoy_header_secs: 1,
|
||||
profiles: Vec::new(),
|
||||
capabilities: Vec::new().into_boxed_slice(),
|
||||
}),
|
||||
);
|
||||
config.web.runtime = Some(Arc::new(WebRuntimeConfig {
|
||||
vhosts,
|
||||
profiles: vec![profile],
|
||||
capabilities: vec![capability].into_boxed_slice(),
|
||||
}));
|
||||
let generation = test_runtime_generation(1, config);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let encoded_capability = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
||||
let (bootstrap, session) = issue_session_credentials(&listener, &runtime, capability).await;
|
||||
|
||||
for secret in [&encoded_capability, &bootstrap, &session] {
|
||||
for raw in [
|
||||
format!(
|
||||
"GET /wrong/{secret} HTTP/1.1\r\nHost: proxy.example.com\r\nConnection: close\r\n\r\n"
|
||||
),
|
||||
format!(
|
||||
"GET /wrong/{} HTTP/1.1\r\nHost: proxy.example.com\r\nConnection: close\r\n\r\n",
|
||||
percent_encode(secret)
|
||||
),
|
||||
format!(
|
||||
"GET /?bridge={secret}&extra=1 HTTP/1.1\r\nHost: proxy.example.com\r\nConnection: close\r\n\r\n"
|
||||
),
|
||||
format!(
|
||||
"POST /wrong HTTP/1.1\r\nHost: proxy.example.com\r\nAuthorization: Bearer {secret}\r\nCookie: opaque={secret}\r\nReferer: https://example.invalid/{secret}\r\nX-Unexpected: random,{secret}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
),
|
||||
format!(
|
||||
"GET /api/v1/ws HTTP/1.1\r\nHost: proxy.example.com\r\nSec-WebSocket-Protocol: chat, tproxy-v1.{secret}\r\nConnection: close\r\n\r\n"
|
||||
),
|
||||
format!(
|
||||
"GET /other/wrong HTTP/1.1\r\nHost: other.example.com\r\nX-Unexpected: {secret}\r\nConnection: close\r\n\r\n"
|
||||
),
|
||||
] {
|
||||
assert_private_not_found(&request(&listener, &runtime, raw.into_bytes()).await);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(std::time::Duration::from_millis(100), site.accept())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let site_task = tokio::spawn(async move {
|
||||
let (mut stream, _) = site.accept().await.unwrap();
|
||||
let mut received = [0; 1024];
|
||||
let _ = stream.read(&mut received).await.unwrap();
|
||||
stream
|
||||
.write_all(
|
||||
b"HTTP/1.1 404 Not Found\r\nContent-Length: 4\r\nConnection: close\r\n\r\nsite",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
let mut forged = bootstrap.into_bytes();
|
||||
forged[0] = if forged[0] == b'A' { b'B' } else { b'A' };
|
||||
let forged = String::from_utf8(forged).unwrap();
|
||||
let response = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request(&format!("/wrong/{forged}")),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(split_response(&response).1, b"site");
|
||||
site_task.await.unwrap();
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
generation.stop_background_tasks().await;
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
use super::*;
|
||||
|
||||
async fn issue_bootstrap(
|
||||
listener: &TcpListener,
|
||||
runtime: &Arc<WebProcessRuntime>,
|
||||
base: &str,
|
||||
capability: [u8; 32],
|
||||
) -> String {
|
||||
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
||||
let response = request(
|
||||
listener,
|
||||
runtime,
|
||||
bridge_request(&format!("{base}?bridge={encoded}")),
|
||||
)
|
||||
.await;
|
||||
let (headers, body) = split_response(&response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
bridge_token(body)
|
||||
}
|
||||
|
||||
async fn create_session(
|
||||
listener: &TcpListener,
|
||||
runtime: &Arc<WebProcessRuntime>,
|
||||
base: &str,
|
||||
bootstrap: &str,
|
||||
) -> String {
|
||||
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
||||
let mut request_bytes = format!(
|
||||
"POST {base}api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
hello.len()
|
||||
)
|
||||
.into_bytes();
|
||||
request_bytes.extend_from_slice(&hello);
|
||||
let response = request(listener, runtime, request_bytes).await;
|
||||
let (headers, _) = split_response(&response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
response_header(headers, "x-session-token").to_string()
|
||||
}
|
||||
|
||||
fn session_request(base: &str, session: &str) -> Vec<u8> {
|
||||
format!(
|
||||
"POST {base}api/v1/down HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nX-Down-Cursor: 0\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
fn assert_private_not_found(response: &[u8]) {
|
||||
let (headers, body) = split_response(response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 404"));
|
||||
assert_eq!(response_header(headers, "cache-control"), "no-store");
|
||||
assert_eq!(body, b"not found\n");
|
||||
}
|
||||
|
||||
fn assert_decoy(response: &[u8]) {
|
||||
assert_eq!(
|
||||
split_response(response).1,
|
||||
b"<!doctype html><title>decoy</title>"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generation_swap_preserves_process_tokens_and_replaces_route_identity() {
|
||||
let old_capability = [101u8; 32];
|
||||
let mut old_config = runtime_config_with_base(old_capability, WebCarrier::Https, "/old/");
|
||||
old_config.web.limits.max_bootstraps_per_ip = 8;
|
||||
old_config.web.timeouts.long_poll_secs = 1;
|
||||
let old_generation = test_runtime_generation(1, old_config);
|
||||
let active = Arc::new(ArcSwap::from(Arc::clone(&old_generation)));
|
||||
let runtime = WebProcessRuntime::start(Arc::clone(&active));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let unused_bootstrap = issue_bootstrap(&listener, &runtime, "/old/", old_capability).await;
|
||||
let used_bootstrap = issue_bootstrap(&listener, &runtime, "/old/", old_capability).await;
|
||||
let session = create_session(&listener, &runtime, "/old/", &used_bootstrap).await;
|
||||
|
||||
let new_capability = [102u8; 32];
|
||||
let mut new_config = runtime_config_with_base(new_capability, WebCarrier::Https, "/new/");
|
||||
new_config.web.limits.max_bootstraps_per_ip = 8;
|
||||
new_config.web.timeouts.long_poll_secs = 1;
|
||||
let new_generation = test_runtime_generation(2, new_config);
|
||||
active.store(Arc::clone(&new_generation));
|
||||
let old_encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(old_capability);
|
||||
let new_encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(new_capability);
|
||||
|
||||
assert_decoy(
|
||||
&request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request(&format!("/old/?bridge={old_encoded}")),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
assert_private_not_found(
|
||||
&request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request(&format!("/old/?bridge={new_encoded}")),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
assert_decoy(
|
||||
&request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request(&format!("/new/?bridge={old_encoded}")),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
let old_bootstrap_path = format!(
|
||||
"GET /old/wrong HTTP/1.1\r\nHost: proxy.example.com\r\nAuthorization: Bearer {unused_bootstrap}\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
.into_bytes();
|
||||
assert_private_not_found(&request(&listener, &runtime, old_bootstrap_path).await);
|
||||
assert_private_not_found(
|
||||
&request(&listener, &runtime, session_request("/old/", &session)).await,
|
||||
);
|
||||
assert!(
|
||||
request(&listener, &runtime, session_request("/new/", &session),)
|
||||
.await
|
||||
.starts_with(b"HTTP/1.1 204")
|
||||
);
|
||||
|
||||
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
||||
let mut stale_bootstrap_request = format!(
|
||||
"POST /new/api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {unused_bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
hello.len()
|
||||
)
|
||||
.into_bytes();
|
||||
stale_bootstrap_request.extend_from_slice(&hello);
|
||||
assert_private_not_found(&request(&listener, &runtime, stale_bootstrap_request).await);
|
||||
|
||||
let current = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request(&format!("/new/?bridge={new_encoded}")),
|
||||
)
|
||||
.await;
|
||||
let (headers, body) = split_response(¤t);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
let new_bootstrap = bridge_token(body);
|
||||
create_session(&listener, &runtime, "/new/", &new_bootstrap).await;
|
||||
|
||||
runtime.shutdown().await;
|
||||
old_generation.stop_sessions().await;
|
||||
old_generation.stop_background_tasks().await;
|
||||
new_generation.stop_sessions().await;
|
||||
new_generation.stop_background_tasks().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn routed_uplink_finishes_under_its_acquisition_generation() {
|
||||
let capability = [103u8; 32];
|
||||
let mut old_config = runtime_config_with_base(capability, WebCarrier::Https, "/old/");
|
||||
old_config.web.timeouts.long_poll_secs = 1;
|
||||
let old_generation = test_runtime_generation(1, old_config);
|
||||
let active = Arc::new(ArcSwap::from(Arc::clone(&old_generation)));
|
||||
let runtime = WebProcessRuntime::start(Arc::clone(&active));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let bootstrap = issue_bootstrap(&listener, &runtime, "/old/", capability).await;
|
||||
let session = create_session(&listener, &runtime, "/old/", &bootstrap).await;
|
||||
|
||||
let address = listener.local_addr().unwrap();
|
||||
let (accepted, client) = tokio::join!(listener.accept(), TcpStream::connect(address));
|
||||
let (server, peer) = accepted.unwrap();
|
||||
let mut client = client.unwrap();
|
||||
let permit = runtime.try_http_connection().unwrap();
|
||||
let task = tokio::spawn(serve_connection(
|
||||
server,
|
||||
peer,
|
||||
WebClientIpSource::XForwardedFor,
|
||||
Arc::from(["127.0.0.1/32".parse().unwrap()]),
|
||||
Arc::clone(&runtime),
|
||||
CancellationToken::new(),
|
||||
permit,
|
||||
));
|
||||
let pong = frame::encode(FrameType::Pong, 0, &[]);
|
||||
let head = format!(
|
||||
"POST /old/api/v1/up HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Type: application/octet-stream\r\nX-Up-Seq: 1\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
pong.len()
|
||||
);
|
||||
client.write_all(head.as_bytes()).await.unwrap();
|
||||
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
loop {
|
||||
let body_readers = runtime
|
||||
.capacity_snapshot()
|
||||
.resources
|
||||
.into_iter()
|
||||
.find(|resource| resource.resource == "body_readers")
|
||||
.unwrap();
|
||||
if body_readers.used == 1 {
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("uplink never entered body collection");
|
||||
|
||||
let mut new_config = runtime_config_with_base([104u8; 32], WebCarrier::Https, "/new/");
|
||||
new_config.web.timeouts.long_poll_secs = 1;
|
||||
let new_generation = test_runtime_generation(2, new_config);
|
||||
active.store(Arc::clone(&new_generation));
|
||||
client.write_all(&pong).await.unwrap();
|
||||
let mut response = Vec::new();
|
||||
client.read_to_end(&mut response).await.unwrap();
|
||||
task.await.unwrap();
|
||||
assert!(response.starts_with(b"HTTP/1.1 204"));
|
||||
|
||||
assert_private_not_found(
|
||||
&request(&listener, &runtime, session_request("/old/", &session)).await,
|
||||
);
|
||||
assert!(
|
||||
request(&listener, &runtime, session_request("/new/", &session),)
|
||||
.await
|
||||
.starts_with(b"HTTP/1.1 204")
|
||||
);
|
||||
|
||||
runtime.shutdown().await;
|
||||
old_generation.stop_sessions().await;
|
||||
old_generation.stop_background_tasks().await;
|
||||
new_generation.stop_sessions().await;
|
||||
new_generation.stop_background_tasks().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generation_transition_burst_never_authenticates_a_torn_route_identity() {
|
||||
let old_capability = [105u8; 32];
|
||||
let new_capability = [106u8; 32];
|
||||
let old_generation = test_runtime_generation(
|
||||
1,
|
||||
runtime_config_with_base(old_capability, WebCarrier::Https, "/old/"),
|
||||
);
|
||||
let new_generation = test_runtime_generation(
|
||||
2,
|
||||
runtime_config_with_base(new_capability, WebCarrier::Https, "/new/"),
|
||||
);
|
||||
let active = Arc::new(ArcSwap::from(Arc::clone(&old_generation)));
|
||||
let runtime = WebProcessRuntime::start(Arc::clone(&active));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let barrier = Arc::new(tokio::sync::Barrier::new(2));
|
||||
let swap_barrier = Arc::clone(&barrier);
|
||||
let swap_active = Arc::clone(&active);
|
||||
let swap_old = Arc::clone(&old_generation);
|
||||
let swap_new = Arc::clone(&new_generation);
|
||||
let swaps = tokio::spawn(async move {
|
||||
swap_barrier.wait().await;
|
||||
for index in 0..10_000 {
|
||||
if index % 2 == 0 {
|
||||
swap_active.store(Arc::clone(&swap_new));
|
||||
} else {
|
||||
swap_active.store(Arc::clone(&swap_old));
|
||||
}
|
||||
if index % 8 == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
barrier.wait().await;
|
||||
let old_encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(old_capability);
|
||||
let new_encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(new_capability);
|
||||
|
||||
for index in 0..256 {
|
||||
let target = if index % 2 == 0 {
|
||||
format!("/old/?bridge={new_encoded}")
|
||||
} else {
|
||||
format!("/new/?bridge={old_encoded}")
|
||||
};
|
||||
let response = request(&listener, &runtime, bridge_request(&target)).await;
|
||||
assert!(!response.starts_with(b"HTTP/1.1 200"));
|
||||
assert!(
|
||||
!std::str::from_utf8(split_response(&response).1)
|
||||
.unwrap()
|
||||
.contains("bootstrap=\"")
|
||||
);
|
||||
}
|
||||
swaps.await.unwrap();
|
||||
|
||||
runtime.shutdown().await;
|
||||
old_generation.stop_sessions().await;
|
||||
old_generation.stop_background_tasks().await;
|
||||
new_generation.stop_sessions().await;
|
||||
new_generation.stop_background_tasks().await;
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
use super::*;
|
||||
|
||||
const RECOVERY_TYPE: &str = "application/vnd.telemt.web-recovery+json";
|
||||
|
||||
async fn issue_bootstrap(
|
||||
listener: &TcpListener,
|
||||
runtime: &Arc<WebProcessRuntime>,
|
||||
path: &str,
|
||||
capability: [u8; 32],
|
||||
) -> String {
|
||||
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
||||
let response = request(
|
||||
listener,
|
||||
runtime,
|
||||
bridge_request(&format!("{path}?bridge={encoded}")),
|
||||
)
|
||||
.await;
|
||||
let (headers, body) = split_response(&response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
bridge_token(body)
|
||||
}
|
||||
|
||||
async fn create_session(
|
||||
listener: &TcpListener,
|
||||
runtime: &Arc<WebProcessRuntime>,
|
||||
path: &str,
|
||||
bootstrap: &str,
|
||||
) -> String {
|
||||
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
||||
let mut request_bytes = format!(
|
||||
"POST {path} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
hello.len()
|
||||
)
|
||||
.into_bytes();
|
||||
request_bytes.extend_from_slice(&hello);
|
||||
let response = request(listener, runtime, request_bytes).await;
|
||||
let (headers, _) = split_response(&response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
response_header(headers, "x-session-token").to_string()
|
||||
}
|
||||
|
||||
fn assert_private_not_found(response: &[u8]) {
|
||||
let (headers, body) = split_response(response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 404"));
|
||||
assert_eq!(response_header(headers, "cache-control"), "no-store");
|
||||
assert_eq!(body, b"not found\n");
|
||||
}
|
||||
|
||||
fn down_request(path: &str, session: &str) -> Vec<u8> {
|
||||
format!(
|
||||
"POST {path} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nX-Down-Cursor: 0\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_token_distinguishes_the_exact_route_from_every_path_alias() {
|
||||
let capability = [81u8; 32];
|
||||
let mut config = runtime_config_with_base(capability, WebCarrier::Https, "/relay/");
|
||||
config.web.timeouts.long_poll_secs = 1;
|
||||
let generation = test_runtime_generation(1, config);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let bootstrap = issue_bootstrap(&listener, &runtime, "/relay/", capability).await;
|
||||
let session = create_session(&listener, &runtime, "/relay/api/v1/session", &bootstrap).await;
|
||||
|
||||
let exact = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
down_request("/relay/api/v1/down", &session),
|
||||
)
|
||||
.await;
|
||||
assert!(exact.starts_with(b"HTTP/1.1 204"));
|
||||
|
||||
for alias in [
|
||||
"/api/v1/down",
|
||||
"/Relay/api/v1/down",
|
||||
"/relayx/api/v1/down",
|
||||
"/relay//api/v1/down",
|
||||
"/relay%2Fapi/v1/down",
|
||||
"/relay/api%2Fv1/down",
|
||||
"/relay/api/v1/down/",
|
||||
"/relay/api/v1/down?q=1",
|
||||
] {
|
||||
let response = request(&listener, &runtime, down_request(alias, &session)).await;
|
||||
assert_private_not_found(&response);
|
||||
}
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
generation.stop_background_tasks().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inactive_capabilities_remain_decoy_and_the_active_path_is_case_sensitive() {
|
||||
let active_capability = [82u8; 32];
|
||||
let inactive_capability = [83u8; 32];
|
||||
let generation = test_runtime_generation(
|
||||
1,
|
||||
runtime_config_with_base(
|
||||
active_capability,
|
||||
WebCarrier::Https,
|
||||
"/Dobry-Cola/super_app/",
|
||||
),
|
||||
);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let active = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(active_capability);
|
||||
let inactive = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(inactive_capability);
|
||||
|
||||
let exact = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request(&format!("/Dobry-Cola/super_app/?bridge={active}")),
|
||||
)
|
||||
.await;
|
||||
assert!(exact.starts_with(b"HTTP/1.1 200"));
|
||||
|
||||
for path in [
|
||||
format!("/dobry-cola/super_app/?bridge={active}"),
|
||||
format!("/Dobry-Cola/super_app?bridge={active}"),
|
||||
format!("/Dobry-Cola/super_app/nested?bridge={active}"),
|
||||
] {
|
||||
assert_private_not_found(&request(&listener, &runtime, bridge_request(&path)).await);
|
||||
}
|
||||
|
||||
let decoy = request(
|
||||
&listener,
|
||||
&runtime,
|
||||
bridge_request(&format!("/Dobry-Cola/super_app/?bridge={inactive}")),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
split_response(&decoy).1,
|
||||
b"<!doctype html><title>decoy</title>"
|
||||
);
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
generation.stop_background_tasks().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn decoy_forwarding_preserves_every_reference_request_target() {
|
||||
let targets = [
|
||||
"/relay/",
|
||||
"/relay/whatever?q=1",
|
||||
"/relay/api/v1/session",
|
||||
"/relay",
|
||||
"/api/v1/ws",
|
||||
"/relay//api/v1/up",
|
||||
"/relay%2Fapi/v1/up",
|
||||
"/Relay/api/v1/down",
|
||||
];
|
||||
let site = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let site_addr = site.local_addr().unwrap();
|
||||
let site_task = tokio::spawn(async move {
|
||||
let mut request_targets = Vec::new();
|
||||
for _ in 0..targets.len() {
|
||||
let (mut stream, _) = site.accept().await.unwrap();
|
||||
let mut received = Vec::new();
|
||||
loop {
|
||||
let mut chunk = [0; 1024];
|
||||
let read = stream.read(&mut chunk).await.unwrap();
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
received.extend_from_slice(&chunk[..read]);
|
||||
if received.windows(4).any(|window| window == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let line = std::str::from_utf8(&received)
|
||||
.unwrap()
|
||||
.split("\r\n")
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
request_targets.push(line);
|
||||
stream
|
||||
.write_all(
|
||||
b"HTTP/1.1 404 Not Found\r\nContent-Length: 4\r\nConnection: close\r\n\r\nsite",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
request_targets
|
||||
});
|
||||
|
||||
let capability = [84u8; 32];
|
||||
let mut config = runtime_config_with_base(capability, WebCarrier::Https, "/relay/");
|
||||
let runtime_config = config.web.runtime.as_ref().unwrap();
|
||||
let mut vhosts = runtime_config.vhosts.clone();
|
||||
let previous = &runtime_config.vhosts["proxy.example.com"];
|
||||
vhosts.insert(
|
||||
"proxy.example.com".to_string(),
|
||||
Arc::new(WebRuntimeVhost {
|
||||
host: previous.host.clone(),
|
||||
base: previous.base.clone(),
|
||||
decoy_fasttrack_mode: previous.decoy_fasttrack_mode,
|
||||
decoy: WebRuntimeDecoy::HttpUpstream {
|
||||
addr: site_addr,
|
||||
authority: "decoy.internal".to_string(),
|
||||
},
|
||||
decoy_header_secs: 1,
|
||||
profiles: previous.profiles.clone(),
|
||||
capabilities: previous.capabilities.clone(),
|
||||
}),
|
||||
);
|
||||
config.web.runtime = Some(Arc::new(WebRuntimeConfig {
|
||||
vhosts,
|
||||
profiles: runtime_config.profiles.clone(),
|
||||
capabilities: runtime_config.capabilities.clone(),
|
||||
}));
|
||||
let generation = test_runtime_generation(1, config);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
|
||||
for target in targets {
|
||||
let response = request(&listener, &runtime, bridge_request(target)).await;
|
||||
assert_eq!(split_response(&response).1, b"site");
|
||||
}
|
||||
let forwarded = site_task.await.unwrap();
|
||||
let expected = targets
|
||||
.into_iter()
|
||||
.map(|target| format!("GET {target} HTTP/1.1"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(forwarded, expected);
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
generation.stop_background_tasks().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn diagnostic_and_recovery_use_only_the_exact_prefixed_root() {
|
||||
let capability = [85u8; 32];
|
||||
let mut config = runtime_config_with_base(capability, WebCarrier::Https, "/relay/");
|
||||
config.web.debug.enabled = true;
|
||||
config.web.debug.sideband = true;
|
||||
let generation = test_runtime_generation(1, config);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
||||
let bootstrap = issue_bootstrap(&listener, &runtime, "/relay/", capability).await;
|
||||
|
||||
let body = br#"{"v":1,"event":"runtime_started"}"#;
|
||||
let mut diagnostic = format!(
|
||||
"POST /relay/api/v1/diagnostic HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
)
|
||||
.into_bytes();
|
||||
diagnostic.extend_from_slice(body);
|
||||
assert!(
|
||||
request(&listener, &runtime, diagnostic)
|
||||
.await
|
||||
.starts_with(b"HTTP/1.1 204")
|
||||
);
|
||||
|
||||
let root_diagnostic = format!(
|
||||
"POST /api/v1/diagnostic HTTP/1.1\r\nHost: proxy.example.com\r\nAuthorization: Bearer {bootstrap}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
.into_bytes();
|
||||
assert_private_not_found(&request(&listener, &runtime, root_diagnostic).await);
|
||||
|
||||
let session = create_session(&listener, &runtime, "/relay/api/v1/session", &bootstrap).await;
|
||||
let recovery_request = |path: &str| {
|
||||
format!(
|
||||
"GET {path}?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAccept: {RECOVERY_TYPE}\r\nAuthorization: Bearer {session}\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
.into_bytes()
|
||||
};
|
||||
assert_private_not_found(&request(&listener, &runtime, recovery_request("/")).await);
|
||||
let recovered = request(&listener, &runtime, recovery_request("/relay/")).await;
|
||||
let (headers, body) = split_response(&recovered);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 200"));
|
||||
assert_eq!(response_header(headers, "content-type"), RECOVERY_TYPE);
|
||||
let document: serde_json::Value = serde_json::from_slice(body).unwrap();
|
||||
let recovery_bootstrap = document["bootstrap"].as_str().unwrap();
|
||||
create_session(
|
||||
&listener,
|
||||
&runtime,
|
||||
"/relay/api/v1/session",
|
||||
recovery_bootstrap,
|
||||
)
|
||||
.await;
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
generation.stop_background_tasks().await;
|
||||
}
|
||||
@@ -211,7 +211,10 @@ async fn malformed_or_over_capacity_recovery_is_indistinguishable_from_decoy() {
|
||||
.await;
|
||||
let (invalid_headers, invalid_body) = split_response(&invalid_capability);
|
||||
assert!(invalid_headers.starts_with(b"HTTP/1.1 200"));
|
||||
assert_eq!(response_header(invalid_headers, "cache-control"), "no-store");
|
||||
assert_eq!(
|
||||
response_header(invalid_headers, "cache-control"),
|
||||
"no-store"
|
||||
);
|
||||
assert_eq!(invalid_body, b"<!doctype html><title>decoy</title>");
|
||||
|
||||
let malformed_accept = request(
|
||||
|
||||
@@ -18,9 +18,9 @@ pub(super) fn mark_internal_credential<B>(
|
||||
let uri_contains = uri
|
||||
.authority()
|
||||
.is_some_and(|authority| contains_secret(authority.as_str().as_bytes(), config, runtime))
|
||||
|| uri.path_and_query().is_some_and(|path| {
|
||||
contains_secret(path.as_str().as_bytes(), config, runtime)
|
||||
});
|
||||
|| uri
|
||||
.path_and_query()
|
||||
.is_some_and(|path| contains_secret(path.as_str().as_bytes(), config, runtime));
|
||||
let headers_contain = request.headers().iter().any(|(name, value)| {
|
||||
contains_secret(name.as_str().as_bytes(), config, runtime)
|
||||
|| contains_secret(value.as_bytes(), config, runtime)
|
||||
@@ -84,3 +84,7 @@ fn hex_value(value: u8) -> Option<u8> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "secrets/tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use base64::Engine as _;
|
||||
|
||||
use super::*;
|
||||
use crate::config::WebCarrier;
|
||||
use crate::maestro::generation::test_runtime_generation;
|
||||
use crate::web::http::tests::runtime_config_with_base;
|
||||
|
||||
fn percent_encode_byte(value: u8, lowercase: bool) -> String {
|
||||
if lowercase {
|
||||
format!("%{value:02x}")
|
||||
} else {
|
||||
format!("%{value:02X}")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scanner_recognizes_exact_credentials_across_encoding_boundaries() {
|
||||
let capability = [71u8; 32];
|
||||
let generation = test_runtime_generation(
|
||||
1,
|
||||
runtime_config_with_base(capability, WebCarrier::Https, "/relay/"),
|
||||
);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let generation_config = generation.config();
|
||||
let config = generation_config.web.runtime.as_ref().unwrap();
|
||||
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
||||
|
||||
assert!(contains_secret(encoded.as_bytes(), config, &runtime));
|
||||
assert!(contains_secret(
|
||||
format!("prefix{encoded}suffix").as_bytes(),
|
||||
config,
|
||||
&runtime,
|
||||
));
|
||||
for index in 0..encoded.len() {
|
||||
let mut escaped = String::with_capacity(encoded.len() + 2);
|
||||
escaped.push_str(&encoded[..index]);
|
||||
escaped.push_str(&percent_encode_byte(
|
||||
encoded.as_bytes()[index],
|
||||
index % 2 == 0,
|
||||
));
|
||||
escaped.push_str(&encoded[index + 1..]);
|
||||
assert!(
|
||||
contains_secret(escaped.as_bytes(), config, &runtime),
|
||||
"percent-encoded byte {index} was not recognized"
|
||||
);
|
||||
}
|
||||
|
||||
let profile = config.profiles[0].clone();
|
||||
let bootstrap = runtime
|
||||
.issue_bootstrap(profile, "192.0.2.10".parse().unwrap())
|
||||
.unwrap()
|
||||
.token;
|
||||
let escaped_bootstrap = bootstrap
|
||||
.bytes()
|
||||
.enumerate()
|
||||
.map(|(index, byte)| percent_encode_byte(byte, index % 2 == 0))
|
||||
.collect::<String>();
|
||||
assert!(contains_secret(bootstrap.as_bytes(), config, &runtime));
|
||||
assert!(contains_secret(
|
||||
escaped_bootstrap.as_bytes(),
|
||||
config,
|
||||
&runtime,
|
||||
));
|
||||
|
||||
let split = format!("{}%zz{}", &encoded[..20], &encoded[20..]);
|
||||
assert!(!contains_secret(split.as_bytes(), config, &runtime));
|
||||
let inactive = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([72u8; 32]);
|
||||
assert!(!contains_secret(inactive.as_bytes(), config, &runtime));
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
generation.stop_background_tasks().await;
|
||||
}
|
||||
@@ -21,6 +21,9 @@ use crate::web::manager::{
|
||||
CarrierCapabilities, CarrierClientClass, CarrierFailure, CarrierRequest, WebProcessRuntime,
|
||||
};
|
||||
|
||||
#[path = "tests/base_path.rs"]
|
||||
mod base_path;
|
||||
|
||||
fn request(protocol: &str) -> Request<()> {
|
||||
Request::builder()
|
||||
.method(Method::GET)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use super::*;
|
||||
|
||||
async fn rejected_upgrade(
|
||||
listener: &TcpListener,
|
||||
runtime: &Arc<WebProcessRuntime>,
|
||||
path: &str,
|
||||
protocol: &str,
|
||||
) -> Vec<u8> {
|
||||
let address = listener.local_addr().unwrap();
|
||||
let (accepted, client) = tokio::join!(listener.accept(), TcpStream::connect(address));
|
||||
let (server, peer) = accepted.unwrap();
|
||||
let mut client = client.unwrap();
|
||||
let permit = runtime.try_http_connection().unwrap();
|
||||
let task = tokio::spawn(super::super::super::serve_connection(
|
||||
server,
|
||||
peer,
|
||||
WebClientIpSource::XForwardedFor,
|
||||
Arc::from(["127.0.0.1/32".parse().unwrap()]),
|
||||
Arc::clone(runtime),
|
||||
CancellationToken::new(),
|
||||
permit,
|
||||
));
|
||||
let request = format!(
|
||||
"GET {path} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close, Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Protocol: {protocol}\r\n\r\n"
|
||||
);
|
||||
client.write_all(request.as_bytes()).await.unwrap();
|
||||
let mut response = Vec::new();
|
||||
client.read_to_end(&mut response).await.unwrap();
|
||||
task.await.unwrap();
|
||||
response
|
||||
}
|
||||
|
||||
fn assert_private_not_found(response: &[u8]) {
|
||||
let (headers, body) = crate::web::http::tests::split_response(response);
|
||||
assert!(headers.starts_with(b"HTTP/1.1 404"));
|
||||
assert_eq!(
|
||||
crate::web::http::tests::response_header(headers, "cache-control"),
|
||||
"no-store"
|
||||
);
|
||||
assert_eq!(body, b"not found\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authentic_websocket_protocol_rejects_every_base_path_alias_locally() {
|
||||
let config = runtime_config_with_base([32; 32], WebCarrier::Websocket, "/relay/");
|
||||
let generation = test_runtime_generation(1, config);
|
||||
let runtime = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let (session, _) = create_session(&runtime);
|
||||
let protocol = format!("tproxy-v1.{session}");
|
||||
|
||||
for path in [
|
||||
"/api/v1/ws",
|
||||
"/Relay/api/v1/ws",
|
||||
"/relayx/api/v1/ws",
|
||||
"/relay//api/v1/ws",
|
||||
"/relay%2Fapi/v1/ws",
|
||||
"/relay/api%2Fv1/ws",
|
||||
"/relay/api/v1/ws/",
|
||||
] {
|
||||
assert_private_not_found(&rejected_upgrade(&listener, &runtime, path, &protocol).await);
|
||||
}
|
||||
|
||||
runtime.shutdown().await;
|
||||
generation.stop_sessions().await;
|
||||
generation.stop_background_tasks().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upgraded_socket_survives_base_path_generation_swap() {
|
||||
let old_generation = test_runtime_generation(
|
||||
1,
|
||||
runtime_config_with_base([33; 32], WebCarrier::Websocket, "/old/"),
|
||||
);
|
||||
let active = Arc::new(ArcSwap::from(Arc::clone(&old_generation)));
|
||||
let runtime = WebProcessRuntime::start(Arc::clone(&active));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let (session, _) = create_session(&runtime);
|
||||
let protocol = format!("tproxy-v1.{session}");
|
||||
let mut socket = upgrade_at(&listener, &runtime, "/old/api/v1/ws", &protocol).await;
|
||||
|
||||
let new_generation = test_runtime_generation(
|
||||
2,
|
||||
runtime_config_with_base([34; 32], WebCarrier::Websocket, "/new/"),
|
||||
);
|
||||
active.store(Arc::clone(&new_generation));
|
||||
socket
|
||||
.send(Message::Ping(Bytes::from_static(b"after-reload")))
|
||||
.await
|
||||
.unwrap();
|
||||
let response = tokio::time::timeout(Duration::from_secs(2), socket.next())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(response, Message::Pong(Bytes::from_static(b"after-reload")));
|
||||
assert_private_not_found(
|
||||
&rejected_upgrade(&listener, &runtime, "/old/api/v1/ws", &protocol).await,
|
||||
);
|
||||
|
||||
let _ = socket.close(None).await;
|
||||
runtime.shutdown().await;
|
||||
old_generation.stop_sessions().await;
|
||||
old_generation.stop_background_tasks().await;
|
||||
new_generation.stop_sessions().await;
|
||||
new_generation.stop_background_tasks().await;
|
||||
}
|
||||
Reference in New Issue
Block a user