CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
host_api_http.cpp
Go to the documentation of this file.
1
16
17#include "plugin_manager/Raii.h"
20#include "cdc_log.h"
21#include "esp_http_client.h"
22#include "esp_crt_bundle.h"
23#include "esp_err.h"
24
25#include <cstdlib>
26#include <cstring>
27#include <memory>
28#include <string>
29#include <type_traits>
30
31extern "C" void* plg_get_active_plugin(void);
32
33namespace {
34
35struct EspHttpClientDeleter {
36 void operator()(esp_http_client_handle_t h) const noexcept
37 {
38 if (h) esp_http_client_cleanup(h);
39 }
40};
41
42using EspHttpClient = std::unique_ptr<
43 std::remove_pointer_t<esp_http_client_handle_t>, EspHttpClientDeleter>;
45
46struct HttpSlot {
47 bool used = false;
48 void* owner = nullptr; // plugin that opened the request
49 // The client used for this request: either borrowed from the connection pool
50 // (`pool_idx >= 0`) or, when the pool is full, a transient client owned by
51 // `owned` (`pool_idx < 0`).
52 esp_http_client_handle_t client = nullptr;
53 int pool_idx = -1;
54 EspHttpClient owned;
55 HttpBody body;
56 size_t body_len = 0;
57 size_t body_cap = 0;
58 size_t body_cursor = 0;
59 int status = 0;
60 size_t content_length = 0;
61};
62
63constexpr size_t MAX_HTTP_SLOTS = 4;
64constexpr size_t MAX_HTTP_BODY_BYTES = 1 * 1024 * 1024;
66
67// Keep-alive connection pool: clients are kept open across requests and reused
68// per origin, so only the first request to a host pays the TCP+TLS handshake.
69// Plugin host calls are serialised by the PluginManager call mutex, so the
70// `s_active_slot` redirect below is safe (no concurrent perform), but several
71// slots may be open at once (different origins), hence a pool rather than one
72// shared client.
73struct PoolConn {
74 EspHttpClient client;
75 std::string origin; // "scheme://host[:port]" this client is bound to
76 bool in_use = false; // currently checked out by an open request
77};
78
79constexpr size_t MAX_HTTP_CONNS = MAX_HTTP_SLOTS;
80PoolConn s_pool[MAX_HTTP_CONNS];
81HttpSlot* s_active_slot = nullptr; // slot the body event handler writes into
82
83// Origin key ("scheme://host[:port]") used to match a pooled connection to a URL.
84std::string originKey(const char* url)
85{
86 std::string u(url ? url : "");
87 size_t scheme = u.find("://");
88 if (scheme == std::string::npos) return u;
89 size_t path = u.find('/', scheme + 3);
90 return path == std::string::npos ? u : u.substr(0, path);
91}
92
93// Append response bytes to the slot buffer, growing PSRAM storage up to
94// MAX_HTTP_BODY_BYTES. Returns false if the cap is reached or realloc fails.
95bool appendBody(HttpSlot& slot, const char* data, size_t len)
96{
97 if (!data || len == 0) return true;
98 size_t needed = slot.body_len + len + 1;
99 if (needed > MAX_HTTP_BODY_BYTES) return false;
100 if (needed > slot.body_cap) {
101 size_t new_cap = slot.body_cap ? slot.body_cap : 4096;
102 while (new_cap < needed) new_cap *= 2;
103 if (new_cap > MAX_HTTP_BODY_BYTES) new_cap = MAX_HTTP_BODY_BYTES;
104 char* raw = slot.body.release();
105 char* grown = static_cast<char*>(std::realloc(raw, new_cap));
106 if (!grown) {
107 slot.body.reset(raw);
108 return false;
109 }
110 slot.body.reset(grown);
111 slot.body_cap = new_cap;
112 }
113 std::memcpy(slot.body.get() + slot.body_len, data, len);
114 slot.body_len += len;
115 slot.body.get()[slot.body_len] = '\0';
116 return true;
117}
118
119esp_err_t http_event_handler(esp_http_client_event_t* evt)
120{
121 // The client is reused across requests, so the body target is the slot
122 // currently performing, not a fixed init-time user_data pointer.
123 HttpSlot* slot = s_active_slot;
124 if (!slot) return ESP_OK;
125 if (evt->event_id == HTTP_EVENT_ON_DATA && evt->data && evt->data_len > 0) {
126 if (!appendBody(*slot, static_cast<const char*>(evt->data),
127 static_cast<size_t>(evt->data_len))) {
128 return ESP_FAIL;
129 }
130 }
131 return ESP_OK;
132}
133
134HttpSlot* slotFor(int handle) { return s_slots.lookup(handle); }
135
136// Release a slot: return a borrowed pool connection to the pool (a transient
137// client is freed by the slot reset) and clear the slot. Shared by
138// host_http_close and the plugin-unload sweep.
139void closeSlot(HttpSlot& slot)
140{
141 if (slot.pool_idx >= 0 && static_cast<size_t>(slot.pool_idx) < MAX_HTTP_CONNS) {
142 s_pool[slot.pool_idx].in_use = false;
143 }
144 slot = HttpSlot{};
145}
146
147esp_http_client_method_t mapMethod(uint8_t m) {
148 switch (m) {
149 case HTTP_GET: return HTTP_METHOD_GET;
150 case HTTP_POST: return HTTP_METHOD_POST;
151 case HTTP_PUT: return HTTP_METHOD_PUT;
152 case HTTP_DELETE: return HTTP_METHOD_DELETE;
153 default: return HTTP_METHOD_GET;
154 }
155}
156
157} // namespace
158
159extern "C" {
160
161esp_http_client_handle_t makeClient(uint8_t method, const char* url, uint32_t timeout_ms)
162{
163 esp_http_client_config_t cfg{};
164 cfg.url = url;
165 cfg.method = mapMethod(method);
166 cfg.timeout_ms = timeout_ms ? static_cast<int>(timeout_ms) : 5000;
167 cfg.event_handler = http_event_handler;
168 cfg.disable_auto_redirect = false;
169 cfg.crt_bundle_attach = esp_crt_bundle_attach;
170 cfg.keep_alive_enable = true;
171 cfg.buffer_size = 4096;
172 cfg.buffer_size_tx = 1024;
173 esp_http_client_handle_t h = esp_http_client_init(&cfg);
174 if (!h) LOG_E("HTTP", "esp_http_client_init failed for url='%s'", url);
175 return h;
176}
177
178int host_http_open(uint8_t method, const char* url, uint32_t timeout_ms)
179{
180 if (!url) return HOST_ERR_INVALID_ARG;
181 int slot_id = 0;
182 HttpSlot* slot = s_slots.allocate(slot_id);
183 if (!slot) return HOST_ERR_NO_MEMORY;
184
185 *slot = HttpSlot{};
186 slot->used = true;
187 slot->owner = plg_get_active_plugin();
188
189 std::string key = originKey(url);
190
191 // 1) Reuse an idle pooled connection for the same origin (keep-alive hit).
192 for (size_t i = 0; i < MAX_HTTP_CONNS; ++i) {
193 if (s_pool[i].client && !s_pool[i].in_use && s_pool[i].origin == key) {
194 esp_http_client_set_url(s_pool[i].client.get(), url);
195 esp_http_client_set_method(s_pool[i].client.get(), mapMethod(method));
196 s_pool[i].in_use = true;
197 slot->pool_idx = static_cast<int>(i);
198 slot->client = s_pool[i].client.get();
199 return slot_id;
200 }
201 }
202
203 // 2) Build a fresh keep-alive client for this origin.
204 esp_http_client_handle_t h = makeClient(method, url, timeout_ms);
205 if (!h) {
206 *slot = HttpSlot{};
207 return HOST_ERR_GENERIC;
208 }
209
210 // 3) Park it in the pool: prefer an empty entry, else evict an idle one.
211 int idx = -1;
212 for (size_t i = 0; i < MAX_HTTP_CONNS; ++i) {
213 if (!s_pool[i].client) { idx = static_cast<int>(i); break; }
214 }
215 if (idx < 0) {
216 for (size_t i = 0; i < MAX_HTTP_CONNS; ++i) {
217 if (!s_pool[i].in_use) { idx = static_cast<int>(i); break; }
218 }
219 }
220 if (idx >= 0) {
221 s_pool[idx].client.reset(h);
222 s_pool[idx].origin = key;
223 s_pool[idx].in_use = true;
224 slot->pool_idx = idx;
225 slot->client = h;
226 } else {
227 // All pooled connections are in flight: use a transient, slot-owned client.
228 slot->owned.reset(h);
229 slot->pool_idx = -1;
230 slot->client = h;
231 }
232 return slot_id;
233}
234
235int host_http_set_header(int handle, const char* key, const char* value)
236{
237 auto* slot = slotFor(handle);
238 if (!slot || !slot->client || !key || !value) return HOST_ERR_INVALID_ARG;
239 return esp_http_client_set_header(slot->client, key, value) == ESP_OK
241}
242
243int host_http_set_body(int handle, const uint8_t* body, size_t len)
244{
245 auto* slot = slotFor(handle);
246 if (!slot || !slot->client) return HOST_ERR_INVALID_ARG;
247 return esp_http_client_set_post_field(slot->client,
248 reinterpret_cast<const char*>(body),
249 static_cast<int>(len)) == ESP_OK
251}
252
253int host_http_perform(int handle)
254{
255 auto* slot = slotFor(handle);
256 if (!slot || !slot->client) return HOST_ERR_INVALID_ARG;
257 s_active_slot = slot;
258 esp_err_t err = esp_http_client_perform(slot->client);
259 s_active_slot = nullptr;
260 slot->status = esp_http_client_get_status_code(slot->client);
261 slot->content_length = static_cast<size_t>(
262 esp_http_client_get_content_length(slot->client));
263 // esp_http_client_perform reports ESP_ERR_NOT_SUPPORTED for a 401 whose
264 // WWW-Authenticate scheme it cannot satisfy (e.g. Bearer) and returns on an
265 // early path where client->response is never allocated. Calling
266 // esp_http_client_read there dereferences a NULL response and faults, so the
267 // body must NOT be drained on the error path. A zero status additionally
268 // means no HTTP exchange happened (connect/TLS failure). In every error case
269 // surface the captured status with an empty body and drop the keep-alive
270 // connection so the next request starts from a clean client.
271 if (err != ESP_OK) {
272 if (slot->status == 0) {
273 LOG_E("HTTP", "perform failed: %s (0x%x)", esp_err_to_name(err), err);
274 } else {
275 LOG_W("HTTP", "perform err %s with status=%d; body not drained",
276 esp_err_to_name(err), slot->status);
277 }
278 if (slot->pool_idx >= 0 && static_cast<size_t>(slot->pool_idx) < MAX_HTTP_CONNS) {
279 s_pool[slot->pool_idx] = PoolConn{};
280 } else {
281 slot->owned.reset();
282 }
283 slot->client = nullptr;
284 slot->pool_idx = -1;
285 if (slot->status == 0) {
286 return HOST_ERR_GENERIC;
287 }
288 }
289 LOG_I("HTTP", "status=%d content_length=%zu", slot->status, slot->content_length);
290 return HOST_OK;
291}
292
293int host_http_status(int handle)
294{
295 auto* slot = slotFor(handle);
296 return slot ? slot->status : HOST_ERR_INVALID_ARG;
297}
298
299int host_http_read_chunk(int handle, uint8_t* buf, size_t buf_size, size_t* out_len)
300{
301 auto* slot = slotFor(handle);
302 if (!slot || !buf || !out_len) return HOST_ERR_INVALID_ARG;
303 size_t remaining = slot->body_len - slot->body_cursor;
304 size_t n = remaining < buf_size ? remaining : buf_size;
305 if (n) std::memcpy(buf, slot->body.get() + slot->body_cursor, n);
306 slot->body_cursor += n;
307 *out_len = n;
308 return HOST_OK;
309}
310
311size_t host_http_content_length(int handle)
312{
313 auto* slot = slotFor(handle);
314 return slot ? slot->content_length : 0;
315}
316
317int host_http_close(int handle)
318{
319 auto* slot = slotFor(handle);
320 if (!slot) return HOST_ERR_INVALID_ARG;
321 closeSlot(*slot); // RAII frees the transient client (if any) + body buffer
322 return HOST_OK;
323}
324
325void plg_http_on_unload(void* plugin)
326{
327 if (!plugin) return;
328 for (size_t i = 0; i < MAX_HTTP_SLOTS; ++i) {
329 HttpSlot& slot = s_slots.slots[i];
330 if (!slot.used || slot.owner != plugin) continue;
331 LOG_W("HTTP", "force-closing leaked HTTP slot %u", static_cast<unsigned>(i + 1));
332 if (s_active_slot == &slot) s_active_slot = nullptr;
333 closeSlot(slot);
334 }
335 // Drop idle pooled keep-alive connections: one reused across a plugin
336 // exit/re-enter can hand back a stale or truncated body. Plugins are
337 // UI-exclusive, so any connection not in flight is safe to discard here; the
338 // next request rebuilds a fresh connection.
339 for (size_t i = 0; i < MAX_HTTP_CONNS; ++i) {
340 if (!s_pool[i].in_use) s_pool[i] = PoolConn{};
341 }
342}
343
344} // extern "C"
Fixed-capacity, 1-based slot table for host-API resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
int host_http_close(int handle)
Release a request handle.
int host_http_read_chunk(int handle, uint8_t *buf, size_t buf_size, size_t *out_len)
Stream one response chunk into buf.
int host_http_set_body(int handle, const uint8_t *body, size_t len)
Stage a request body before perform().
#define HTTP_DELETE
Definition host_api.h:329
int host_http_status(int handle)
HTTP response status code, or negative on error.
int host_http_perform(int handle)
Send the request and read response headers.
#define HTTP_GET
Definition host_api.h:326
size_t host_http_content_length(int handle)
Response Content-Length, or 0 when unknown / chunked.
int host_http_set_header(int handle, const char *key, const char *value)
Add a request header before perform().
#define HTTP_PUT
Definition host_api.h:328
#define HTTP_POST
Definition host_api.h:327
int host_http_open(uint8_t method, const char *url, uint32_t timeout_ms)
Open an HTTP request.
CDC Badge OS plugin host API - canonical C ABI contract.
#define HOST_OK
Definition host_api.h:37
#define HOST_ERR_INVALID_ARG
Definition host_api.h:39
#define HOST_ERR_NO_MEMORY
Definition host_api.h:43
#define HOST_ERR_GENERIC
Definition host_api.h:38
esp_http_client_handle_t makeClient(uint8_t method, const char *url, uint32_t timeout_ms)
void * plg_get_active_plugin(void)
void plg_http_on_unload(void *plugin)
::cdc::core::CStdUniquePtr< T > CStdUniquePtr
Definition Raii.h:24
Thin alias layer that re-exports cdc::core RAII wrappers in the cdc::plugin_manager namespace for sou...
T * lookup(int id)
1-based lookup. Returns nullptr if id is out of range or slot unused.
Definition SlotTable.h:26
std::array< T, N > slots
Definition SlotTable.h:21
T * allocate(int &out_id)
Definition SlotTable.h:37