Skip to main content

smb_server_test_dashboard/
http.rs

1//! A minimal blocking HTTP/1.1 request handler covering the routes the
2//! dashboard needs: the static page and two read-only JSON endpoints.
3
4use crate::{csv, json};
5use std::io::{BufRead, BufReader, Read, Write};
6use std::net::TcpStream;
7use std::path::Path;
8
9/// The single-page dashboard, embedded so the binary is self-contained.
10const INDEX_HTML: &str = include_str!("index.html");
11
12/// HTTP status codes used by the dashboard's read-only routes.
13const HTTP_OK: u16 = 200;
14const HTTP_NOT_FOUND: u16 = 404;
15const HTTP_METHOD_NOT_ALLOWED: u16 = 405;
16
17/// Read the request line, route it, and write the response.
18pub fn handle_connection(stream: TcpStream, csv_path: &Path) {
19    let mut reader = BufReader::new(stream);
20    let Some((method, target)) = read_request_line(&mut reader) else {
21        return;
22    };
23    drain_headers(&mut reader);
24    let stream = reader.get_mut();
25
26    if method != "GET" {
27        respond(stream, HTTP_METHOD_NOT_ALLOWED, "text/plain", b"method not allowed");
28        return;
29    }
30    route(stream, &target, csv_path);
31}
32
33fn read_request_line(reader: &mut BufReader<TcpStream>) -> Option<(String, String)> {
34    let mut line = String::new();
35    if reader.read_line(&mut line).ok()? == 0 {
36        return None;
37    }
38    let mut parts = line.split_whitespace();
39    let method = parts.next()?.to_string();
40    let target = parts.next()?.to_string();
41    Some((method, target))
42}
43
44/// Consume the rest of the request headers up to the blank line.
45fn drain_headers(reader: &mut BufReader<TcpStream>) {
46    let mut line = String::new();
47    while let Ok(n) = reader.read_line(&mut line) {
48        if n == 0 || line == "\r\n" || line == "\n" {
49            break;
50        }
51        line.clear();
52    }
53}
54
55fn route(stream: &mut TcpStream, target: &str, csv_path: &Path) {
56    let path = target.split('?').next().unwrap_or("/");
57    match path {
58        "/" | "/index.html" => respond(stream, HTTP_OK, "text/html; charset=utf-8", INDEX_HTML.as_bytes()),
59        "/api/status" => respond_json(stream, &status_json(csv_path)),
60        "/api/tests" => respond_json(stream, &tests_json(csv_path)),
61        "/healthz" => respond(stream, HTTP_OK, "text/plain", b"ok"),
62        _ => respond(stream, HTTP_NOT_FOUND, "text/plain", b"not found"),
63    }
64}
65
66fn status_json(csv_path: &Path) -> String {
67    let rows = csv::load(csv_path);
68    let summary = csv::summarize(&rows);
69    json::status_body(&rows, &summary)
70}
71
72fn tests_json(csv_path: &Path) -> String {
73    let rows = csv::load(csv_path);
74    json::tests_body(&rows)
75}
76
77fn respond_json(stream: &mut TcpStream, body: &str) {
78    respond(stream, HTTP_OK, "application/json; charset=utf-8", body.as_bytes());
79}
80
81/// Write a complete HTTP/1.1 response and close the connection.
82fn respond(stream: &mut TcpStream, code: u16, content_type: &str, body: &[u8]) {
83    let reason = match code {
84        200 => "OK",
85        404 => "Not Found",
86        405 => "Method Not Allowed",
87        _ => "Error",
88    };
89    let header = format!(
90        "HTTP/1.1 {code} {reason}\r\n\
91         Content-Type: {content_type}\r\n\
92         Content-Length: {}\r\n\
93         Access-Control-Allow-Origin: *\r\n\
94         Cache-Control: no-store\r\n\
95         Connection: close\r\n\r\n",
96        body.len()
97    );
98    let _ = stream.write_all(header.as_bytes());
99    let _ = stream.write_all(body);
100    let _ = stream.flush();
101    // Drain anything the client is still sending so the RST doesn't truncate us.
102    let _ = stream.read(&mut [0u8; 0]);
103}