vpp_sim/
api.rs

1use crate::telemetry::TelemetryRow;
2use std::io::{self, BufRead, BufReader, Write};
3use std::net::{TcpListener, TcpStream};
4
5pub fn run_http_server(bind_addr: &str, telemetry: Vec<TelemetryRow>) -> io::Result<()> {
6    let listener = TcpListener::bind(bind_addr)?;
7    println!("HTTP API listening on http://{bind_addr}");
8    serve(listener, telemetry)
9}
10
11fn serve(listener: TcpListener, telemetry: Vec<TelemetryRow>) -> io::Result<()> {
12    for incoming in listener.incoming() {
13        let stream = match incoming {
14            Ok(stream) => stream,
15            Err(err) => {
16                eprintln!("warning: failed to accept connection: {err}");
17                continue;
18            }
19        };
20
21        if let Err(err) = handle_connection(stream, &telemetry) {
22            eprintln!("warning: failed to handle request: {err}");
23        }
24    }
25
26    Ok(())
27}
28
29fn handle_connection(mut stream: TcpStream, telemetry: &[TelemetryRow]) -> io::Result<()> {
30    let mut request_line = String::new();
31    {
32        let mut reader = BufReader::new(&mut stream);
33        if reader.read_line(&mut request_line)? == 0 {
34            return Ok(());
35        }
36
37        // Consume and ignore headers.
38        loop {
39            let mut line = String::new();
40            if reader.read_line(&mut line)? == 0 {
41                break;
42            }
43            if line == "\r\n" {
44                break;
45            }
46        }
47    }
48
49    let request_line = request_line.trim_end_matches(['\r', '\n']);
50    let mut parts = request_line.split_whitespace();
51    let method = parts.next().unwrap_or("");
52    let target = parts.next().unwrap_or("");
53
54    if method != "GET" {
55        return write_response(
56            &mut stream,
57            "405 Method Not Allowed",
58            "application/json",
59            "{\"error\":\"only GET is supported\"}",
60        );
61    }
62
63    let (path, query) = split_target(target);
64    match path {
65        "/state" => {
66            if let Some(snapshot) = telemetry.last() {
67                let body = serde_json::to_string(snapshot)
68                    .map_err(|err| io::Error::other(format!("serialize state: {err}")))?;
69                write_response(&mut stream, "200 OK", "application/json", &body)
70            } else {
71                write_response(
72                    &mut stream,
73                    "404 Not Found",
74                    "application/json",
75                    "{\"error\":\"no telemetry available\"}",
76                )
77            }
78        }
79        "/telemetry" => {
80            let (from, to) = match parse_from_to(query) {
81                Ok(range) => range,
82                Err(err) => {
83                    let body = format!("{{\"error\":\"{err}\"}}");
84                    return write_response(
85                        &mut stream,
86                        "400 Bad Request",
87                        "application/json",
88                        &body,
89                    );
90                }
91            };
92            let rows: Vec<&TelemetryRow> = telemetry
93                .iter()
94                .filter(|row| {
95                    let in_from = from.map(|start| row.timestep >= start).unwrap_or(true);
96                    let in_to = to.map(|end| row.timestep <= end).unwrap_or(true);
97                    in_from && in_to
98                })
99                .collect();
100            let body = serde_json::to_string(&rows)
101                .map_err(|err| io::Error::other(format!("serialize telemetry: {err}")))?;
102            write_response(&mut stream, "200 OK", "application/json", &body)
103        }
104        _ => write_response(
105            &mut stream,
106            "404 Not Found",
107            "application/json",
108            "{\"error\":\"not found\"}",
109        ),
110    }
111}
112
113fn split_target(target: &str) -> (&str, &str) {
114    if let Some((path, query)) = target.split_once('?') {
115        (path, query)
116    } else {
117        (target, "")
118    }
119}
120
121fn parse_from_to(query: &str) -> io::Result<(Option<usize>, Option<usize>)> {
122    let mut from = None;
123    let mut to = None;
124
125    for pair in query.split('&').filter(|entry| !entry.is_empty()) {
126        let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
127        match key {
128            "from" => {
129                from = Some(parse_usize_param("from", value)?);
130            }
131            "to" => {
132                to = Some(parse_usize_param("to", value)?);
133            }
134            _ => {}
135        }
136    }
137
138    if let (Some(start), Some(end)) = (from, to) {
139        if start > end {
140            return Err(io::Error::new(
141                io::ErrorKind::InvalidInput,
142                "query parameter `from` must be <= `to`",
143            ));
144        }
145    }
146
147    Ok((from, to))
148}
149
150fn parse_usize_param(name: &str, value: &str) -> io::Result<usize> {
151    value.parse::<usize>().map_err(|_| {
152        io::Error::new(
153            io::ErrorKind::InvalidInput,
154            format!("query parameter `{name}` must be a non-negative integer"),
155        )
156    })
157}
158
159fn write_response(
160    stream: &mut TcpStream,
161    status: &str,
162    content_type: &str,
163    body: &str,
164) -> io::Result<()> {
165    write!(
166        stream,
167        "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
168        body.len(),
169        body
170    )
171}
172
173#[cfg(test)]
174mod tests {
175    use super::parse_from_to;
176
177    #[test]
178    fn parses_query_range() {
179        assert_eq!(
180            parse_from_to("from=1&to=3").expect("query should parse"),
181            (Some(1), Some(3))
182        );
183        assert_eq!(
184            parse_from_to("").expect("empty query should parse"),
185            (None, None)
186        );
187    }
188
189    #[test]
190    fn rejects_invalid_query_range() {
191        assert!(parse_from_to("from=abc").is_err());
192        assert!(parse_from_to("from=5&to=1").is_err());
193    }
194}