1use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
13use axum::response::{IntoResponse, Response};
14use axum::routing::get;
15use axum::Router;
16
17const INDEX_HTML: &str = include_str!("../ui/index.html");
18const APP_JS: &str = include_str!("../ui/app.js");
19const APP_CSS: &str = include_str!("../ui/app.css");
20const FAVICON_SVG: &str = include_str!("../ui/favicon.svg");
21
22pub const CSP: &str = "default-src 'none'; script-src 'self'; style-src 'self'; \
26img-src 'self' data:; connect-src 'self'; font-src 'self'; base-uri 'none'; \
27form-action 'none'; frame-ancestors 'none'";
28
29pub fn router() -> Router {
32 Router::new()
33 .route("/", get(root))
34 .route("/ui", get(|| async { redirect("/ui/") }))
35 .route(
36 "/ui/",
37 get(|| async { asset("text/html; charset=utf-8", INDEX_HTML) }),
38 )
39 .route(
40 "/ui/index.html",
41 get(|| async { asset("text/html; charset=utf-8", INDEX_HTML) }),
42 )
43 .route(
44 "/ui/app.js",
45 get(|| async { asset("text/javascript; charset=utf-8", APP_JS) }),
46 )
47 .route(
48 "/ui/app.css",
49 get(|| async { asset("text/css; charset=utf-8", APP_CSS) }),
50 )
51 .route(
52 "/ui/favicon.svg",
53 get(|| async { asset("image/svg+xml", FAVICON_SVG) }),
54 )
55 .route("/ui/{*rest}", get(not_found))
56}
57
58fn security_headers(headers: &mut HeaderMap) {
59 let set = |h: &mut HeaderMap, k: &'static str, v: &'static str| {
60 h.insert(k, HeaderValue::from_static(v));
61 };
62 set(headers, "content-security-policy", CSP);
63 set(headers, "x-content-type-options", "nosniff");
64 set(headers, "x-frame-options", "DENY");
65 set(headers, "referrer-policy", "no-referrer");
66 set(headers, "cross-origin-opener-policy", "same-origin");
67 set(headers, "cross-origin-resource-policy", "same-origin");
68 set(
69 headers,
70 "permissions-policy",
71 "camera=(), microphone=(), geolocation=(), payment=()",
72 );
73 set(headers, "cache-control", "no-cache");
75}
76
77fn asset(content_type: &'static str, body: &'static str) -> Response {
78 let mut resp = (StatusCode::OK, body).into_response();
79 resp.headers_mut()
80 .insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
81 security_headers(resp.headers_mut());
82 resp
83}
84
85fn redirect(to: &'static str) -> Response {
86 let mut resp = StatusCode::FOUND.into_response();
87 resp.headers_mut()
88 .insert(header::LOCATION, HeaderValue::from_static(to));
89 security_headers(resp.headers_mut());
90 resp
91}
92
93async fn not_found() -> Response {
94 let mut resp = (StatusCode::NOT_FOUND, "not found\n").into_response();
95 resp.headers_mut().insert(
96 header::CONTENT_TYPE,
97 HeaderValue::from_static("text/plain; charset=utf-8"),
98 );
99 security_headers(resp.headers_mut());
100 resp
101}
102
103async fn root(headers: HeaderMap) -> Response {
106 let html = headers
107 .get_all(header::ACCEPT)
108 .iter()
109 .filter_map(|v| v.to_str().ok())
110 .any(|v| v.contains("text/html"));
111 if html {
112 return redirect("/ui/");
113 }
114 (
115 StatusCode::NOT_FOUND,
116 axum::Json(serde_json::json!({ "error": {
117 "code": "NOT_FOUND",
118 "message": "no such endpoint; the API is under /v1/ and the web UI under /ui/",
119 }})),
120 )
121 .into_response()
122}