Skip to main content

web_admin/
routes.rs

1use alloc::string::String;
2use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
3use picoserve::{
4    extract::FromRequestParts,
5    request::RequestParts,
6    response::Response,
7    routing::{get, parse_path_segment, post},
8};
9
10use crate::{net, sysinfo};
11const ADMIN_TOKEN_PATH: &str = "/initfs/web-admin.token";
12const KILL_RATE_LIMIT_WINDOW_NS: u64 = 10_000_000_000;
13const KILL_RATE_LIMIT_MAX_PER_WINDOW: u32 = 8;
14static KILL_WINDOW_START_NS: AtomicU64 = AtomicU64::new(0);
15static KILL_WINDOW_COUNT: AtomicU32 = AtomicU32::new(0);
16
17// ---------------------------------------------------------------------------
18// Content types for picoserve responses
19// ---------------------------------------------------------------------------
20
21pub struct HtmlContent(pub &'static str);
22
23impl picoserve::response::Content for HtmlContent {
24    /// Implements content type.
25    fn content_type(&self) -> &'static str {
26        "text/html; charset=utf-8"
27    }
28    /// Implements content length.
29    fn content_length(&self) -> usize {
30        self.0.len()
31    }
32    async fn write_content<W: picoserve::io::Write>(self, mut writer: W) -> Result<(), W::Error> {
33        writer.write_all(self.0.as_bytes()).await
34    }
35}
36
37pub struct JsonContent(pub String);
38
39impl picoserve::response::Content for JsonContent {
40    /// Implements content type.
41    fn content_type(&self) -> &'static str {
42        "application/json"
43    }
44    /// Implements content length.
45    fn content_length(&self) -> usize {
46        self.0.len()
47    }
48    async fn write_content<W: picoserve::io::Write>(self, mut writer: W) -> Result<(), W::Error> {
49        writer.write_all(self.0.as_bytes()).await
50    }
51}
52
53// ---------------------------------------------------------------------------
54// Helper: JSON response with CORS
55// ---------------------------------------------------------------------------
56
57/// Implements json ok.
58fn json_ok(
59    body: String,
60) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
61    Response::ok(JsonContent(body)).with_header("Access-Control-Allow-Origin", "*")
62}
63
64/// Implements json admin.
65fn json_admin(
66    body: String,
67) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
68    Response::ok(JsonContent(body))
69}
70
71/// Implements admin token.
72fn admin_token() -> String {
73    let token = net::read_file_text(ADMIN_TOKEN_PATH);
74    let trimmed = token.trim();
75    String::from(trimmed)
76}
77
78/// Implements allow kill now.
79fn allow_kill_now() -> bool {
80    let now = net::clock_gettime_ns();
81    let start = KILL_WINDOW_START_NS.load(Ordering::Relaxed);
82    if now.saturating_sub(start) > KILL_RATE_LIMIT_WINDOW_NS {
83        KILL_WINDOW_START_NS.store(now, Ordering::Relaxed);
84        KILL_WINDOW_COUNT.store(0, Ordering::Relaxed);
85    }
86    let prev = KILL_WINDOW_COUNT.fetch_add(1, Ordering::Relaxed);
87    prev < KILL_RATE_LIMIT_MAX_PER_WINDOW
88}
89
90struct AdminAuth;
91
92impl<'r, State> FromRequestParts<'r, State> for AdminAuth {
93    type Rejection = String;
94
95    /// Builds a value from request parts.
96    async fn from_request_parts(
97        _state: &'r State,
98        request_parts: &RequestParts<'r>,
99    ) -> Result<Self, Self::Rejection> {
100        let expected = admin_token();
101        if expected.is_empty() {
102            return Err(String::from(
103                r#"{"killed":false,"error":"admin token not configured"}"#,
104            ));
105        }
106        let provided = request_parts
107            .headers()
108            .get("authorization")
109            .and_then(|v| v.as_str().ok())
110            .unwrap_or("");
111        let ok = if let Some(bearer) = provided.strip_prefix("Bearer ") {
112            bearer == expected
113        } else {
114            provided == expected
115        };
116        if ok {
117            Ok(Self)
118        } else {
119            Err(String::from(r#"{"killed":false,"error":"unauthorized"}"#))
120        }
121    }
122}
123
124// ---------------------------------------------------------------------------
125// Route handlers
126// ---------------------------------------------------------------------------
127
128/// Implements index.
129async fn index() -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body>
130{
131    Response::ok(HtmlContent(DASHBOARD_HTML))
132}
133
134/// Implements api health.
135async fn api_health(
136) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
137    json_ok(sysinfo::json_health())
138}
139
140/// Implements api uptime.
141async fn api_uptime(
142) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
143    json_ok(sysinfo::json_uptime())
144}
145
146/// Implements api version.
147async fn api_version(
148) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
149    json_ok(sysinfo::json_version())
150}
151
152/// Implements api cpuinfo.
153async fn api_cpuinfo(
154) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
155    json_ok(sysinfo::json_cpuinfo())
156}
157
158/// Implements api meminfo.
159async fn api_meminfo(
160) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
161    json_ok(sysinfo::json_meminfo())
162}
163
164/// Implements api silos.
165async fn api_silos(
166) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
167    json_ok(sysinfo::json_silos())
168}
169
170/// Implements api processes.
171async fn api_processes(
172) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
173    json_ok(sysinfo::json_processes())
174}
175
176/// Implements api network.
177async fn api_network(
178) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
179    json_ok(sysinfo::json_network())
180}
181
182/// Implements api routes.
183async fn api_routes(
184) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
185    json_ok(sysinfo::json_routes())
186}
187
188async fn api_graphics_open(
189    sid: u32,
190    _auth: AdminAuth,
191) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
192    json_admin(sysinfo::json_graphics_open(sid))
193}
194
195async fn api_graphics_close(
196    session_id: u64,
197    _auth: AdminAuth,
198) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
199    json_admin(sysinfo::json_graphics_close(session_id))
200}
201
202async fn api_graphics_info(
203    session_id: u64,
204    _auth: AdminAuth,
205) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
206    json_admin(sysinfo::json_graphics_info(session_id))
207}
208
209/// Implements api all.
210async fn api_all() -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body>
211{
212    json_ok(sysinfo::json_all())
213}
214
215/// Implements api kill.
216async fn api_kill(
217    pid: u32,
218    _auth: AdminAuth,
219) -> Response<impl picoserve::response::HeadersIter, impl picoserve::response::Body> {
220    if !allow_kill_now() {
221        return json_admin(String::from(r#"{"killed":false,"error":"rate limit"}"#));
222    }
223    json_admin(sysinfo::json_kill_result(pid))
224}
225
226// ---------------------------------------------------------------------------
227// Router construction
228// ---------------------------------------------------------------------------
229
230/// Implements build router.
231pub fn build_router() -> picoserve::Router<impl picoserve::routing::PathRouter> {
232    picoserve::Router::new()
233        .route("/", get(index))
234        .route("/api/health", get(api_health))
235        .route("/api/uptime", get(api_uptime))
236        .route("/api/version", get(api_version))
237        .route("/api/cpuinfo", get(api_cpuinfo))
238        .route("/api/meminfo", get(api_meminfo))
239        .route("/api/silos", get(api_silos))
240        .route("/api/processes", get(api_processes))
241        .route("/api/network", get(api_network))
242        .route("/api/routes", get(api_routes))
243        .route("/api/all", get(api_all))
244        .route(
245            ("/api/graphics/open", parse_path_segment::<u32>()),
246            post(|sid, auth| async move { api_graphics_open(sid, auth).await }),
247        )
248        .route(
249            ("/api/graphics/close", parse_path_segment::<u64>()),
250            post(|session_id, auth| async move { api_graphics_close(session_id, auth).await }),
251        )
252        .route(
253            ("/api/graphics/info", parse_path_segment::<u64>()),
254            get(|session_id, auth| async move { api_graphics_info(session_id, auth).await }),
255        )
256        .route(
257            ("/api/kill", parse_path_segment::<u32>()),
258            post(|pid, auth| async move { api_kill(pid, auth).await }),
259        )
260}
261
262// ---------------------------------------------------------------------------
263// Embedded HTML dashboard
264// ---------------------------------------------------------------------------
265
266const DASHBOARD_HTML: &str = include_str!("../assets/dashboard.html");