1use core::fmt::Write as _;
16use core::str;
17
18use embassy_executor::Spawner;
19use embassy_net::Stack;
20use embassy_net::tcp::{State, TcpSocket};
21use embassy_time::{Duration, Instant, Timer, with_timeout};
22use embedded_io_async::Write as _;
23use heapless::String;
24use static_cell::StaticCell;
25
26use crate::config::ConfigPatch;
27use crate::storage;
28
29use super::ws;
30
31pub const HTTP_PORT: u16 = 80;
33
34const SLOTS: usize = 4;
37
38const RX_SIZE: usize = 1536;
40
41const TX_SIZE: usize = 1024;
43
44const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
46
47static INDEX_HTML: &[u8] =
49 include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/web/dist/index.html"));
50
51static FREDOKA_TTF: &[u8] = include_bytes!(concat!(
53 env!("CARGO_MANIFEST_DIR"),
54 "/web/dist/fonts/Fredoka.ttf"
55));
56
57macro_rules! slot_task {
58 ($(#[$meta:meta])* $task_name:ident, $rx_name:ident, $tx_name:ident) => {
59 $(#[$meta])*
60 #[embassy_executor::task]
61 pub async fn $task_name(stack: Stack<'static>) {
62 static $rx_name: StaticCell<[u8; RX_SIZE]> = StaticCell::new();
63 static $tx_name: StaticCell<[u8; TX_SIZE]> = StaticCell::new();
64 serve(
65 stack,
66 $rx_name.init([0; RX_SIZE]),
67 $tx_name.init([0; TX_SIZE]),
68 )
69 .await;
70 }
71 };
72}
73
74slot_task!(
75 slot0,
77 SLOT0_RX,
78 SLOT0_TX
79);
80slot_task!(
81 slot1,
83 SLOT1_RX,
84 SLOT1_TX
85);
86slot_task!(
87 slot2,
89 SLOT2_RX,
90 SLOT2_TX
91);
92slot_task!(
93 slot3,
95 SLOT3_RX,
96 SLOT3_TX
97);
98
99pub fn spawn_all(spawner: Spawner, stack: Stack<'static>) {
103 macro_rules! spawn_slot {
104 ($result:expr) => {
105 match $result {
106 Ok(token) => spawner.spawn(token),
107 Err(err) => log::error!("http: slot task build failed: {err:?}"),
108 }
109 };
110 }
111 spawn_slot!(slot0(stack));
112 spawn_slot!(slot1(stack));
113 spawn_slot!(slot2(stack));
114 spawn_slot!(slot3(stack));
115}
116
117async fn serve(stack: Stack<'static>, rx: &'static mut [u8], tx: &'static mut [u8]) {
119 let mut socket = TcpSocket::new(stack, rx, tx);
120 socket.set_timeout(Some(REQUEST_TIMEOUT));
121 socket.set_nagle_enabled(false);
122 loop {
123 if let Err(err) = socket.accept(HTTP_PORT).await {
124 log::warn!("http: accept error: {err:?}");
125 Timer::after(Duration::from_millis(100)).await;
126 continue;
127 }
128 match with_timeout(REQUEST_TIMEOUT, route(&mut socket)).await {
129 Ok(Ok(Outcome::Upgrade(key))) => {
130 socket.set_timeout(None);
133 if ws::serve(&mut socket, &key).await.is_err() {
134 log::debug!("http: websocket session ended abnormally");
135 }
136 }
137 Ok(Ok(Outcome::Handled)) => {}
138 Ok(Err(())) => log::debug!("http: request failed"),
139 Err(_) => log::debug!("http: request timed out"),
140 }
141 socket.set_timeout(Some(REQUEST_TIMEOUT));
142 recycle(&mut socket).await;
143 }
144}
145
146async fn recycle(socket: &mut TcpSocket<'_>) {
148 socket.close();
149 let deadline = Instant::now().saturating_add(Duration::from_secs(3));
150 while socket.state() != State::Closed && Instant::now() < deadline {
151 Timer::after(Duration::from_millis(50)).await;
152 }
153 if socket.state() != State::Closed {
154 socket.abort();
155 Timer::after(Duration::from_millis(20)).await;
156 }
157}
158
159enum Outcome {
161 Handled,
163 Upgrade(String<32>),
165}
166
167enum ReadError {
169 Unreadable,
171 BadLength,
173 TooLarge,
175}
176
177async fn route(socket: &mut TcpSocket<'_>) -> Result<Outcome, ()> {
179 let mut buf = [0u8; RX_SIZE];
180 let n = match read_request(socket, &mut buf).await {
181 Ok(n) => n,
182 Err(ReadError::Unreadable) => return Err(()),
183 Err(ReadError::BadLength) => {
184 log::warn!("http: malformed Content-Length");
185 send_status(socket, 400).await?;
186 return Ok(Outcome::Handled);
187 }
188 Err(ReadError::TooLarge) => {
189 log::warn!("http: payload exceeds request buffer");
190 send_status(socket, 413).await?;
191 return Ok(Outcome::Handled);
192 }
193 };
194 let head_end = find_headers_end(buf.get(..n).ok_or(())?).ok_or(())?;
195 let headers = buf.get(..head_end).ok_or(())?;
196 let Ok(parsed) = parse_request_line(headers) else {
197 log::warn!("http: malformed request line");
198 send_status(socket, 400).await?;
199 return Ok(Outcome::Handled);
200 };
201 let (method, path) = parsed;
202
203 if method == "GET"
205 && path == "/ws"
206 && let Some(key) = ws_key(headers)
207 {
208 return Ok(Outcome::Upgrade(key));
209 }
210
211 let Ok(body) = parse_body(buf.get(..n).ok_or(())?, head_end) else {
212 log::warn!("http: malformed request body");
213 send_status(socket, 400).await?;
214 return Ok(Outcome::Handled);
215 };
216 let status: u16 = match (method, path) {
217 ("GET", "/" | "/index.html") => {
218 send_response(socket, 200, "text/html; charset=utf-8", INDEX_HTML).await?;
219 200
220 }
221 ("GET", "/fonts/Fredoka.ttf") => {
222 send_response(socket, 200, "font/ttf", FREDOKA_TTF).await?;
223 200
224 }
225 ("GET", "/api/config") => {
226 get_config(socket).await?;
227 200
228 }
229 ("POST", "/api/config") => post_config(socket, body).await?,
230 _ => {
231 send_status(socket, 404).await?;
232 404
233 }
234 };
235 if path.starts_with("/api/") {
236 log::info!("http: {method} {path} -> {status}");
237 } else {
238 log::debug!("http: {method} {path} -> {status}");
239 }
240 Ok(Outcome::Handled)
241}
242
243async fn read_request(socket: &mut TcpSocket<'_>, buf: &mut [u8]) -> Result<usize, ReadError> {
245 let mut filled = 0usize;
246 loop {
247 let Some(chunk) = buf.get_mut(filled..) else {
248 return Err(ReadError::TooLarge);
249 };
250 let read = socket
251 .read(chunk)
252 .await
253 .map_err(|_| ReadError::Unreadable)?;
254 if read == 0 {
255 return Err(ReadError::Unreadable);
256 }
257 filled = filled.checked_add(read).ok_or(ReadError::Unreadable)?;
258 if let Some(head_end) = find_headers_end(buf.get(..filled).ok_or(ReadError::Unreadable)?) {
259 let announced = content_length(buf.get(..head_end).ok_or(ReadError::Unreadable)?)
260 .map_err(|()| ReadError::BadLength)?;
261 let total = head_end.checked_add(announced).ok_or(ReadError::TooLarge)?;
262 if total > buf.len() {
263 return Err(ReadError::TooLarge);
264 }
265 if filled >= total {
266 return Ok(filled);
267 }
268 }
269 }
270}
271
272fn find_headers_end(buf: &[u8]) -> Option<usize> {
274 buf.windows(4).position(|w| w == b"\r\n\r\n")
275}
276
277fn content_length(headers: &[u8]) -> Result<usize, ()> {
279 const NEEDLE: &[u8] = b"content-length:";
281 for line in headers.split(|b| *b == b'\n') {
282 let line = trim(line);
283 if starts_with_ci(line, NEEDLE) {
284 let value = trim(line.get(NEEDLE.len()..).ok_or(())?);
285 let text = str::from_utf8(value).map_err(|_| ())?;
286 return text.parse().map_err(|_| ());
287 }
288 }
289 Ok(0)
290}
291
292fn parse_request_line(headers: &[u8]) -> Result<(&str, &str), ()> {
295 let line_end = headers
296 .windows(2)
297 .position(|w| w == b"\r\n")
298 .unwrap_or(headers.len());
299 let line = str::from_utf8(headers.get(..line_end).ok_or(())?).map_err(|_| ())?;
300 let mut parts = line.split(' ');
301 let method = parts.next().ok_or(())?;
302 let path = parts.next().ok_or(())?;
303 if method.is_empty() || path.is_empty() {
304 return Err(());
305 }
306 Ok((method, path))
307}
308
309fn parse_body(buf: &[u8], head_end: usize) -> Result<&[u8], ()> {
311 let start = head_end.checked_add(4).ok_or(())?;
312 let announced = content_length(buf.get(..head_end).ok_or(())?)?;
313 let end = start.checked_add(announced).ok_or(())?;
314 buf.get(start..end).ok_or(())
315}
316
317fn starts_with_ci(line: &[u8], prefix: &[u8]) -> bool {
319 line.len() >= prefix.len()
320 && line
321 .get(..prefix.len())
322 .is_some_and(|head| head.eq_ignore_ascii_case(prefix))
323}
324
325fn trim(mut bytes: &[u8]) -> &[u8] {
327 while bytes.first().is_some_and(u8::is_ascii_whitespace) {
328 bytes = bytes.get(1..).unwrap_or(bytes);
329 }
330 while bytes.last().is_some_and(u8::is_ascii_whitespace) {
331 bytes = bytes.get(..bytes.len().saturating_sub(1)).unwrap_or(bytes);
332 }
333 bytes
334}
335
336fn ws_key(headers: &[u8]) -> Option<String<32>> {
338 const NEEDLE: &[u8] = b"sec-websocket-key:";
340 for line in headers.split(|b| *b == b'\n') {
341 let line = trim(line);
342 if starts_with_ci(line, NEEDLE) {
343 let value = trim(line.get(NEEDLE.len()..)?);
344 let key = str::from_utf8(value).ok()?;
345 return key.trim().parse().ok();
346 }
347 }
348 None
349}
350
351async fn get_config(socket: &mut TcpSocket<'_>) -> Result<(), ()> {
353 let config = storage::load();
354 let mut body = [0u8; 768];
355 let n = serde_json_core::to_slice(&config, &mut body).map_err(|_| ())?;
356 send_response(socket, 200, "application/json", body.get(..n).ok_or(())?).await
357}
358
359async fn post_config(socket: &mut TcpSocket<'_>, body: &[u8]) -> Result<u16, ()> {
361 let (patch, _used): (ConfigPatch, usize) = match serde_json_core::from_slice(body) {
362 Ok(parsed) => parsed,
363 Err(err) => {
364 log::warn!("http: bad config patch: {err}");
365 send_status(socket, 400).await?;
366 return Ok(400);
367 }
368 };
369 let mut config = storage::load();
370 if !config.merge_patch(patch) {
371 log::warn!("http: config patch rejected");
372 send_status(socket, 400).await?;
373 return Ok(400);
374 }
375 if !storage::save(&config) {
376 send_status(socket, 500).await?;
377 return Ok(500);
378 }
379 config.apply();
380 log::info!("http: config saved via REST");
381 send_response(socket, 200, "application/json", b"{}").await?;
382 Ok(200)
383}
384
385async fn send_status(socket: &mut TcpSocket<'_>, status: u16) -> Result<(), ()> {
387 let reason = match status {
388 400 => "Bad Request",
389 404 => "Not Found",
390 413 => "Payload Too Large",
391 500 => "Internal Server Error",
392 _ => "OK",
393 };
394 let mut head = String::<96>::new();
396 write!(
397 &mut head,
398 "HTTP/1.1 {status} {reason}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
399 )
400 .map_err(|_| ())?;
401 socket.write_all(head.as_bytes()).await.map_err(|_| ())
402}
403
404async fn send_response(
406 socket: &mut TcpSocket<'_>,
407 status: u16,
408 content_type: &str,
409 body: &[u8],
410) -> Result<(), ()> {
411 let mut head = String::<128>::new();
412 write!(
413 &mut head,
414 "HTTP/1.1 {status} OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
415 body.len()
416 )
417 .map_err(|_| ())?;
418 socket.write_all(head.as_bytes()).await.map_err(|_| ())?;
419 for chunk in body.chunks(TX_SIZE) {
420 socket.write_all(chunk).await.map_err(|_| ())?;
421 }
422 Ok(())
423}