mirror of
https://github.com/telemt/telemt.git
synced 2026-09-05 18:16:06 +03:00
cf0cd08387
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
541 lines
22 KiB
Rust
541 lines
22 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::sync::Arc;
|
|
|
|
use arc_swap::ArcSwap;
|
|
use base64::Engine as _;
|
|
use bytes::Bytes;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::{TcpListener, TcpStream};
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
use super::serve_connection;
|
|
use crate::config::{
|
|
ProxyConfig, WebCarrier, WebCarriers, WebClientIpSource, WebRuntimeConfig, WebRuntimeDecoy,
|
|
WebRuntimeProfile, WebRuntimeVhost, WebSecretMode, WebStaticAsset, WebStaticSite,
|
|
};
|
|
use crate::maestro::generation::test_runtime_generation;
|
|
use crate::web::frame::{self, FrameType};
|
|
use crate::web::manager::{
|
|
CloseOperationSelector, ControlError, SessionDetail, SessionFilter, SessionListRequest,
|
|
SessionRefError, WebProcessRuntime,
|
|
};
|
|
|
|
#[path = "legacy_tests.rs"]
|
|
mod legacy_tests;
|
|
#[path = "negotiation_tests.rs"]
|
|
mod negotiation_tests;
|
|
// Reload-stability tests for session-owned timeout policy.
|
|
#[path = "session_policy_tests.rs"]
|
|
mod session_policy_tests;
|
|
// Runtime control integration stays separate from carrier protocol scenarios.
|
|
#[path = "control_tests.rs"]
|
|
mod control_tests;
|
|
|
|
const TEST_CARRIER_DEADLINES_SECS: [u64; 4] = [3, 5, 8, 12];
|
|
|
|
pub(super) fn runtime_config(capability: [u8; 32], carrier: WebCarrier) -> ProxyConfig {
|
|
runtime_config_with_carriers(capability, carrier, false, true, Arc::from([carrier]))
|
|
}
|
|
|
|
pub(super) fn negotiation_runtime_config(
|
|
capability: [u8; 32],
|
|
carrier: WebCarrier,
|
|
carrier_learning: bool,
|
|
carriers: Arc<[WebCarrier]>,
|
|
) -> ProxyConfig {
|
|
runtime_config_with_carriers(capability, carrier, true, carrier_learning, carriers)
|
|
}
|
|
|
|
fn runtime_config_with_carriers(
|
|
capability: [u8; 32],
|
|
carrier: WebCarrier,
|
|
carrier_negotiation_enabled: bool,
|
|
carrier_learning: bool,
|
|
carriers: Arc<[WebCarrier]>,
|
|
) -> ProxyConfig {
|
|
runtime_config_with_carriers_and_deadlines(
|
|
capability,
|
|
carrier,
|
|
carrier_negotiation_enabled,
|
|
carrier_learning,
|
|
carriers,
|
|
TEST_CARRIER_DEADLINES_SECS,
|
|
)
|
|
}
|
|
|
|
pub(super) fn negotiation_runtime_config_with_deadlines(
|
|
capability: [u8; 32],
|
|
carrier: WebCarrier,
|
|
carrier_learning: bool,
|
|
carriers: Arc<[WebCarrier]>,
|
|
carrier_negotiation_deadlines_secs: [u64; 4],
|
|
) -> ProxyConfig {
|
|
runtime_config_with_carriers_and_deadlines(
|
|
capability,
|
|
carrier,
|
|
true,
|
|
carrier_learning,
|
|
carriers,
|
|
carrier_negotiation_deadlines_secs,
|
|
)
|
|
}
|
|
|
|
fn runtime_config_with_carriers_and_deadlines(
|
|
capability: [u8; 32],
|
|
carrier: WebCarrier,
|
|
carrier_negotiation_enabled: bool,
|
|
carrier_learning: bool,
|
|
carriers: Arc<[WebCarrier]>,
|
|
carrier_negotiation_deadlines_secs: [u64; 4],
|
|
) -> ProxyConfig {
|
|
let profile = Arc::new(WebRuntimeProfile {
|
|
host: "proxy.example.com".to_string(),
|
|
public_addr: "203.0.113.10:443".parse().unwrap(),
|
|
user: "alice".to_string(),
|
|
secret_mode: WebSecretMode::Plain,
|
|
carrier,
|
|
carrier_negotiation_enabled,
|
|
carrier_learning,
|
|
carriers: Arc::clone(&carriers),
|
|
carrier_negotiation_deadlines_secs,
|
|
capability,
|
|
key_fingerprint: "0000000000000000".to_string(),
|
|
max_sessions: 4,
|
|
max_streams: 16,
|
|
max_streams_per_session: 4,
|
|
});
|
|
let mut assets = BTreeMap::new();
|
|
assets.insert(
|
|
"/index.html".to_string(),
|
|
WebStaticAsset {
|
|
body: Bytes::from_static(b"<!doctype html><title>decoy</title>"),
|
|
content_type: "text/html; charset=utf-8",
|
|
etag: "\"test\"".to_string(),
|
|
},
|
|
);
|
|
let site = Arc::new(WebStaticSite {
|
|
assets,
|
|
index: "index.html".to_string(),
|
|
});
|
|
let vhost = Arc::new(WebRuntimeVhost {
|
|
host: "proxy.example.com".to_string(),
|
|
decoy: WebRuntimeDecoy::StaticDirectory(Arc::clone(&site)),
|
|
decoy_header_secs: 1,
|
|
profiles: vec![Arc::clone(&profile)],
|
|
});
|
|
let mut vhosts = BTreeMap::new();
|
|
vhosts.insert("proxy.example.com".to_string(), vhost);
|
|
vhosts.insert(
|
|
"other.example.com".to_string(),
|
|
Arc::new(WebRuntimeVhost {
|
|
host: "other.example.com".to_string(),
|
|
decoy: WebRuntimeDecoy::StaticDirectory(site),
|
|
decoy_header_secs: 1,
|
|
profiles: Vec::new(),
|
|
}),
|
|
);
|
|
let mut config = ProxyConfig::default();
|
|
config.web.enabled = true;
|
|
config.web.carrier = carrier;
|
|
config.web.carriers = if carrier_negotiation_enabled {
|
|
WebCarriers::Enabled(carriers.to_vec())
|
|
} else {
|
|
WebCarriers::Disabled
|
|
};
|
|
config.web.carrier_learning = carrier_learning;
|
|
config.web.timeouts.carrier_negotiation_deadlines_secs = carrier_negotiation_deadlines_secs;
|
|
config.web.limits.max_bootstraps_per_ip = 1;
|
|
config.web.timeouts.shutdown_secs = 1;
|
|
config.web.runtime = Some(Arc::new(WebRuntimeConfig {
|
|
vhosts,
|
|
profiles: vec![profile],
|
|
}));
|
|
config
|
|
}
|
|
|
|
pub(super) async fn request(
|
|
listener: &TcpListener,
|
|
runtime: &Arc<WebProcessRuntime>,
|
|
request: Vec<u8>,
|
|
) -> Vec<u8> {
|
|
let addr = listener.local_addr().unwrap();
|
|
let (accepted, client) = tokio::join!(listener.accept(), TcpStream::connect(addr));
|
|
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,
|
|
));
|
|
client.write_all(&request).await.unwrap();
|
|
let mut response = Vec::new();
|
|
client.read_to_end(&mut response).await.unwrap();
|
|
task.await.unwrap();
|
|
response
|
|
}
|
|
|
|
pub(super) fn split_response(response: &[u8]) -> (&[u8], &[u8]) {
|
|
let separator = response
|
|
.windows(4)
|
|
.position(|window| window == b"\r\n\r\n")
|
|
.unwrap();
|
|
(&response[..separator], &response[separator + 4..])
|
|
}
|
|
|
|
pub(super) fn response_header<'a>(headers: &'a [u8], name: &str) -> &'a str {
|
|
std::str::from_utf8(headers)
|
|
.unwrap()
|
|
.lines()
|
|
.filter_map(|line| line.split_once(':'))
|
|
.find_map(|(header, value)| header.eq_ignore_ascii_case(name).then_some(value.trim()))
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn https_carrier_bootstraps_and_closes_one_session() {
|
|
let capability = [7u8; 32];
|
|
let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https));
|
|
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
|
|
let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime));
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
|
let root = format!(
|
|
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes();
|
|
let root_response = request(&listener, &runtime, root).await;
|
|
let (root_headers, root_body) = split_response(&root_response);
|
|
assert!(root_headers.starts_with(b"HTTP/1.1 200"));
|
|
let root_body = std::str::from_utf8(root_body).unwrap();
|
|
let bootstrap = root_body
|
|
.split_once("bootstrap=\"")
|
|
.and_then(|(_, suffix)| suffix.split_once('"'))
|
|
.map(|(token, _)| token)
|
|
.unwrap();
|
|
assert_eq!(bootstrap.len(), 43);
|
|
|
|
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
|
let mut wrong_host = format!(
|
|
"POST /api/v1/session HTTP/1.1\r\nHost: other.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();
|
|
wrong_host.extend_from_slice(&hello);
|
|
let wrong_host_response = request(&listener, &runtime, wrong_host).await;
|
|
assert!(wrong_host_response.starts_with(b"HTTP/1.1 404"));
|
|
|
|
let mut create = format!(
|
|
"POST /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();
|
|
let create_retry = create.clone();
|
|
create.extend_from_slice(&hello);
|
|
let mut create_retry = create_retry;
|
|
create_retry.extend_from_slice(&hello);
|
|
let create_response = request(&listener, &runtime, create).await;
|
|
let (create_headers, create_body) = split_response(&create_response);
|
|
assert!(create_headers.starts_with(b"HTTP/1.1 200"));
|
|
assert_eq!(response_header(create_headers, "x-carrier-mode"), "https");
|
|
assert_eq!(create_body, frame::encode(FrameType::Welcome, 0, &[]));
|
|
let session = response_header(create_headers, "x-session-token");
|
|
assert_eq!(session.len(), 43);
|
|
|
|
let replacement =
|
|
test_runtime_generation(2, runtime_config(capability, WebCarrier::HttpsLanes));
|
|
active_runtime.store(Arc::clone(&replacement));
|
|
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
|
|
let retry_response = request(&listener, &runtime, create_retry).await;
|
|
let (retry_headers, retry_body) = split_response(&retry_response);
|
|
assert!(retry_headers.starts_with(b"HTTP/1.1 200"));
|
|
assert_eq!(response_header(retry_headers, "x-session-token"), session);
|
|
assert_eq!(response_header(retry_headers, "x-carrier-mode"), "https");
|
|
assert_eq!(retry_body, frame::encode(FrameType::Welcome, 0, &[]));
|
|
|
|
let next_root = format!(
|
|
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes();
|
|
let next_root_response = request(&listener, &runtime, next_root).await;
|
|
let (_, next_root_body) = split_response(&next_root_response);
|
|
assert!(
|
|
next_root_body
|
|
.windows(11)
|
|
.any(|value| value == b"bootstrap=\"")
|
|
);
|
|
assert!(
|
|
next_root_body
|
|
.windows(b"const negotiationEnabled=false".len())
|
|
.any(|value| value == b"const negotiationEnabled=false")
|
|
);
|
|
|
|
let close = format!(
|
|
"DELETE /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes();
|
|
let close_retry = close.clone();
|
|
let close_response = request(&listener, &runtime, close).await;
|
|
assert!(close_response.starts_with(b"HTTP/1.1 204"));
|
|
let close_retry_response = request(&listener, &runtime, close_retry).await;
|
|
assert!(close_retry_response.starts_with(b"HTTP/1.1 204"));
|
|
|
|
runtime.shutdown().await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
replacement.stop_sessions().await;
|
|
replacement.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn rejected_bridge_bootstrap_falls_back_to_uncacheable_static_index() {
|
|
let capability = [15u8; 32];
|
|
let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https));
|
|
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
|
|
let runtime = WebProcessRuntime::start(active_runtime);
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
|
let bridge_request = || {
|
|
format!(
|
|
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes()
|
|
};
|
|
|
|
let first_response = request(&listener, &runtime, bridge_request()).await;
|
|
let (_, first_body) = split_response(&first_response);
|
|
assert!(first_body.windows(11).any(|value| value == b"bootstrap=\""));
|
|
|
|
let fallback_response = request(&listener, &runtime, bridge_request()).await;
|
|
let (fallback_headers, fallback_body) = split_response(&fallback_response);
|
|
assert!(fallback_headers.starts_with(b"HTTP/1.1 200"));
|
|
assert_eq!(
|
|
response_header(fallback_headers, "cache-control"),
|
|
"no-store"
|
|
);
|
|
assert_eq!(fallback_body, b"<!doctype html><title>decoy</title>");
|
|
|
|
runtime.shutdown().await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn bootstrap_survives_client_address_family_change() {
|
|
let capability = [8u8; 32];
|
|
let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https));
|
|
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
|
|
let runtime = WebProcessRuntime::start(active_runtime);
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
|
let root = format!(
|
|
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 2001:db8::10\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes();
|
|
let root_response = request(&listener, &runtime, root).await;
|
|
let (_, root_body) = split_response(&root_response);
|
|
let root_body = std::str::from_utf8(root_body).unwrap();
|
|
let bootstrap = root_body
|
|
.split_once("bootstrap=\"")
|
|
.and_then(|(_, suffix)| suffix.split_once('"'))
|
|
.map(|(token, _)| token)
|
|
.unwrap();
|
|
|
|
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
|
let mut create = format!(
|
|
"POST /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 create_response = request(&listener, &runtime, create).await;
|
|
assert!(create_response.starts_with(b"HTTP/1.1 200"));
|
|
|
|
runtime.shutdown().await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unused_bootstrap_survives_equivalent_runtime_generation_swap() {
|
|
let capability = [10u8; 32];
|
|
let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https));
|
|
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
|
|
let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime));
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
|
let root = format!(
|
|
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes();
|
|
let root_response = request(&listener, &runtime, root).await;
|
|
let (_, root_body) = split_response(&root_response);
|
|
let root_body = std::str::from_utf8(root_body).unwrap();
|
|
let bootstrap = root_body
|
|
.split_once("bootstrap=\"")
|
|
.and_then(|(_, suffix)| suffix.split_once('"'))
|
|
.map(|(token, _)| token)
|
|
.unwrap();
|
|
|
|
let replacement = test_runtime_generation(2, runtime_config(capability, WebCarrier::Https));
|
|
active_runtime.store(Arc::clone(&replacement));
|
|
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
|
let mut create = format!(
|
|
"POST /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 create_response = request(&listener, &runtime, create).await;
|
|
assert!(create_response.starts_with(b"HTTP/1.1 200"));
|
|
|
|
runtime.shutdown().await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
replacement.stop_sessions().await;
|
|
replacement.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn bridge_bootstrap_uses_the_generation_that_selected_its_profile() {
|
|
let initial = test_runtime_generation(1, runtime_config([21; 32], WebCarrier::Https));
|
|
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&initial)));
|
|
let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime));
|
|
let profile = initial.config().web.runtime.as_ref().unwrap().profiles[0].clone();
|
|
let replacement = test_runtime_generation(2, runtime_config([22; 32], WebCarrier::HttpsLanes));
|
|
active_runtime.store(Arc::clone(&replacement));
|
|
|
|
let result =
|
|
runtime.issue_bootstrap_for_generation(&initial, profile, "192.0.2.10".parse().unwrap());
|
|
|
|
assert!(result.is_ok());
|
|
runtime.shutdown().await;
|
|
initial.stop_sessions().await;
|
|
initial.stop_background_tasks().await;
|
|
replacement.stop_sessions().await;
|
|
replacement.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unused_bootstrap_is_rejected_after_profile_identity_change() {
|
|
let capability = [11u8; 32];
|
|
let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https));
|
|
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
|
|
let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime));
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
|
let root = format!(
|
|
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes();
|
|
let root_response = request(&listener, &runtime, root).await;
|
|
let (_, root_body) = split_response(&root_response);
|
|
let root_body = std::str::from_utf8(root_body).unwrap();
|
|
let bootstrap = root_body
|
|
.split_once("bootstrap=\"")
|
|
.and_then(|(_, suffix)| suffix.split_once('"'))
|
|
.map(|(token, _)| token)
|
|
.unwrap();
|
|
|
|
let replacement =
|
|
test_runtime_generation(2, runtime_config(capability, WebCarrier::HttpsLanes));
|
|
active_runtime.store(Arc::clone(&replacement));
|
|
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
|
let mut create = format!(
|
|
"POST /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 create_response = request(&listener, &runtime, create).await;
|
|
assert!(!create_response.starts_with(b"HTTP/1.1 200"));
|
|
|
|
runtime.shutdown().await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
replacement.stop_sessions().await;
|
|
replacement.stop_background_tasks().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn https_lanes_is_advertised_and_requires_canonical_lane_headers() {
|
|
let capability = [9u8; 32];
|
|
let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::HttpsLanes));
|
|
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation)));
|
|
let runtime = WebProcessRuntime::start(active_runtime);
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability);
|
|
let root = format!(
|
|
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n"
|
|
)
|
|
.into_bytes();
|
|
let root_response = request(&listener, &runtime, root).await;
|
|
let (_, root_body) = split_response(&root_response);
|
|
let root_body = std::str::from_utf8(root_body).unwrap();
|
|
assert!(root_body.contains("const negotiationEnabled=false"));
|
|
let bootstrap = root_body
|
|
.split_once("bootstrap=\"")
|
|
.and_then(|(_, suffix)| suffix.split_once('"'))
|
|
.map(|(token, _)| token)
|
|
.unwrap();
|
|
|
|
let hello = frame::encode(FrameType::Hello, 0, &[1]);
|
|
let mut create = format!(
|
|
"POST /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 create_response = request(&listener, &runtime, create).await;
|
|
let (create_headers, _) = split_response(&create_response);
|
|
assert_eq!(
|
|
response_header(create_headers, "x-carrier-mode"),
|
|
"https-lanes"
|
|
);
|
|
let session = response_header(create_headers, "x-session-token").to_string();
|
|
|
|
let pong = frame::encode(FrameType::Pong, 0, &[]);
|
|
let mut uplink = format!(
|
|
"POST /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\nX-Lane-ID: 0\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
|
pong.len()
|
|
)
|
|
.into_bytes();
|
|
uplink.extend_from_slice(&pong);
|
|
let uplink_response = request(&listener, &runtime, uplink).await;
|
|
let (uplink_headers, _) = split_response(&uplink_response);
|
|
assert!(uplink_headers.starts_with(b"HTTP/1.1 204"));
|
|
assert_eq!(response_header(uplink_headers, "x-up-ack"), "1");
|
|
assert!(
|
|
!std::str::from_utf8(uplink_headers)
|
|
.unwrap()
|
|
.lines()
|
|
.any(|line| line.to_ascii_lowercase().starts_with("content-length:"))
|
|
);
|
|
|
|
let mut missing_lane = format!(
|
|
"POST /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: 2\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
|
pong.len()
|
|
)
|
|
.into_bytes();
|
|
missing_lane.extend_from_slice(&pong);
|
|
let missing_lane_response = request(&listener, &runtime, missing_lane).await;
|
|
assert!(!missing_lane_response.starts_with(b"HTTP/1.1 204"));
|
|
|
|
let mut aliased_lane = format!(
|
|
"POST /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: 2\r\nX-Lane-ID: 00\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
|
pong.len()
|
|
)
|
|
.into_bytes();
|
|
aliased_lane.extend_from_slice(&pong);
|
|
let aliased_lane_response = request(&listener, &runtime, aliased_lane).await;
|
|
assert!(!aliased_lane_response.starts_with(b"HTTP/1.1 204"));
|
|
|
|
runtime.shutdown().await;
|
|
generation.stop_sessions().await;
|
|
generation.stop_background_tasks().await;
|
|
}
|