Skip to main content

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}