use std::sync::Arc;
use tokio_tungstenite::tungstenite::{
handshake::server::{ErrorResponse, Request, Response},
http::{HeaderValue, Response as HttpResponse, StatusCode, header},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Admission {
NotABrowser,
AllowedOrigin,
Refused,
}
pub(super) fn admit(origin: Option<&HeaderValue>, allowed: &[String]) -> Admission {
let Some(origin) = origin else {
return Admission::NotABrowser;
};
match origin.to_str() {
Ok(named) if allowed.iter().any(|entry| entry == named) => Admission::AllowedOrigin,
Ok(_) | Err(_) => Admission::Refused,
}
}
#[allow(clippy::result_large_err)]
pub(super) fn origin_guard(
allowed: Arc<Vec<String>>,
) -> impl FnOnce(&Request, Response) -> Result<Response, ErrorResponse> + Unpin {
move |request, response| {
match admit(request.headers().get(header::ORIGIN), &allowed) {
Admission::NotABrowser | Admission::AllowedOrigin => Ok(response),
Admission::Refused => Err(refused()),
}
}
}
fn refused() -> ErrorResponse {
HttpResponse::builder()
.status(StatusCode::FORBIDDEN)
.body(Some(
"basis: this bridge does not serve web pages unless their origin is \
allowed explicitly. A page can reach a loopback socket without \
asking anyone, so the allowlist is the only thing standing between \
a visited site and this workspace."
.to_string(),
))
.expect("a status and a body always build a response")
}
#[cfg(test)]
mod tests {
use super::*;
fn allowed() -> Vec<String> {
vec!["http://localhost:5173".to_string()]
}
#[test]
fn a_caller_without_an_origin_is_not_a_browser() {
assert_eq!(admit(None, &[]), Admission::NotABrowser);
assert_eq!(
admit(None, &allowed()),
Admission::NotABrowser,
"a native client is served whether or not any page is"
);
}
#[test]
fn a_named_origin_is_admitted() {
assert_eq!(
admit(
Some(&HeaderValue::from_static("http://localhost:5173")),
&allowed()
),
Admission::AllowedOrigin
);
}
#[test]
fn an_unnamed_page_is_refused() {
assert_eq!(
admit(
Some(&HeaderValue::from_static("https://evil.example")),
&allowed()
),
Admission::Refused
);
}
#[test]
fn the_empty_allowlist_refuses_every_page() {
for origin in [
"http://localhost:5173",
"http://127.0.0.1:5173",
"null",
"https://evil.example",
] {
assert_eq!(
admit(Some(&HeaderValue::from_static(origin)), &[]),
Admission::Refused,
"{origin} must not be served by default: a page reaches loopback unasked"
);
}
}
#[test]
fn an_origin_that_is_not_text_is_refused() {
let malformed = HeaderValue::from_bytes(&[0xff, 0xfe]).expect("a header of raw bytes");
assert_eq!(admit(Some(&malformed), &allowed()), Admission::Refused);
}
#[test]
fn an_origin_must_match_exactly() {
for near_miss in [
"http://localhost:5174",
"https://localhost:5173",
"http://localhost:5173/",
"http://localhost:5173.evil.example",
] {
assert_eq!(
admit(Some(&HeaderValue::from_static(near_miss)), &allowed()),
Admission::Refused,
"{near_miss} is a different origin from the one that was allowed"
);
}
}
}