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}