DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
http_client.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file http_client.cpp
6 * @brief Outbound HTTP(S) client: URL parse + request build + response parse,
7 * and the raw-lwIP / mbedTLS transport (ESP32 only).
8 */
9
11
12#if DETWS_ENABLE_HTTP_CLIENT
13
16#include <stdio.h>
17#include <string.h>
18
19// ---------------------------------------------------------------------------
20// Pure helpers (host-testable)
21// ---------------------------------------------------------------------------
22
23bool http_client_parse_url(const char *url, bool *is_https, char *host, size_t host_cap, uint16_t *port, char *path,
24 size_t path_cap)
25{
26 if (!url || !is_https || !host || !port || !path)
27 return false;
28
29 const char *p = url;
30 if (strncmp(p, "https://", 8) == 0)
31 {
32 *is_https = true;
33 *port = 443;
34 p += 8;
35 }
36 else if (strncmp(p, "http://", 7) == 0) // NOSONAR: HTTP client must accept http:// URLs
37 {
38 *is_https = false;
39 *port = 80;
40 p += 7;
41 }
42 else
43 return false;
44
45 // host (up to ':' or '/')
46 const char *h = p;
47 while (*p && *p != ':' && *p != '/')
48 p++;
49 size_t hlen = (size_t)(p - h);
50 if (hlen == 0 || hlen >= host_cap)
51 return false;
52 memcpy(host, h, hlen);
53 host[hlen] = '\0';
54
55 // optional :port
56 if (*p == ':')
57 {
58 p++;
59 if (*p < '0' || *p > '9')
60 return false;
61 uint32_t pn = 0;
62 while (*p >= '0' && *p <= '9')
63 {
64 pn = pn * 10 + (uint32_t)(*p++ - '0');
65 if (pn > 65535)
66 return false;
67 }
68 *port = (uint16_t)pn;
69 }
70
71 // path ("/" if absent)
72 if (*p == '\0')
73 {
74 if (path_cap < 2)
75 return false;
76 path[0] = '/';
77 path[1] = '\0';
78 }
79 else
80 {
81 size_t plen = strnlen(p, path_cap + 1);
82 if (plen >= path_cap)
83 return false;
84 memcpy(path, p, plen + 1);
85 }
86 return true;
87}
88
89size_t http_client_build_request(const char *method, const char *host, uint16_t port, const char *path,
90 const char *content_type, const uint8_t *body, size_t body_len, char *out, size_t cap)
91{
92 if (!method || !host || !path || !out || cap == 0)
93 return 0;
94
95 // Host header carries the port only when it is non-default.
96 char hosthdr[80];
97 if (port == 80 || port == 443)
98 snprintf(hosthdr, sizeof(hosthdr), "%s", host);
99 else
100 snprintf(hosthdr, sizeof(hosthdr), "%s:%u", host, (unsigned)port);
101
102 int n;
103 if (body && body_len)
104 {
105 n = snprintf(out, cap,
106 "%s %s HTTP/1.1\r\n"
107 "Host: %s\r\n"
108 "User-Agent: DetWS\r\n"
109 "Content-Type: %s\r\n"
110 "Content-Length: %u\r\n"
111 "Connection: close\r\n\r\n",
112 method, path, hosthdr, content_type ? content_type : DET_MIME_OCTET_STREAM, (unsigned)body_len);
113 if (n < 0 || (size_t)n + body_len > cap)
114 return 0;
115 memcpy(out + n, body, body_len);
116 return (size_t)n + body_len;
117 }
118
119 n = snprintf(out, cap,
120 "%s %s HTTP/1.1\r\n"
121 "Host: %s\r\n"
122 "User-Agent: DetWS\r\n"
123 "Connection: close\r\n\r\n",
124 method, path, hosthdr);
125 if (n < 0 || (size_t)n >= cap)
126 return 0;
127 return (size_t)n;
128}
129
130// Case-insensitive search for header @p name in the header block [buf, end);
131// returns a pointer to the value (past "name:" and spaces) or nullptr.
132static const char *find_header(const uint8_t *buf, const uint8_t *end, const char *name)
133{
134 size_t nlen = strnlen(name, (size_t)(end - buf) + 1);
135 const uint8_t *p = buf;
136 while (p + nlen + 1 < end)
137 {
138 // at the start of a header line?
139 if (strncasecmp((const char *)p, name, nlen) == 0 && p[nlen] == ':')
140 {
141 const uint8_t *v = p + nlen + 1;
142 while (v < end && (*v == ' ' || *v == '\t'))
143 v++;
144 return (const char *)v;
145 }
146 // advance to the next line
147 while (p < end && *p != '\n')
148 p++;
149 if (p < end)
150 p++;
151 }
152 return nullptr;
153}
154
155int http_client_parse_response(uint8_t *buf, size_t len, size_t *body_off, size_t *body_len)
156{
157 if (!buf || len < 12 || memcmp(buf, "HTTP/1.", 7) != 0)
158 return (int)HttpClientError::HTTP_CLIENT_ERR_RESPONSE;
159
160 // Status code: first space, then 3 digits.
161 const uint8_t *sp = (const uint8_t *)memchr(buf, ' ', len);
162 if (!sp || sp + 4 > buf + len)
163 return (int)HttpClientError::HTTP_CLIENT_ERR_RESPONSE;
164 int status = (sp[1] - '0') * 100 + (sp[2] - '0') * 10 + (sp[3] - '0');
165 if (status < 100 || status > 599)
166 return (int)HttpClientError::HTTP_CLIENT_ERR_RESPONSE;
167
168 // Header terminator "\r\n\r\n".
169 uint8_t *hdr_end = nullptr;
170 for (size_t i = 0; i + 3 < len; i++)
171 if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n')
172 {
173 hdr_end = buf + i;
174 break;
175 }
176 if (!hdr_end)
177 return (int)HttpClientError::HTTP_CLIENT_ERR_RESPONSE;
178
179 size_t off = (size_t)(hdr_end + 4 - buf);
180 size_t avail = len - off;
181
182 const char *te = find_header(buf, hdr_end, "Transfer-Encoding");
183 if (te && strncasecmp(te, "chunked", 7) == 0)
184 {
185 // Decode chunked transfer-encoding in place into the body region.
186 size_t in = off, out = off;
187 while (in < len)
188 {
189 // chunk size line (hex) up to CRLF
190 size_t csz = 0;
191 bool any = false;
192 while (in < len && buf[in] != '\r')
193 {
194 uint8_t c = buf[in++];
195 int d;
196 if (c >= '0' && c <= '9')
197 d = c - '0';
198 else if (c >= 'a' && c <= 'f')
199 d = c - 'a' + 10;
200 else if (c >= 'A' && c <= 'F')
201 d = c - 'A' + 10;
202 else
203 break; // stop at ';' (chunk ext) or junk
204 csz = csz * 16 + (size_t)d;
205 any = true;
206 }
207 // skip to end of the size line
208 while (in < len && buf[in] != '\n')
209 in++;
210 if (in < len)
211 in++; // past '\n'
212 if (!any)
213 break;
214 if (csz == 0)
215 break; // last chunk
216 // Wrap-safe: csz is an unbounded attacker-controlled hex size; on a 32-bit target
217 // in + csz could overflow below len and skip this clamp, leaving a huge memmove.
218 // Compare against the bytes remaining instead (in <= len here).
219 if (csz > len - in)
220 csz = len - in; // truncated; take what we have
221 memmove(buf + out, buf + in, csz);
222 out += csz;
223 in += csz;
224 // skip trailing CRLF after chunk data
225 while (in < len && (buf[in] == '\r' || buf[in] == '\n'))
226 in++;
227 }
228 *body_off = off;
229 *body_len = out - off;
230 return status;
231 }
232
233 const char *cl = find_header(buf, hdr_end, "Content-Length");
234 if (cl)
235 {
236 long n = det_strtol(cl, nullptr);
237 if (n < 0)
238 n = 0;
239 size_t want = (size_t)n;
240 *body_off = off;
241 *body_len = (want < avail) ? want : avail; // bounded by what we received
242 return status;
243 }
244
245 // No framing header: connection-close delimited (rest of the buffer).
246 *body_off = off;
247 *body_len = avail;
248 return status;
249}
250
251// ---------------------------------------------------------------------------
252// Transport (ESP32 only): raw-lwIP TCP client + DNS, optional client mbedTLS.
253// ---------------------------------------------------------------------------
254#if defined(ARDUINO)
255
256#include "network_drivers/transport/client.h" // shared outbound TCP client (L4)
257#include <Arduino.h> // delay() / millis()
258
259#if DETWS_ENABLE_HTTP_CLIENT_TLS
261#include <mbedtls/ssl.h> // MBEDTLS_ERR_SSL_WANT_READ for the BIO recv callback
262#endif
263
264// Optional stage tracing: build with -DDETWS_HTTP_CLIENT_DEBUG to print where a
265// request stalls (DNS / connect / send / receive). Goes to the console UART.
266#ifdef DETWS_HTTP_CLIENT_DEBUG
267#define CL_DBG(...) printf(__VA_ARGS__)
268#else
269#define CL_DBG(...) ((void)0)
270#endif
271
272// All HTTP-client state, owned by one instance (internal linkage): a single in-flight request
273// (one loop task). rx holds the *response to parse*: the raw wire bytes for http, or (for
274// https) the plaintext decrypted by the TLS engine; the TCP connection lives in the shared
275// client pool (det_client). One named owner, unreachable from any other translation unit.
276struct HttpClientCtx
277{
278 uint8_t rx[DETWS_HTTP_CLIENT_BUF_SIZE];
279 int cid = -1; // active outbound connection id (det_client pool)
280};
281static HttpClientCtx s_http;
282
283#if DETWS_ENABLE_HTTP_CLIENT_TLS
284// mbedTLS BIO over the shared client transport: send wire bytes through the pool,
285// recv by draining its wire ring (which carries ciphertext for https).
286static int cl_tls_send(void *ctx, const unsigned char *buf, size_t len)
287{
288 (void)ctx;
289 size_t cap = len > 0xFFFF ? 0xFFFF : len;
290 return det_client_send(s_http.cid, buf, cap) ? (int)cap : -1;
291}
292static int cl_tls_recv(void *ctx, unsigned char *buf, size_t len)
293{
294 (void)ctx;
295 size_t n = det_client_read(s_http.cid, buf, len);
296 if (n == 0)
297 return det_client_is_closed(s_http.cid) ? 0 : MBEDTLS_ERR_SSL_WANT_READ;
298 return (int)n;
299}
300#endif // DETWS_ENABLE_HTTP_CLIENT_TLS
301
302// Core request: build, connect, send, receive, parse.
303static int http_request(const char *method, const char *url, const char *content_type, const uint8_t *body,
304 size_t body_len, HttpClientResult *out)
305{
306 if (out)
307 {
308 out->status = 0;
309 out->body = nullptr;
310 out->body_len = 0;
311 }
312
313 bool is_https;
314 char host[80], path[160];
315 uint16_t port;
316 if (!http_client_parse_url(url, &is_https, host, sizeof(host), &port, path, sizeof(path)))
317 return (int)HttpClientError::HTTP_CLIENT_ERR_URL;
318 CL_DBG("[hc] url host=%s port=%u https=%d path=%s\n", host, (unsigned)port, (int)is_https, path);
319#if !DETWS_ENABLE_HTTP_CLIENT_TLS
320 if (is_https)
321 return (int)HttpClientError::HTTP_CLIENT_ERR_TLS;
322#endif
323
324 // Build the request line + headers (+ optional body).
325 char req[768];
326 size_t reqlen = http_client_build_request(method, host, port, path, content_type, body, body_len, req, sizeof(req));
327 if (reqlen == 0)
328 return (int)HttpClientError::HTTP_CLIENT_ERR_URL;
329
330 uint32_t deadline = millis() + DETWS_HTTP_CLIENT_TIMEOUT_MS;
331
332 // Open the connection (DNS + connect) via the shared client transport.
333 s_http.cid = det_client_open(host, port, DETWS_HTTP_CLIENT_TIMEOUT_MS);
334 CL_DBG("[hc] det_client_open cid=%d\n", s_http.cid);
335 if (s_http.cid < 0)
336 return (s_http.cid == -2) ? (int)HttpClientError::HTTP_CLIENT_ERR_DNS
337 : (int)HttpClientError::HTTP_CLIENT_ERR_CONNECT;
338
339 // The response to parse: raw wire bytes (http) or decrypted plaintext (https),
340 // both land in s_http.rx.
341 size_t resp_len = 0;
342
343 if (is_https)
344 {
345#if DETWS_ENABLE_HTTP_CLIENT_TLS
346 int rc = det_tls_client_run(host, (const uint8_t *)req, reqlen, s_http.rx, sizeof(s_http.rx), &resp_len,
347 cl_tls_send, cl_tls_recv, deadline);
348 CL_DBG("[hc] tls rc=%d pt_len=%u\n", rc, (unsigned)resp_len);
349 if (rc < 0)
350 {
351 det_client_close(s_http.cid);
352 s_http.cid = -1;
353 return (int)HttpClientError::HTTP_CLIENT_ERR_TLS;
354 }
355#else
356 det_client_close(s_http.cid);
357 s_http.cid = -1;
358 return (int)HttpClientError::HTTP_CLIENT_ERR_TLS;
359#endif
360 }
361 else
362 {
363 // Plaintext: send the request, then drain wire bytes into s_http.rx until the
364 // peer closes (and the ring is empty), the buffer fills, or we time out.
365 if (!det_client_send(s_http.cid, req, reqlen))
366 {
367 det_client_close(s_http.cid);
368 s_http.cid = -1;
369 return (int)HttpClientError::HTTP_CLIENT_ERR_SEND;
370 }
371 while ((int32_t)(deadline - millis()) > 0)
372 {
373 size_t n = det_client_read(s_http.cid, s_http.rx + resp_len, sizeof(s_http.rx) - resp_len);
374 resp_len += n;
375 if (resp_len >= sizeof(s_http.rx))
376 break;
377 if (det_client_is_closed(s_http.cid) && det_client_available(s_http.cid) == 0)
378 break;
379 if (n == 0)
380 delay(5);
381 }
382 }
383
384 CL_DBG("[hc] done resp_len=%u\n", (unsigned)resp_len);
385 det_client_close(s_http.cid);
386 s_http.cid = -1;
387
388 if (resp_len == 0)
389 return (int)HttpClientError::HTTP_CLIENT_ERR_TIMEOUT;
390
391 size_t body_off = 0, blen = 0;
392 int status = http_client_parse_response(s_http.rx, resp_len, &body_off, &blen);
393 if (status < 0)
394 return (int)HttpClientError::HTTP_CLIENT_ERR_RESPONSE;
395 if (out)
396 {
397 out->status = status;
398 out->body = s_http.rx + body_off;
399 out->body_len = blen;
400 }
401 return status;
402}
403
404int http_get(const char *url, HttpClientResult *out)
405{
406 return http_request("GET", url, nullptr, nullptr, 0, out);
407}
408
409int http_post(const char *url, const char *content_type, const uint8_t *body, size_t body_len, HttpClientResult *out)
410{
411 return http_request("POST", url, content_type, body, body_len, out);
412}
413
414void http_client_set_ca(const uint8_t *ca, size_t ca_len)
415{
416#if DETWS_ENABLE_HTTP_CLIENT_TLS
417 det_tls_client_set_ca(ca, ca_len);
418#else
419 (void)ca;
420 (void)ca_len;
421#endif
422}
423void http_client_set_pin(const uint8_t sha256[32])
424{
425#if DETWS_ENABLE_HTTP_CLIENT_TLS
426 det_tls_client_set_pin(sha256);
427#else
428 (void)sha256;
429#endif
430}
431void http_client_clear_verify()
432{
433#if DETWS_ENABLE_HTTP_CLIENT_TLS
434 det_tls_client_clear_verify();
435#endif
436}
437
438#else // host build: transport is a stub
439
440int http_get(const char *, HttpClientResult *)
441{
442 return (int)HttpClientError::HTTP_CLIENT_ERR_CONNECT;
443}
444int http_post(const char *, const char *, const uint8_t *, size_t, HttpClientResult *)
445{
446 return (int)HttpClientError::HTTP_CLIENT_ERR_CONNECT;
447}
448void http_client_set_ca(const uint8_t *, size_t)
449{
450}
451void http_client_set_pin(const uint8_t *)
452{
453}
454void http_client_clear_verify()
455{
456}
457
458#endif // ARDUINO
459
460#endif // DETWS_ENABLE_HTTP_CLIENT
#define DETWS_HTTP_CLIENT_TIMEOUT_MS
Outbound HTTP client connect/response timeout in milliseconds.
#define DETWS_HTTP_CLIENT_BUF_SIZE
Receive buffer (and max response size) for the outbound HTTP client, bytes.
size_t det_client_available(int)
Wire bytes currently buffered and ready to read.
Definition client.cpp:326
bool det_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:318
int det_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:310
size_t det_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:330
void det_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:334
bool det_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:322
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Zero-heap outbound HTTP(S) client over raw lwIP (DETWS_ENABLE_HTTP_CLIENT).
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
long det_strtol(const char *s, const char **end)
Parse a base-10 long; sets end past the digits (or to s if none).
Definition numparse.h:32
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).