use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use axum::routing::{get, post};
use http_body_util::BodyExt;
use tower::ServiceExt;
use crate::gate::{LAUNCH_PARAM, LaunchToken, gate, launch_token};
fn app() -> Router {
Router::new()
.route("/", get(|| async { "the page" }))
.route("/js/main.js", get(|| async { "the runtime" }))
.route("/todo", post(|| async { "an rpc" }))
.layer(axum::middleware::from_fn(gate))
}
async fn send(request: Request<Body>) -> (StatusCode, axum::http::HeaderMap, String) {
let response = app().oneshot(request).await.expect("a response");
let status = response.status();
let headers = response.headers().clone();
let body = response.into_body().collect().await.unwrap().to_bytes();
(status, headers, String::from_utf8_lossy(&body).to_string())
}
fn with_cookie(path: &str, token: &str) -> Request<Body> {
Request::builder()
.uri(path)
.header(header::COOKIE, format!("{LAUNCH_PARAM}={token}"))
.body(Body::empty())
.unwrap()
}
#[test]
fn the_token_is_random_and_stable_within_a_launch() {
let once = launch_token();
assert_eq!(once, launch_token(), "the token changed mid-launch");
assert_eq!(once.len(), 48, "24 random bytes, as hex");
assert!(once.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn the_launch_url_carries_the_token_once() {
let url = LaunchToken::launch_url("http://127.0.0.1:5173");
assert_eq!(
url,
format!("http://127.0.0.1:5173/?{LAUNCH_PARAM}={}", launch_token())
);
assert_eq!(url, LaunchToken::launch_url("http://127.0.0.1:5173/"));
}
#[tokio::test]
async fn a_request_with_no_token_is_refused() {
let (status, _, body) = send(Request::builder().uri("/").body(Body::empty()).unwrap()).await;
assert_eq!(status, StatusCode::FORBIDDEN);
assert!(body.is_empty(), "{body:?}");
}
#[tokio::test]
async fn a_request_with_the_wrong_token_is_refused() {
let (status, _, _) = send(with_cookie("/", &"0".repeat(48))).await;
assert_eq!(status, StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn the_launch_url_sets_the_cookie_and_redirects_to_the_clean_url() {
let (status, headers, _) = send(
Request::builder()
.uri(format!("/?{LAUNCH_PARAM}={}", launch_token()))
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(status, StatusCode::SEE_OTHER);
let cookie = headers[header::SET_COOKIE].to_str().unwrap();
assert!(cookie.starts_with(&format!("{LAUNCH_PARAM}={}", launch_token())));
assert!(cookie.contains("HttpOnly"), "{cookie}");
assert!(cookie.contains("SameSite=Strict"), "{cookie}");
assert!(cookie.contains("Path=/"), "{cookie}");
assert!(!cookie.contains("Secure"), "{cookie}");
assert_eq!(headers[header::LOCATION], "/");
}
#[tokio::test]
async fn the_launch_url_keeps_the_querystring_it_was_given() {
let (_, headers, _) = send(
Request::builder()
.uri(format!(
"/?next=%2Faccount&{LAUNCH_PARAM}={}",
launch_token()
))
.body(Body::empty())
.unwrap(),
)
.await;
assert_eq!(headers[header::LOCATION], "/?next=%2Faccount");
}
#[tokio::test]
async fn the_cookie_admits_everything_a_rahti_page_needs() {
let token = launch_token();
let (status, _, body) = send(with_cookie("/", token)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "the page");
let (status, _, body) = send(with_cookie("/js/main.js", token)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "the runtime");
let rpc = Request::builder()
.method("POST")
.uri("/todo")
.header(header::COOKIE, format!("{LAUNCH_PARAM}={token}"))
.body(Body::empty())
.unwrap();
let (status, _, body) = send(rpc).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "an rpc");
}
#[tokio::test]
async fn the_cookie_is_found_beside_the_others_a_rahti_page_carries() {
let request = Request::builder()
.uri("/")
.header(
header::COOKIE,
format!(
"rahti_session_9f2c=abc; {LAUNCH_PARAM}={}; rahti_csrf_3000=def",
launch_token()
),
)
.body(Body::empty())
.unwrap();
let (status, _, _) = send(request).await;
assert_eq!(status, StatusCode::OK);
}
#[tokio::test]
async fn a_token_in_the_query_of_a_post_is_not_accepted() {
let request = Request::builder()
.method("POST")
.uri(format!("/todo?{LAUNCH_PARAM}={}", launch_token()))
.body(Body::empty())
.unwrap();
let (status, _, _) = send(request).await;
assert_eq!(status, StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn a_cookie_header_that_is_not_a_cookie_header_is_refused_quietly() {
for bad in ["", ";;;", "=", LAUNCH_PARAM, "__rahti_native"] {
let request = Request::builder()
.uri("/")
.header(header::COOKIE, bad)
.body(Body::empty())
.unwrap();
let (status, _, _) = send(request).await;
assert_eq!(status, StatusCode::FORBIDDEN, "{bad:?} was accepted");
}
}
#[tokio::test]
async fn a_prefix_of_the_token_is_not_the_token() {
let short = &launch_token()[..20];
let (status, _, _) = send(with_cookie("/", short)).await;
assert_eq!(status, StatusCode::FORBIDDEN);
}