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 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}