Skip to main content

ove/
net_http.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 HTTP/1.1 client with RAII response management.
8//!
9//! [`Client`] wraps the oveRTOS HTTP client handle with automatic resource
10//! cleanup.  [`Response`] owns the heap-allocated body and headers returned
11//! by `ove_http_get` / `ove_http_post` and frees them on drop.
12//!
13//! Works in both heap and zero-heap modes.
14//!
15//! ## Async alternative
16//!
17//! For async HTTP on top of [`crate::async_net`] use the
18//! [`reqwless`](https://crates.io/crates/reqwless) crate from crates.io.
19//! Supports HTTPS via `embedded-tls`, chunked encoding, redirects, and
20//! produces an `embedded_io_async::Read` body. See [`crate::async_net`]'s
21//! module docs for the full pairing recipe.
22
23use core::fmt;
24
25use crate::bindings;
26use crate::error::{Error, Result};
27
28// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
29// calls below): any handle passed to the C API is non-null and refers to a
30// live RTOS object — wrapper constructors establish validity via
31// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
32// a handle is released. Pointer and slice arguments reference caller-owned
33// memory valid for the duration of the call; the C side copies whatever it
34// retains and does not alias them past return (verified against the
35// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
36// pointer casts from user data, slice reconstruction via `from_raw_parts`,
37// or storing a callback across the FFI boundary — carry their own
38// `// SAFETY:` comment.
39
40// ---------------------------------------------------------------------------
41// Method
42// ---------------------------------------------------------------------------
43
44/// HTTP request method.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Method {
47    /// HTTP GET.
48    Get,
49    /// HTTP POST.
50    Post,
51    /// HTTP PUT.
52    Put,
53    /// HTTP DELETE.
54    Delete,
55    /// HTTP PATCH.
56    Patch,
57}
58
59impl Method {
60    fn to_c(self) -> bindings::ove_http_method_t {
61        match self {
62            Method::Get => bindings::OVE_HTTP_GET,
63            Method::Post => bindings::OVE_HTTP_POST,
64            Method::Put => bindings::OVE_HTTP_PUT,
65            Method::Delete => bindings::OVE_HTTP_DELETE,
66            Method::Patch => bindings::OVE_HTTP_PATCH,
67        }
68    }
69}
70
71// ---------------------------------------------------------------------------
72// Header
73// ---------------------------------------------------------------------------
74
75/// HTTP request header.
76///
77/// Both `name` and `value` must be null-terminated byte strings
78/// (e.g. `b"Authorization\0"`, `b"Bearer token\0"`).
79pub struct Header<'a> {
80    pub name: &'a [u8],
81    pub value: &'a [u8],
82}
83
84// ---------------------------------------------------------------------------
85// ClientStorage
86// ---------------------------------------------------------------------------
87
88/// Backing storage for an HTTP client in zero-heap mode.
89///
90/// In heap mode this is a zero-size placeholder.  Pass a `&mut ClientStorage`
91/// to [`Client::create`] — the borrow checker ensures the storage outlives
92/// the client.
93///
94/// ```ignore
95/// let mut storage = ClientStorage::new();
96/// let client = Client::create(&mut storage)?;
97/// ```
98// FFI handle storage; the field is intentionally only addressed via raw
99// pointers passed to C, so clippy's `field is never read` doesn't apply.
100#[allow(dead_code)]
101pub struct ClientStorage(bindings::ove_http_client_storage_t);
102
103impl Default for ClientStorage {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109impl ClientStorage {
110    /// Zero-initialised backing storage for a [`Client`] in zero-heap
111    /// mode.  Place in a `static` and pass to [`Client::from_static`].
112    pub fn new() -> Self {
113        Self(unsafe { core::mem::zeroed() })
114    }
115}
116
117// ---------------------------------------------------------------------------
118// Response
119// ---------------------------------------------------------------------------
120
121/// HTTP response with RAII lifetime for body and header buffers.
122///
123/// Returned by [`Client::get`] and [`Client::post`].  The body and header
124/// memory is freed automatically when this value is dropped.
125pub struct Response {
126    raw: bindings::ove_http_response_t,
127}
128
129impl Response {
130    /// HTTP status code (e.g. 200, 404).
131    pub fn status(&self) -> i32 {
132        self.raw.status
133    }
134
135    /// Response body as a byte slice (empty if no body).
136    pub fn body(&self) -> &[u8] {
137        if self.raw.body.is_null() || self.raw.body_len == 0 {
138            return &[];
139        }
140        // SAFETY: null/zero-len handled above; `body` points to `body_len`
141        // bytes owned by this `Response` and freed only on its `Drop`, so the
142        // slice is valid for `&self`.
143        unsafe { core::slice::from_raw_parts(self.raw.body as *const u8, self.raw.body_len) }
144    }
145
146    /// Raw response headers as a byte slice (empty if none).
147    pub fn headers(&self) -> &[u8] {
148        if self.raw.headers.is_null() || self.raw.headers_len == 0 {
149            return &[];
150        }
151        // SAFETY: null/zero-len handled above; `headers` points to
152        // `headers_len` bytes owned by this `Response` and freed only on its
153        // `Drop`, so the slice is valid for `&self`.
154        unsafe { core::slice::from_raw_parts(self.raw.headers as *const u8, self.raw.headers_len) }
155    }
156}
157
158impl fmt::Debug for Response {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        f.debug_struct("Response")
161            .field("status", &self.raw.status)
162            .field("body_len", &self.raw.body_len)
163            .field("headers_len", &self.raw.headers_len)
164            .finish()
165    }
166}
167
168impl Drop for Response {
169    fn drop(&mut self) {
170        unsafe { bindings::ove_http_response_free(&mut self.raw) }
171    }
172}
173
174// ---------------------------------------------------------------------------
175// Client
176// ---------------------------------------------------------------------------
177
178/// HTTP/1.1 client.
179///
180/// Wraps `ove_http_client_t` with automatic cleanup on drop.
181pub struct Client {
182    handle: bindings::ove_http_client_t,
183}
184
185impl Client {
186    /// Create a new HTTP client via heap allocation (only in heap mode).
187    #[cfg(not(zero_heap))]
188    pub fn new() -> Result<Self> {
189        let mut handle: bindings::ove_http_client_t = core::ptr::null_mut();
190        let rc = unsafe { bindings::ove_http_client_create(&mut handle) };
191        Error::from_code(rc)?;
192        Ok(Self { handle })
193    }
194
195    /// Create from caller-provided static storage.
196    ///
197    /// # Safety
198    /// Caller must ensure `storage` outlives the `Client` and is not
199    /// shared with another primitive.
200    #[cfg(zero_heap)]
201    pub unsafe fn from_static(storage: *mut bindings::ove_http_client_storage_t) -> Result<Self> {
202        let mut handle: bindings::ove_http_client_t = core::ptr::null_mut();
203        let rc = unsafe { bindings::ove_http_client_init(&mut handle, storage) };
204        Error::from_code(rc)?;
205        Ok(Self { handle })
206    }
207
208    /// Create a client that works in both heap and zero-heap modes.
209    ///
210    /// In heap mode, allocates via the RTOS heap.  In zero-heap mode,
211    /// uses caller-provided `storage`.  The borrow checker ensures the
212    /// storage outlives the client.
213    pub fn create(storage: &mut ClientStorage) -> Result<Self> {
214        #[cfg(not(zero_heap))]
215        {
216            let _ = storage;
217            Self::new()
218        }
219        #[cfg(zero_heap)]
220        {
221            unsafe { Self::from_static(&mut storage.0) }
222        }
223    }
224
225    /// Perform an HTTP GET request.
226    ///
227    /// `url` must be a null-terminated URL (e.g. `b"http://example.com/path\0"`).
228    ///
229    /// # Errors
230    /// Returns an error if the request fails at the transport or protocol level.
231    pub fn get(&self, url: &[u8]) -> Result<Response> {
232        let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
233        let rc = unsafe { bindings::ove_http_get(self.handle, url.as_ptr() as *const _, &mut raw) };
234        Error::from_code(rc)?;
235        Ok(Response { raw })
236    }
237
238    /// Perform an HTTP POST request.
239    ///
240    /// `url` and `content_type` must be null-terminated byte strings.
241    ///
242    /// # Errors
243    /// Returns an error if the request fails at the transport or protocol level.
244    pub fn post(&self, url: &[u8], content_type: &[u8], body: &[u8]) -> Result<Response> {
245        let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
246        let rc = unsafe {
247            bindings::ove_http_post(
248                self.handle,
249                url.as_ptr() as *const _,
250                content_type.as_ptr() as *const _,
251                body.as_ptr() as *const _,
252                body.len(),
253                &mut raw,
254            )
255        };
256        Error::from_code(rc)?;
257        Ok(Response { raw })
258    }
259
260    /// Perform an HTTP request with explicit method, optional body, and
261    /// optional extra headers.
262    ///
263    /// `url` and `content_type` must be null-terminated byte strings.
264    /// Each [`Header`] name/value must also be null-terminated.
265    ///
266    /// # Errors
267    /// Returns an error if the request fails at the transport or protocol level.
268    pub fn request_ex(
269        &self,
270        method: Method,
271        url: &[u8],
272        content_type: &[u8],
273        body: &[u8],
274        headers: &[Header<'_>],
275    ) -> Result<Response> {
276        // Convert Header slices into the C struct array on the stack.
277        const MAX_HEADERS: usize = 16;
278        let count = headers.len().min(MAX_HEADERS);
279        let mut c_headers: [bindings::ove_http_header_t; MAX_HEADERS] =
280            unsafe { core::mem::zeroed() };
281        for i in 0..count {
282            c_headers[i].name = headers[i].name.as_ptr() as *const _;
283            c_headers[i].value = headers[i].value.as_ptr() as *const _;
284        }
285
286        let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
287        let rc = unsafe {
288            bindings::ove_http_request_ex(
289                self.handle,
290                method.to_c(),
291                url.as_ptr() as *const _,
292                content_type.as_ptr() as *const _,
293                body.as_ptr() as *const _,
294                body.len(),
295                c_headers.as_ptr(),
296                count,
297                &mut raw,
298            )
299        };
300        Error::from_code(rc)?;
301        Ok(Response { raw })
302    }
303}
304
305crate::ove_handle_impl!(Client, ove_http_client_destroy, ove_http_client_deinit);