Skip to content

Commit 08081ac

Browse files
committed
security and doc update
1 parent 40e22e3 commit 08081ac

File tree

4 files changed

+81
-10
lines changed

4 files changed

+81
-10
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ An asynchronous HTTP client library for ESP32 microcontrollers, built on top of
2323
-**Multiple simultaneous requests** - Handle multiple requests concurrently
2424
-**Chunked transfer decoding** - Validates framing and exposes parsed trailers
2525
-**Optional redirect following** - Follow 301/302/303 (converted to GET) and 307/308 (method preserved)
26-
-**Header & body guards** - Limit buffered response headers/body to avoid runaway responses
26+
-**Header & body guards** - Limits buffered headers (~2.8 KiB) and body (8 KiB) by default to avoid runaway responses
2727
-**Zero-copy streaming** - Combine `req->setNoStoreBody(true)` with `client.onBodyChunk(...)` to stream large payloads without heap spikes
2828

2929
> ⚠ Limitations: provide trust material for HTTPS (CA, fingerprint or insecure flag) and remember the full body is buffered in memory unless you opt into zero-copy streaming via `setNoStoreBody(true)`.
@@ -150,10 +150,10 @@ void setDefaultConnectTimeout(uint32_t ms);
150150
// Follow HTTP redirects (max hops clamps to >=1). Disabled by default.
151151
void setFollowRedirects(bool enable, uint8_t maxHops = 3);
152152
153-
// Abort if response headers exceed this many bytes (0 = unlimited)
153+
// Abort if response headers exceed this many bytes (default ~2.8 KiB, 0 = unlimited)
154154
void setMaxHeaderBytes(size_t maxBytes);
155155
156-
// Soft limit for buffered response bodies (bytes, 0 = unlimited)
156+
// Soft limit for buffered response bodies (default 8192 bytes, 0 = unlimited)
157157
void setMaxBodySize(size_t maxBytes);
158158
159159
// Limit simultaneous active requests (0 = unlimited, others queued)
@@ -162,12 +162,20 @@ void setMaxParallel(uint16_t maxParallel);
162162
// Set User-Agent string
163163
void setUserAgent(const char* userAgent);
164164
165+
// Keep-alive connection pooling (idle timeout in ms, clamped to >= 1000)
166+
void setKeepAlive(bool enable, uint16_t idleMs = 5000);
167+
165168
// Cookie jar helpers
166169
void clearCookies();
167170
void setCookie(const char* name, const char* value, const char* path = "/", const char* domain = nullptr,
168171
bool secure = false);
169172
```
170173

174+
Cookies are captured automatically from `Set-Cookie` responses and replayed on matching hosts/paths; call
175+
`clearCookies()` to wipe the jar or `setCookie()` to pre-seed entries manually. Keep-alive pooling is off by default;
176+
enable it with `setKeepAlive(true, idleMs)` to reuse TCP/TLS connections for the same host/port (respecting server
177+
`Connection: close` requests).
178+
171179
#### Callback Types
172180

173181
```cpp

src/AsyncHttpClient.cpp

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
static constexpr size_t kMaxChunkSizeLineLen = 64;
1212
static constexpr size_t kMaxChunkTrailerLineLen = 256;
1313
static constexpr size_t kMaxChunkTrailerLines = 32;
14+
static constexpr size_t kDefaultMaxHeaderBytes = 2800; // ~2.8 KiB
15+
static constexpr size_t kDefaultMaxBodyBytes = 8192; // 8 KiB
16+
static constexpr size_t kMaxCookieCount = 16;
17+
static constexpr size_t kMaxCookieBytes = 4096;
18+
static const char* kPublicSuffixes[] = {"com", "net", "org", "gov", "edu", "mil", "int",
19+
"co.uk", "ac.uk", "gov.uk", "uk", "io", "co"};
20+
1421
static bool equalsIgnoreCase(const String& a, const char* b) {
1522
size_t lenA = a.length();
1623
size_t lenB = strlen(b);
@@ -26,7 +33,8 @@ static bool equalsIgnoreCase(const String& a, const char* b) {
2633

2734
AsyncHttpClient::AsyncHttpClient()
2835
: _defaultTimeout(10000), _defaultUserAgent(String("ESPAsyncWebClient/") + ESP_ASYNC_WEB_CLIENT_VERSION),
29-
_bodyChunkCallback(nullptr), _followRedirects(false), _maxRedirectHops(3), _maxHeaderBytes(0) {
36+
_bodyChunkCallback(nullptr), _maxBodySize(kDefaultMaxBodyBytes), _followRedirects(false), _maxRedirectHops(3),
37+
_maxHeaderBytes(kDefaultMaxHeaderBytes) {
3038
#if defined(ARDUINO_ARCH_ESP32) && defined(ASYNC_HTTP_ENABLE_AUTOLOOP)
3139
// Create recursive mutex for shared containers when auto-loop may run in background
3240
_reqMutex = xSemaphoreCreateRecursiveMutex();
@@ -1292,6 +1300,18 @@ bool AsyncHttpClient::isIpLiteral(const String& host) const {
12921300
return hasColon || hasDot;
12931301
}
12941302

1303+
static bool isPublicSuffix(const String& domain) {
1304+
if (domain.length() == 0)
1305+
return false;
1306+
String lower = domain;
1307+
lower.toLowerCase();
1308+
for (auto suffix : kPublicSuffixes) {
1309+
if (lower.equals(suffix))
1310+
return true;
1311+
}
1312+
return false;
1313+
}
1314+
12951315
bool AsyncHttpClient::normalizeCookieDomain(String& domain, const String& host, bool domainAttributeProvided) const {
12961316
String cleaned = domain;
12971317
cleaned.trim();
@@ -1316,6 +1336,8 @@ bool AsyncHttpClient::normalizeCookieDomain(String& domain, const String& host,
13161336
return false;
13171337
if (cleaned.indexOf('.') == -1)
13181338
return false;
1339+
if (isPublicSuffix(cleaned))
1340+
return false;
13191341

13201342
domain = cleaned;
13211343
return true;
@@ -1409,6 +1431,8 @@ void AsyncHttpClient::storeResponseCookie(const AsyncHttpRequest* request, const
14091431
String raw = setCookieValue;
14101432
if (raw.length() == 0)
14111433
return;
1434+
if (raw.length() > kMaxCookieBytes)
1435+
return;
14121436
int semi = raw.indexOf(';');
14131437
String pair = semi == -1 ? raw : raw.substring(0, semi);
14141438
pair.trim();
@@ -1456,6 +1480,9 @@ void AsyncHttpClient::storeResponseCookie(const AsyncHttpRequest* request, const
14561480
return;
14571481
if (!cookie.path.startsWith("/"))
14581482
cookie.path = "/" + cookie.path;
1483+
size_t payloadSize = cookie.name.length() + cookie.value.length() + cookie.domain.length() + cookie.path.length();
1484+
if (payloadSize > kMaxCookieBytes)
1485+
return;
14591486

14601487
lock();
14611488
for (auto it = _cookies.begin(); it != _cookies.end();) {
@@ -1466,7 +1493,10 @@ void AsyncHttpClient::storeResponseCookie(const AsyncHttpRequest* request, const
14661493
++it;
14671494
}
14681495
}
1469-
if (!remove)
1496+
if (!remove) {
1497+
if (_cookies.size() >= kMaxCookieCount)
1498+
_cookies.erase(_cookies.begin());
14701499
_cookies.push_back(cookie);
1500+
}
14711501
unlock();
14721502
}

src/AsyncTransport.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ class AsyncTlsTransport : public AsyncTransport {
274274
std::vector<uint8_t> _encryptedBuffer;
275275
size_t _encryptedOffset = 0;
276276
std::vector<uint8_t> _fingerprintBytes;
277+
bool _fingerprintInvalid = false;
277278

278279
mbedtls_ssl_context _ssl;
279280
mbedtls_ssl_config _sslConfig;
@@ -295,7 +296,8 @@ static int hexValue(char c) {
295296
return -1;
296297
}
297298

298-
static std::vector<uint8_t> parseFingerprintString(const String& text) {
299+
static std::vector<uint8_t> parseFingerprintString(const String& text, bool* outValid) {
300+
bool valid = true;
299301
std::vector<uint8_t> bytes;
300302
int accum = -1;
301303
for (size_t i = 0; i < text.length(); ++i) {
@@ -306,6 +308,7 @@ static std::vector<uint8_t> parseFingerprintString(const String& text) {
306308
int v = hexValue(ch);
307309
if (v < 0) {
308310
bytes.clear();
311+
valid = false;
309312
break;
310313
}
311314
if (accum == -1) {
@@ -318,7 +321,10 @@ static std::vector<uint8_t> parseFingerprintString(const String& text) {
318321
if (accum != -1) {
319322
// Odd number of nibbles -> invalid
320323
bytes.clear();
324+
valid = false;
321325
}
326+
if (outValid)
327+
*outValid = valid || text.length() == 0;
322328
return bytes;
323329
}
324330

@@ -334,7 +340,9 @@ AsyncTlsTransport::AsyncTlsTransport(const AsyncHttpTLSConfig& config) : _client
334340
mbedtls_pk_init(&_clientKey);
335341
mbedtls_entropy_init(&_entropy);
336342
mbedtls_ctr_drbg_init(&_ctrDrbg);
337-
_fingerprintBytes = parseFingerprintString(_config.fingerprint);
343+
bool fpValid = true;
344+
_fingerprintBytes = parseFingerprintString(_config.fingerprint, &fpValid);
345+
_fingerprintInvalid = (_config.fingerprint.length() > 0 && !fpValid);
338346
}
339347

340348
AsyncTlsTransport::~AsyncTlsTransport() {
@@ -364,6 +372,10 @@ void AsyncTlsTransport::shutdownClient() {
364372
bool AsyncTlsTransport::connect(const char* host, uint16_t port) {
365373
if (!_client)
366374
return false;
375+
if (_fingerprintInvalid) {
376+
fail(TLS_FINGERPRINT_MISMATCH, "Invalid TLS fingerprint format");
377+
return false;
378+
}
367379
_host = host;
368380
_port = port;
369381
_handshakeStartMs = millis();

src/UrlParser.cpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#include "UrlParser.h"
2+
#include <cerrno>
3+
#include <cstdlib>
24

35
namespace UrlParser {
46

@@ -9,6 +11,24 @@ static bool startsWith(const std::string& s, const char* prefix) {
911
return s.size() >= n && s.compare(0, n, prefix) == 0;
1012
}
1113

14+
static bool parsePort(const std::string& portStr, uint16_t* out) {
15+
if (!out || portStr.empty())
16+
return false;
17+
for (char c : portStr) {
18+
if (c < '0' || c > '9')
19+
return false;
20+
}
21+
errno = 0;
22+
char* end = nullptr;
23+
unsigned long val = std::strtoul(portStr.c_str(), &end, 10);
24+
if (end == portStr.c_str() || *end != '\0')
25+
return false;
26+
if (errno == ERANGE || val > 65535)
27+
return false;
28+
*out = static_cast<uint16_t>(val);
29+
return true;
30+
}
31+
1232
bool parse(const std::string& originalUrl, ParsedUrl& out) {
1333
std::string url = originalUrl; // working copy
1434
out.secure = false;
@@ -54,9 +74,10 @@ bool parse(const std::string& originalUrl, ParsedUrl& out) {
5474
if (colon != std::string::npos) {
5575
std::string portStr = out.host.substr(colon + 1);
5676
out.host = out.host.substr(0, colon);
57-
if (!portStr.empty()) {
58-
out.port = static_cast<uint16_t>(std::stoi(portStr));
59-
}
77+
uint16_t parsedPort = 0;
78+
if (!parsePort(portStr, &parsedPort))
79+
return false;
80+
out.port = parsedPort;
6081
}
6182

6283
return !out.host.empty();

0 commit comments

Comments
 (0)