ove/net_httpd.rs
1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Blocking embedded HTTP server with REST-style routing.
8//!
9//! Wraps the oveRTOS `ove_httpd_*` API. The server runs on a background
10//! task; routes are registered with [`route`] and dispatched to
11//! [`Handler`] callbacks that receive a [`Request`] / [`Response`] pair.
12//!
13//! The server is a singleton (one per process), so start/stop/route are
14//! free functions rather than methods on a struct.
15//!
16//! ## Async alternative
17//!
18//! For an async HTTP server on top of [`crate::async_net`] use
19//! [`picoserve`](https://crates.io/crates/picoserve) from crates.io.
20//! Macro-driven routing, JSON via `serde`, optional WebSocket upgrade.
21//! See [`crate::async_net`]'s module docs for the full pairing recipe.
22
23use core::fmt;
24use core::marker::PhantomData;
25
26use crate::bindings;
27use crate::error::{Error, Result};
28
29// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
30// calls below): any handle passed to the C API is non-null and refers to a
31// live RTOS object — wrapper constructors establish validity via
32// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
33// a handle is released. Pointer and slice arguments reference caller-owned
34// memory valid for the duration of the call; the C side copies whatever it
35// retains and does not alias them past return (verified against the
36// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
37// pointer casts from user data, slice reconstruction via `from_raw_parts`,
38// or storing a callback across the FFI boundary — carry their own
39// `// SAFETY:` comment.
40
41// ---------------------------------------------------------------------------
42// Handler type
43// ---------------------------------------------------------------------------
44
45/// Route handler callback.
46///
47/// This is the raw C function pointer type:
48/// `fn(req: *mut ove_httpd_req_t, resp: *mut ove_httpd_resp_t) -> i32`.
49pub type Handler = bindings::ove_httpd_handler_t;
50
51// ---------------------------------------------------------------------------
52// Request
53// ---------------------------------------------------------------------------
54
55/// HTTP request passed to a route handler.
56///
57/// Borrows the underlying C request for the duration of the handler call.
58/// Do not store this value beyond the handler invocation.
59pub struct Request<'a> {
60 raw: *mut bindings::ove_httpd_req_t,
61 _marker: PhantomData<&'a ()>,
62}
63
64impl<'a> Request<'a> {
65 /// Create a `Request` from a raw C pointer.
66 ///
67 /// # Safety
68 /// `raw` must be a valid pointer obtained from an active httpd handler
69 /// callback and must remain valid for the lifetime `'a`.
70 pub unsafe fn from_raw(raw: *mut bindings::ove_httpd_req_t) -> Self {
71 Self {
72 raw,
73 _marker: PhantomData,
74 }
75 }
76
77 /// The HTTP method string (e.g. "GET", "POST").
78 pub fn method(&self) -> &[u8] {
79 let p = unsafe { bindings::ove_httpd_req_method(self.raw) };
80 if p.is_null() {
81 return &[];
82 }
83 unsafe { core::ffi::CStr::from_ptr(p).to_bytes() }
84 }
85
86 /// The full request path (e.g. "/api/leds/0").
87 pub fn path(&self) -> &[u8] {
88 let p = unsafe { bindings::ove_httpd_req_path(self.raw) };
89 if p.is_null() {
90 return &[];
91 }
92 unsafe { core::ffi::CStr::from_ptr(p).to_bytes() }
93 }
94
95 /// The query string after '?' (empty slice if none).
96 pub fn query(&self) -> &[u8] {
97 let p = unsafe { bindings::ove_httpd_req_query(self.raw) };
98 if p.is_null() {
99 return &[];
100 }
101 unsafe { core::ffi::CStr::from_ptr(p).to_bytes() }
102 }
103
104 /// The request body (empty slice if none).
105 pub fn body(&self) -> &[u8] {
106 let p = unsafe { bindings::ove_httpd_req_body(self.raw) };
107 let len = unsafe { bindings::ove_httpd_req_body_len(self.raw) };
108 if p.is_null() || len == 0 {
109 return &[];
110 }
111 // SAFETY: null/zero-len handled above; `p` points to `len` bytes of
112 // request body owned by the C request object, valid for `&self` (the
113 // handler borrow that produced this `Request`).
114 unsafe { core::slice::from_raw_parts(p as *const u8, len) }
115 }
116
117 /// A path segment by index.
118 ///
119 /// For path "/api/leds/0": segment 0 = "api", 1 = "leds", 2 = "0".
120 /// Returns `None` if the index is out of range.
121 pub fn segment(&self, idx: i32) -> Option<&[u8]> {
122 let p = unsafe { bindings::ove_httpd_req_segment(self.raw, idx) };
123 if p.is_null() {
124 return None;
125 }
126 Some(unsafe { core::ffi::CStr::from_ptr(p).to_bytes() })
127 }
128}
129
130impl fmt::Debug for Request<'_> {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.debug_struct("Request")
133 .field(
134 "path",
135 &core::str::from_utf8(self.path()).unwrap_or("<non-utf8>"),
136 )
137 .finish()
138 }
139}
140
141// ---------------------------------------------------------------------------
142// Response
143// ---------------------------------------------------------------------------
144
145/// HTTP response writer passed to a route handler.
146///
147/// Borrows the underlying C response for the duration of the handler call.
148/// Do not store this value beyond the handler invocation.
149pub struct Response<'a> {
150 raw: *mut bindings::ove_httpd_resp_t,
151 _marker: PhantomData<&'a ()>,
152}
153
154impl<'a> Response<'a> {
155 /// Create a `Response` from a raw C pointer.
156 ///
157 /// # Safety
158 /// `raw` must be a valid pointer obtained from an active httpd handler
159 /// callback and must remain valid for the lifetime `'a`.
160 pub unsafe fn from_raw(raw: *mut bindings::ove_httpd_resp_t) -> Self {
161 Self {
162 raw,
163 _marker: PhantomData,
164 }
165 }
166
167 /// Send a JSON response.
168 ///
169 /// `json` must be a null-terminated byte string.
170 ///
171 /// # Errors
172 /// Returns an error if the response cannot be sent.
173 pub fn json(&self, status: i32, json: &[u8]) -> Result<()> {
174 let rc =
175 unsafe { bindings::ove_httpd_resp_json(self.raw, status, json.as_ptr() as *const _) };
176 Error::from_code(rc)
177 }
178
179 /// Send an HTML response.
180 ///
181 /// # Errors
182 /// Returns an error if the response cannot be sent.
183 pub fn html(&self, status: i32, html: &[u8]) -> Result<()> {
184 let rc = unsafe {
185 bindings::ove_httpd_resp_html(self.raw, status, html.as_ptr() as *const _, html.len())
186 };
187 Error::from_code(rc)
188 }
189
190 /// Send a response with arbitrary content type.
191 ///
192 /// `content_type` must be a null-terminated byte string.
193 ///
194 /// # Errors
195 /// Returns an error if the response cannot be sent.
196 pub fn send(&self, status: i32, content_type: &[u8], body: &[u8]) -> Result<()> {
197 let rc = unsafe {
198 bindings::ove_httpd_resp_send(
199 self.raw,
200 status,
201 content_type.as_ptr() as *const _,
202 body.as_ptr() as *const _,
203 body.len(),
204 )
205 };
206 Error::from_code(rc)
207 }
208
209 /// Send a JSON error response.
210 ///
211 /// `msg` must be a null-terminated byte string.
212 ///
213 /// # Errors
214 /// Returns an error if the response cannot be sent.
215 pub fn error(&self, status: i32, msg: &[u8]) -> Result<()> {
216 let rc =
217 unsafe { bindings::ove_httpd_resp_error(self.raw, status, msg.as_ptr() as *const _) };
218 Error::from_code(rc)
219 }
220
221 /// Send a pre-gzipped response (adds `Content-Encoding: gzip`).
222 ///
223 /// `content_type` must be a null-terminated byte string.
224 ///
225 /// # Errors
226 /// Returns an error if the response cannot be sent.
227 pub fn send_gz(&self, status: i32, content_type: &[u8], body: &[u8]) -> Result<()> {
228 let rc = unsafe {
229 bindings::ove_httpd_resp_send_gz(
230 self.raw,
231 status,
232 content_type.as_ptr() as *const _,
233 body.as_ptr() as *const _,
234 body.len(),
235 )
236 };
237 Error::from_code(rc)
238 }
239}
240
241impl fmt::Debug for Response<'_> {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 f.debug_struct("Response")
244 .field("raw", &format_args!("{:p}", self.raw))
245 .finish()
246 }
247}
248
249// ---------------------------------------------------------------------------
250// Server free functions
251// ---------------------------------------------------------------------------
252
253/// Start the HTTP server on the given port.
254///
255/// Spawns a background task that accepts connections. Register routes
256/// with [`route`] before or after starting.
257///
258/// # Errors
259/// Returns an error if the server cannot bind or start.
260pub fn start(port: u16, max_body: i32) -> Result<()> {
261 let cfg = bindings::ove_httpd_config_t {
262 port,
263 max_body_size: max_body,
264 };
265 let rc = unsafe { bindings::ove_httpd_start(&cfg) };
266 Error::from_code(rc)
267}
268
269/// Stop the HTTP server and close the listening socket.
270pub fn stop() {
271 unsafe { bindings::ove_httpd_stop() }
272}
273
274/// Register a route handler.
275///
276/// `method` and `path` must be null-terminated byte strings
277/// (e.g. `b"GET\0"`, `b"/api/leds\0"`).
278///
279/// # Errors
280/// Returns an error if the route table is full.
281pub fn route(method: &[u8], path: &[u8], handler: Handler) -> Result<()> {
282 let rc = unsafe {
283 bindings::ove_httpd_route(
284 method.as_ptr() as *const _,
285 path.as_ptr() as *const _,
286 handler,
287 )
288 };
289 Error::from_code(rc)
290}
291
292/// Register the built-in dashboard routes (`/api/info`, `/api/leds`, etc.).
293///
294/// Call after [`start`] to add the standard device management API.
295pub fn register_builtin_routes() {
296 unsafe { bindings::ove_httpd_register_builtin_routes() }
297}
298
299/// Append a log line to the httpd log ring buffer.
300///
301/// Call from a log output hook to capture lines for `GET /api/log`.
302pub fn log_append(line: &[u8]) {
303 unsafe { bindings::ove_httpd_log_append(line.as_ptr() as *const _) }
304}
305
306/// Bind the HTTP server to a specific network interface.
307pub fn set_netif(handle: bindings::ove_netif_t) {
308 unsafe { bindings::ove_httpd_set_netif(handle) }
309}
310
311// ---------------------------------------------------------------------------
312// WebSocket support
313// ---------------------------------------------------------------------------
314
315#[cfg(has_net_httpd_ws)]
316pub mod ws {
317 use crate::bindings;
318 use crate::error::{Error, Result};
319
320 /// Register a WebSocket route.
321 ///
322 /// `path` must be a null-terminated byte string (e.g. `b"/ws\0"`).
323 ///
324 /// # Errors
325 /// Returns an error if the route table is full.
326 pub fn route(
327 path: &[u8],
328 on_message: bindings::ove_httpd_ws_handler_t,
329 on_close: bindings::ove_httpd_ws_close_handler_t,
330 ) -> Result<()> {
331 let rc = unsafe {
332 bindings::ove_httpd_ws_route(path.as_ptr() as *const _, on_message, on_close)
333 };
334 Error::from_code(rc)
335 }
336
337 /// Send a text message to a WebSocket connection.
338 ///
339 /// # Errors
340 /// Returns an error if the send fails.
341 pub fn send(conn: *mut bindings::ove_httpd_ws_conn_t, data: &[u8]) -> Result<()> {
342 let rc =
343 unsafe { bindings::ove_httpd_ws_send(conn, data.as_ptr() as *const _, data.len()) };
344 Error::from_code(rc)
345 }
346
347 /// Broadcast a message to all WebSocket connections on a path.
348 ///
349 /// `path` must be a null-terminated byte string.
350 ///
351 /// Returns the number of connections the message was sent to.
352 pub fn broadcast(path: &[u8], data: &[u8]) -> i32 {
353 unsafe {
354 bindings::ove_httpd_ws_broadcast(
355 path.as_ptr() as *const _,
356 data.as_ptr() as *const _,
357 data.len(),
358 )
359 }
360 }
361
362 /// Return the number of active WebSocket connections.
363 pub fn active_count() -> i32 {
364 unsafe { bindings::ove_httpd_ws_active_count() }
365 }
366}