#![cfg(feature = "server")]
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
reason = "test code"
)]
use std::net::SocketAddr;
use std::sync::Arc;
use axum::body::Body;
use axum::extract::connect_info::MockConnectInfo;
use axum::http::Request;
use tower::ServiceExt;
use wire::{Answer, Command, Reply, ServeConfig};
const AT: &str = "127.0.0.1:8787";
fn config() -> ServeConfig {
ServeConfig {
deltas: tokio::sync::broadcast::channel(16).0,
logs: tokio::sync::broadcast::channel(16).0,
outputs: tokio::sync::broadcast::channel(16).0,
outputs_so_far: Arc::new(Vec::new),
monitor: wire::MonitorFeed {
watch: Arc::new(|_| -> Box<dyn Send> { Box::new(()) }),
samples: tokio::sync::broadcast::channel(1).0,
beat: Arc::new(|_| {}),
},
client: Arc::new(wire::ClientAssets::Embedded(&[])),
commands: Arc::new(|_, _| Ok(())),
drop_sink: Arc::new(|_, _| Ok(String::new())),
transcribe_sink: Arc::new(|_, _| Ok(String::new())),
events: tokio::sync::broadcast::channel(16).0,
queries: Arc::new(|_| {
(
kernel::Seq::FIRST,
Ok(Answer::Unavailable {
query: "none".to_owned(),
reason: None,
}),
)
}),
secrets: Arc::new(|_: Command<kernel::Sealed<String>>, _: Reply| Ok(())),
acp: Arc::new(|_, _| {
Ok(wire::AcpProgress {
run: String::new(),
turns: 0,
finished: true,
})
}),
city: None,
head: Arc::default(),
epoch: None,
door: wire::LocalDoor::new(
Vec::new(),
wire::DoorSenses {
clock: Arc::new(|| Ok(kernel::TimeMs::new(0))),
entropy: Arc::new(|bytes: &mut [u8]| {
bytes.fill(7);
Ok(())
}),
},
Arc::new(|_: &[wire::PairedBrowser]| Ok(())),
)
.unwrap(),
}
}
async fn status(method: &str, path: &str, headers: &[(&str, &str)]) -> u16 {
let at: SocketAddr = AT.parse().unwrap();
let wire::BindVerdict::Serve(face) =
wire::decide_bind(&at, Some(kernel::B3Hash::digest(b"native-key")))
else {
panic!("a loopback address with a key is served");
};
let app = wire::router(&config(), face, &wire::ListenerOrigins::of(at))
.unwrap()
.layer(MockConnectInfo(at));
let mut request = Request::builder().method(method).uri(path);
for (name, value) in headers {
request = request.header(*name, *value);
}
app.oneshot(request.body(Body::empty()).unwrap())
.await
.unwrap()
.status()
.as_u16()
}
const UPGRADE: [(&str, &str); 4] = [
("connection", "upgrade"),
("upgrade", "websocket"),
("sec-websocket-version", "13"),
(
"sec-websocket-key",
concat!("dGhl", "IHNh", "bXBs", "ZSBu", "b25j", "ZQ=="),
),
];
#[tokio::test]
async fn a_cross_site_page_is_refused_before_the_socket_upgrades() {
for origin in [
"http://evil.example",
"http://127.0.0.1:5173",
"http://localhost:3000",
"null",
] {
let mut headers = UPGRADE.to_vec();
headers.extend([("host", AT), ("origin", origin)]);
assert_eq!(status("GET", "/ws", &headers).await, 403, "{origin}");
}
}
#[tokio::test]
async fn a_foreign_host_is_refused_before_the_socket_upgrades_or_the_page_is_served() {
for host in ["evil.example:8787", "0.0.0.0:8787", "127.0.0.1:9999"] {
let mut headers = UPGRADE.to_vec();
headers.push(("host", host));
assert_eq!(status("GET", "/ws", &headers).await, 403, "{host}");
assert_eq!(status("GET", "/", &[("host", host)]).await, 403, "{host}");
}
}
#[tokio::test]
async fn an_acting_door_refuses_a_page_on_another_port_of_this_machine() {
let headers = [
("host", AT),
("origin", "http://127.0.0.1:5173"),
("sec-fetch-site", "same-site"),
];
for path in ["/transcribe", "/drop?name=a", "/enroll", "/acp"] {
assert_eq!(status("POST", path, &headers).await, 403, "{path}");
}
}
#[tokio::test]
async fn a_preflight_is_refused() {
let headers = [
("host", AT),
("origin", "http://evil.example"),
("access-control-request-method", "POST"),
];
assert_eq!(status("OPTIONS", "/enroll", &headers).await, 403);
}