Skip to main content

tandem_server/webui/
mod.rs

1use axum::body::Body;
2use axum::http::header;
3use axum::http::{HeaderValue, StatusCode};
4use axum::response::{IntoResponse, Response};
5use axum::routing::get;
6use axum::Router;
7
8static ADMIN_HTML: &str = include_str!("admin.html");
9
10const CSP_HEADER: &str = "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src data:; frame-ancestors 'none'; base-uri 'none'; form-action 'self'";
11
12pub fn web_ui_router<S>(prefix: &str) -> Router<S>
13where
14    S: Clone + Send + Sync + 'static,
15{
16    let base = normalize_prefix(prefix);
17    let wildcard = format!("{}/{{*path}}", base);
18    Router::new()
19        .route(&base, get(serve_index))
20        .route(&format!("{}/", base), get(serve_index))
21        .route(&wildcard, get(serve_index))
22}
23
24async fn serve_index() -> impl IntoResponse {
25    let mut response = Response::new(Body::from(ADMIN_HTML));
26    *response.status_mut() = StatusCode::OK;
27    let headers = response.headers_mut();
28    headers.insert(
29        header::CONTENT_TYPE,
30        HeaderValue::from_static("text/html; charset=utf-8"),
31    );
32    headers.insert(
33        header::CACHE_CONTROL,
34        HeaderValue::from_static("no-store, max-age=0"),
35    );
36    headers.insert(
37        header::HeaderName::from_static("content-security-policy"),
38        HeaderValue::from_static(CSP_HEADER),
39    );
40    headers.insert(
41        header::HeaderName::from_static("x-frame-options"),
42        HeaderValue::from_static("DENY"),
43    );
44    headers.insert(
45        header::HeaderName::from_static("x-content-type-options"),
46        HeaderValue::from_static("nosniff"),
47    );
48    headers.insert(
49        header::HeaderName::from_static("referrer-policy"),
50        HeaderValue::from_static("no-referrer"),
51    );
52    response
53}
54
55fn normalize_prefix(prefix: &str) -> String {
56    let raw = prefix.trim();
57    if raw.is_empty() || raw == "/" {
58        return "/admin".to_string();
59    }
60    let with_leading = if raw.starts_with('/') {
61        raw.to_string()
62    } else {
63        format!("/{raw}")
64    };
65    with_leading.trim_end_matches('/').to_string()
66}