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//! 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
16use core::fmt;
17use core::marker::PhantomData;
18
19use crate::bindings;
20use crate::error::{Error, Result};
21
22// ---------------------------------------------------------------------------
23// Handler type
24// ---------------------------------------------------------------------------
25
26/// Route handler callback.
27///
28/// This is the raw C function pointer type:
29/// `fn(req: *mut ove_httpd_req_t, resp: *mut ove_httpd_resp_t) -> i32`.
30pub type Handler = bindings::ove_httpd_handler_t;
31
32// ---------------------------------------------------------------------------
33// Request
34// ---------------------------------------------------------------------------
35
36/// HTTP request passed to a route handler.
37///
38/// Borrows the underlying C request for the duration of the handler call.
39/// Do not store this value beyond the handler invocation.
40pub struct Request<'a> {
41    raw: *mut bindings::ove_httpd_req_t,
42    _marker: PhantomData<&'a ()>,
43}
44
45impl<'a> Request<'a> {
46    /// Create a `Request` from a raw C pointer.
47    ///
48    /// # Safety
49    /// `raw` must be a valid pointer obtained from an active httpd handler
50    /// callback and must remain valid for the lifetime `'a`.
51    pub unsafe fn from_raw(raw: *mut bindings::ove_httpd_req_t) -> Self {
52        Self { raw, _marker: PhantomData }
53    }
54
55    /// The HTTP method string (e.g. "GET", "POST").
56    pub fn method(&self) -> &[u8] {
57        let p = unsafe { bindings::ove_httpd_req_method(self.raw) };
58        if p.is_null() { return &[]; }
59        unsafe { core::ffi::CStr::from_ptr(p).to_bytes() }
60    }
61
62    /// The full request path (e.g. "/api/leds/0").
63    pub fn path(&self) -> &[u8] {
64        let p = unsafe { bindings::ove_httpd_req_path(self.raw) };
65        if p.is_null() { return &[]; }
66        unsafe { core::ffi::CStr::from_ptr(p).to_bytes() }
67    }
68
69    /// The query string after '?' (empty slice if none).
70    pub fn query(&self) -> &[u8] {
71        let p = unsafe { bindings::ove_httpd_req_query(self.raw) };
72        if p.is_null() { return &[]; }
73        unsafe { core::ffi::CStr::from_ptr(p).to_bytes() }
74    }
75
76    /// The request body (empty slice if none).
77    pub fn body(&self) -> &[u8] {
78        let p = unsafe { bindings::ove_httpd_req_body(self.raw) };
79        let len = unsafe { bindings::ove_httpd_req_body_len(self.raw) };
80        if p.is_null() || len == 0 { return &[]; }
81        unsafe { core::slice::from_raw_parts(p as *const u8, len) }
82    }
83
84    /// A path segment by index.
85    ///
86    /// For path "/api/leds/0": segment 0 = "api", 1 = "leds", 2 = "0".
87    /// Returns `None` if the index is out of range.
88    pub fn segment(&self, idx: i32) -> Option<&[u8]> {
89        let p = unsafe { bindings::ove_httpd_req_segment(self.raw, idx) };
90        if p.is_null() { return None; }
91        Some(unsafe { core::ffi::CStr::from_ptr(p).to_bytes() })
92    }
93}
94
95impl fmt::Debug for Request<'_> {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("Request")
98            .field("path", &core::str::from_utf8(self.path()).unwrap_or("<non-utf8>"))
99            .finish()
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Response
105// ---------------------------------------------------------------------------
106
107/// HTTP response writer passed to a route handler.
108///
109/// Borrows the underlying C response for the duration of the handler call.
110/// Do not store this value beyond the handler invocation.
111pub struct Response<'a> {
112    raw: *mut bindings::ove_httpd_resp_t,
113    _marker: PhantomData<&'a ()>,
114}
115
116impl<'a> Response<'a> {
117    /// Create a `Response` from a raw C pointer.
118    ///
119    /// # Safety
120    /// `raw` must be a valid pointer obtained from an active httpd handler
121    /// callback and must remain valid for the lifetime `'a`.
122    pub unsafe fn from_raw(raw: *mut bindings::ove_httpd_resp_t) -> Self {
123        Self { raw, _marker: PhantomData }
124    }
125
126    /// Send a JSON response.
127    ///
128    /// `json` must be a null-terminated byte string.
129    ///
130    /// # Errors
131    /// Returns an error if the response cannot be sent.
132    pub fn json(&self, status: i32, json: &[u8]) -> Result<()> {
133        let rc = unsafe {
134            bindings::ove_httpd_resp_json(self.raw, status, json.as_ptr() as *const _)
135        };
136        Error::from_code(rc)
137    }
138
139    /// Send an HTML response.
140    ///
141    /// # Errors
142    /// Returns an error if the response cannot be sent.
143    pub fn html(&self, status: i32, html: &[u8]) -> Result<()> {
144        let rc = unsafe {
145            bindings::ove_httpd_resp_html(
146                self.raw,
147                status,
148                html.as_ptr() as *const _,
149                html.len(),
150            )
151        };
152        Error::from_code(rc)
153    }
154
155    /// Send a response with arbitrary content type.
156    ///
157    /// `content_type` must be a null-terminated byte string.
158    ///
159    /// # Errors
160    /// Returns an error if the response cannot be sent.
161    pub fn send(
162        &self,
163        status: i32,
164        content_type: &[u8],
165        body: &[u8],
166    ) -> Result<()> {
167        let rc = unsafe {
168            bindings::ove_httpd_resp_send(
169                self.raw,
170                status,
171                content_type.as_ptr() as *const _,
172                body.as_ptr() as *const _,
173                body.len(),
174            )
175        };
176        Error::from_code(rc)
177    }
178
179    /// Send a JSON error response.
180    ///
181    /// `msg` must be a null-terminated byte string.
182    ///
183    /// # Errors
184    /// Returns an error if the response cannot be sent.
185    pub fn error(&self, status: i32, msg: &[u8]) -> Result<()> {
186        let rc = unsafe {
187            bindings::ove_httpd_resp_error(self.raw, status, msg.as_ptr() as *const _)
188        };
189        Error::from_code(rc)
190    }
191
192    /// Send a pre-gzipped response (adds `Content-Encoding: gzip`).
193    ///
194    /// `content_type` must be a null-terminated byte string.
195    ///
196    /// # Errors
197    /// Returns an error if the response cannot be sent.
198    pub fn send_gz(
199        &self,
200        status: i32,
201        content_type: &[u8],
202        body: &[u8],
203    ) -> Result<()> {
204        let rc = unsafe {
205            bindings::ove_httpd_resp_send_gz(
206                self.raw,
207                status,
208                content_type.as_ptr() as *const _,
209                body.as_ptr() as *const _,
210                body.len(),
211            )
212        };
213        Error::from_code(rc)
214    }
215}
216
217impl fmt::Debug for Response<'_> {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.debug_struct("Response")
220            .field("raw", &format_args!("{:p}", self.raw))
221            .finish()
222    }
223}
224
225// ---------------------------------------------------------------------------
226// Server free functions
227// ---------------------------------------------------------------------------
228
229/// Start the HTTP server on the given port.
230///
231/// Spawns a background task that accepts connections.  Register routes
232/// with [`route`] before or after starting.
233///
234/// # Errors
235/// Returns an error if the server cannot bind or start.
236pub fn start(port: u16, max_body: i32) -> Result<()> {
237    let cfg = bindings::ove_httpd_config_t {
238        port,
239        max_body_size: max_body,
240    };
241    let rc = unsafe { bindings::ove_httpd_start(&cfg) };
242    Error::from_code(rc)
243}
244
245/// Stop the HTTP server and close the listening socket.
246pub fn stop() {
247    unsafe { bindings::ove_httpd_stop() }
248}
249
250/// Register a route handler.
251///
252/// `method` and `path` must be null-terminated byte strings
253/// (e.g. `b"GET\0"`, `b"/api/leds\0"`).
254///
255/// # Errors
256/// Returns an error if the route table is full.
257pub fn route(method: &[u8], path: &[u8], handler: Handler) -> Result<()> {
258    let rc = unsafe {
259        bindings::ove_httpd_route(
260            method.as_ptr() as *const _,
261            path.as_ptr() as *const _,
262            handler,
263        )
264    };
265    Error::from_code(rc)
266}
267
268/// Register the built-in dashboard routes (`/api/info`, `/api/leds`, etc.).
269///
270/// Call after [`start`] to add the standard device management API.
271pub fn register_builtin_routes() {
272    unsafe { bindings::ove_httpd_register_builtin_routes() }
273}
274
275/// Append a log line to the httpd log ring buffer.
276///
277/// Call from a log output hook to capture lines for `GET /api/log`.
278pub fn log_append(line: &[u8]) {
279    unsafe { bindings::ove_httpd_log_append(line.as_ptr() as *const _) }
280}
281
282/// Bind the HTTP server to a specific network interface.
283pub fn set_netif(handle: bindings::ove_netif_t) {
284    unsafe { bindings::ove_httpd_set_netif(handle) }
285}
286
287// ---------------------------------------------------------------------------
288// WebSocket support
289// ---------------------------------------------------------------------------
290
291#[cfg(has_net_httpd_ws)]
292pub mod ws {
293    use crate::bindings;
294    use crate::error::{Error, Result};
295
296    /// Register a WebSocket route.
297    ///
298    /// `path` must be a null-terminated byte string (e.g. `b"/ws\0"`).
299    ///
300    /// # Errors
301    /// Returns an error if the route table is full.
302    pub fn route(
303        path: &[u8],
304        on_message: bindings::ove_httpd_ws_handler_t,
305        on_close: bindings::ove_httpd_ws_close_handler_t,
306    ) -> Result<()> {
307        let rc = unsafe {
308            bindings::ove_httpd_ws_route(
309                path.as_ptr() as *const _,
310                on_message,
311                on_close,
312            )
313        };
314        Error::from_code(rc)
315    }
316
317    /// Send a text message to a WebSocket connection.
318    ///
319    /// # Errors
320    /// Returns an error if the send fails.
321    pub fn send(conn: *mut bindings::ove_httpd_ws_conn_t, data: &[u8]) -> Result<()> {
322        let rc = unsafe {
323            bindings::ove_httpd_ws_send(conn, data.as_ptr() as *const _, data.len())
324        };
325        Error::from_code(rc)
326    }
327
328    /// Broadcast a message to all WebSocket connections on a path.
329    ///
330    /// `path` must be a null-terminated byte string.
331    ///
332    /// Returns the number of connections the message was sent to.
333    pub fn broadcast(path: &[u8], data: &[u8]) -> i32 {
334        unsafe {
335            bindings::ove_httpd_ws_broadcast(
336                path.as_ptr() as *const _,
337                data.as_ptr() as *const _,
338                data.len(),
339            )
340        }
341    }
342
343    /// Return the number of active WebSocket connections.
344    pub fn active_count() -> i32 {
345        unsafe { bindings::ove_httpd_ws_active_count() }
346    }
347}