Update ESPAsyncWebServer to v3.7.10

This commit is contained in:
Daniel Öster 2025-07-28 09:23:49 +03:00
parent 302282abeb
commit 537c01a8b7
21 changed files with 589 additions and 177 deletions

View file

@ -38,7 +38,7 @@ It is also deployed in these registries:
- Arduino Library Registry: [https://github.com/arduino/library-registry](https://github.com/arduino/library-registry) - Arduino Library Registry: [https://github.com/arduino/library-registry](https://github.com/arduino/library-registry)
- ESP Component Registry [https://components.espressif.com/components/esp32async/espasyncbebserver/](https://components.espressif.com/components/esp32async/espasyncbebserver/) - ESP Component Registry [https://components.espressif.com/components/esp32async/espasyncwebserver](https://components.espressif.com/components/esp32async/espasyncwebserver)
- PlatformIO Registry: [https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer](https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer) - PlatformIO Registry: [https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer](https://registry.platformio.org/libraries/esp32async/ESPAsyncWebServer)
@ -72,6 +72,19 @@ lib_deps =
ESP32Async/ESPAsyncWebServer ESP32Async/ESPAsyncWebServer
``` ```
### LibreTiny (BK7231N/T, RTL8710B, etc.)
Version 1.9.1 or newer is required.
```ini
[env:stable]
platform = libretiny @ ^1.9.1
lib_ldf_mode = chain
lib_deps =
ESP32Async/AsyncTCP
ESP32Async/ESPAsyncWebServer
```
### Unofficial dependencies ### Unofficial dependencies
**AsyncTCPSock** **AsyncTCPSock**
@ -100,7 +113,7 @@ platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = rpipicow board = rpipicow
board_build.core = earlephilhower board_build.core = earlephilhower
lib_deps = lib_deps =
ayushsharma82/RPAsyncTCP@^1.3.1 ayushsharma82/RPAsyncTCP@^1.3.2
ESP32Async/ESPAsyncWebServer ESP32Async/ESPAsyncWebServer
lib_ignore = lib_ignore =
lwIP_ESPHost lwIP_ESPHost

View file

@ -1,6 +1,6 @@
{ {
"name": "ESPAsyncWebServer", "name": "ESPAsyncWebServer",
"version": "3.7.2", "version": "3.7.10",
"description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.", "description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.",
"keywords": "http,async,websocket,webserver", "keywords": "http,async,websocket,webserver",
"homepage": "https://github.com/ESP32Async/ESPAsyncWebServer", "homepage": "https://github.com/ESP32Async/ESPAsyncWebServer",
@ -18,14 +18,18 @@
"platforms": [ "platforms": [
"espressif32", "espressif32",
"espressif8266", "espressif8266",
"raspberrypi" "raspberrypi",
"libretiny"
], ],
"dependencies": [ "dependencies": [
{ {
"owner": "ESP32Async", "owner": "ESP32Async",
"name": "AsyncTCP", "name": "AsyncTCP",
"version": "^3.3.6", "version": "^3.4.5",
"platforms": "espressif32" "platforms": [
"espressif32",
"libretiny"
]
}, },
{ {
"owner": "ESP32Async", "owner": "ESP32Async",
@ -40,7 +44,7 @@
{ {
"owner": "ayushsharma82", "owner": "ayushsharma82",
"name": "RPAsyncTCP", "name": "RPAsyncTCP",
"version": "^1.3.1", "version": "^1.3.2",
"platforms": "raspberrypi" "platforms": "raspberrypi"
} }
], ],

View file

@ -1,6 +1,6 @@
name=ESP Async WebServer name=ESP Async WebServer
includes=ESPAsyncWebServer.h includes=ESPAsyncWebServer.h
version=3.7.2 version=3.7.10
author=ESP32Async author=ESP32Async
maintainer=ESP32Async maintainer=ESP32Async
sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040

View file

@ -193,7 +193,7 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A
AsyncEventSourceClient::~AsyncEventSourceClient() { AsyncEventSourceClient::~AsyncEventSourceClient() {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_messageQueue.clear(); _messageQueue.clear();
close(); close();
@ -211,7 +211,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
#ifdef ESP32 #ifdef ESP32
// length() is not thread-safe, thus acquiring the lock before this call.. // length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_messageQueue.emplace_back(message, len); _messageQueue.emplace_back(message, len);
@ -241,7 +241,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
#ifdef ESP32 #ifdef ESP32
// length() is not thread-safe, thus acquiring the lock before this call.. // length() is not thread-safe, thus acquiring the lock before this call..
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_messageQueue.emplace_back(std::move(msg)); _messageQueue.emplace_back(std::move(msg));
@ -261,7 +261,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) { void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) {
#ifdef ESP32 #ifdef ESP32
// Same here, acquiring the lock early // Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
// adjust in-flight len // adjust in-flight len
@ -290,7 +290,7 @@ void AsyncEventSourceClient::_onPoll() {
if (_messageQueue.size()) { if (_messageQueue.size()) {
#ifdef ESP32 #ifdef ESP32
// Same here, acquiring the lock early // Same here, acquiring the lock early
std::lock_guard<std::mutex> lock(_lockmq); std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif #endif
_runQueue(); _runQueue();
} }
@ -367,7 +367,7 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient *client) {
return; return;
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
_clients.emplace_back(client); _clients.emplace_back(client);
if (_connectcb) { if (_connectcb) {
@ -382,7 +382,7 @@ void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient *client) {
_disconnectcb(client); _disconnectcb(client);
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
for (auto i = _clients.begin(); i != _clients.end(); ++i) { for (auto i = _clients.begin(); i != _clients.end(); ++i) {
if (i->get() == client) { if (i->get() == client) {
@ -398,10 +398,15 @@ void AsyncEventSource::close() {
// iterator should remain valid even when AsyncEventSource::_handleDisconnect() // iterator should remain valid even when AsyncEventSource::_handleDisconnect()
// is called very early // is called very early
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
for (const auto &c : _clients) { for (const auto &c : _clients) {
if (c->connected()) { if (c->connected()) {
/**
* @brief: Fix self-deadlock by using recursive_mutex instead.
* Due to c->close() shall call the callback function _onDisconnect()
* The calling flow _onDisconnect() --> _handleDisconnect() --> deadlock
*/
c->close(); c->close();
} }
} }
@ -412,7 +417,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
size_t aql = 0; size_t aql = 0;
uint32_t nConnectedClients = 0; uint32_t nConnectedClients = 0;
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
if (!_clients.size()) { if (!_clients.size()) {
return 0; return 0;
@ -430,7 +435,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
AsyncEvent_SharedData_t shared_msg = std::make_shared<String>(generateEventMessage(message, event, id, reconnect)); AsyncEvent_SharedData_t shared_msg = std::make_shared<String>(generateEventMessage(message, event, id, reconnect));
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
size_t hits = 0; size_t hits = 0;
size_t miss = 0; size_t miss = 0;
@ -446,7 +451,7 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c
size_t AsyncEventSource::count() const { size_t AsyncEventSource::count() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_client_queue_lock); std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif #endif
size_t n_clients{0}; size_t n_clients{0};
for (const auto &i : _clients) { for (const auto &i : _clients) {

View file

@ -6,8 +6,13 @@
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32 #if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#ifdef LIBRETINY
#ifdef round
#undef round
#endif
#endif
#include <mutex> #include <mutex>
#ifndef SSE_MAX_QUEUED_MESSAGES #ifndef SSE_MAX_QUEUED_MESSAGES
#define SSE_MAX_QUEUED_MESSAGES 32 #define SSE_MAX_QUEUED_MESSAGES 32
@ -129,7 +134,7 @@ private:
size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer
std::list<AsyncEventSourceMessage> _messageQueue; std::list<AsyncEventSourceMessage> _messageQueue;
#ifdef ESP32 #ifdef ESP32
mutable std::mutex _lockmq; mutable std::recursive_mutex _lockmq;
#endif #endif
bool _queueMessage(const char *message, size_t len); bool _queueMessage(const char *message, size_t len);
bool _queueMessage(AsyncEvent_SharedData_t &&msg); bool _queueMessage(AsyncEvent_SharedData_t &&msg);
@ -230,7 +235,7 @@ private:
#ifdef ESP32 #ifdef ESP32
// Same as for individual messages, protect mutations of _clients list // Same as for individual messages, protect mutations of _clients list
// since simultaneous access from different tasks is possible // since simultaneous access from different tasks is possible
mutable std::mutex _client_queue_lock; mutable std::recursive_mutex _client_queue_lock;
#endif #endif
ArEventHandlerFunction _connectcb = nullptr; ArEventHandlerFunction _connectcb = nullptr;
ArEventHandlerFunction _disconnectcb = nullptr; ArEventHandlerFunction _disconnectcb = nullptr;

View file

@ -113,53 +113,77 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons
void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) { void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) {
if (_onRequest) { if (_onRequest) {
// GET request:
if (request->method() == HTTP_GET) { if (request->method() == HTTP_GET) {
JsonVariant json; JsonVariant json;
_onRequest(request, json); _onRequest(request, json);
return; return;
} else if (request->_tempObject != NULL) { }
// POST / PUT / ... requests:
// check if JSON body is too large, if it is, don't deserialize
if (request->contentLength() > _maxContentLength) {
#ifdef ESP32
log_e("Content length exceeds maximum allowed");
#endif
request->send(413);
return;
}
if (request->_tempObject == NULL) {
// there is no body
request->send(400);
return;
}
#if ARDUINOJSON_VERSION_MAJOR == 5 #if ARDUINOJSON_VERSION_MAJOR == 5
DynamicJsonBuffer jsonBuffer; DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((uint8_t *)(request->_tempObject)); JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject);
if (json.success()) { if (json.success()) {
#elif ARDUINOJSON_VERSION_MAJOR == 6 #elif ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) { if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>(); JsonVariant json = jsonBuffer.as<JsonVariant>();
#else #else
JsonDocument jsonBuffer; JsonDocument jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) { if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>(); JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif #endif
_onRequest(request, json); _onRequest(request, json);
return; } else {
} // error parsing the body
request->send(400);
} }
request->send(_contentLength > _maxContentLength ? 413 : 400);
} else {
request->send(500);
} }
} }
void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (_onRequest) { if (_onRequest) {
_contentLength = total; // ignore callback if size is larger than maxContentLength
if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { if (total > _maxContentLength) {
request->_tempObject = malloc(total); return;
}
if (index == 0) {
// this check allows request->_tempObject to be initialized from a middleware
if (request->_tempObject == NULL) { if (request->_tempObject == NULL) {
request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string
if (request->_tempObject == NULL) {
#ifdef ESP32 #ifdef ESP32
log_e("Failed to allocate"); log_e("Failed to allocate");
#endif #endif
request->abort(); request->abort();
return; return;
}
} }
} }
if (request->_tempObject != NULL) { if (request->_tempObject != NULL) {
memcpy((uint8_t *)(request->_tempObject) + index, data, len); uint8_t *buffer = (uint8_t *)request->_tempObject;
memcpy(buffer + index, data, len);
} }
} }
} }

View file

@ -79,7 +79,6 @@ protected:
String _uri; String _uri;
WebRequestMethodComposite _method; WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest; ArJsonRequestHandlerFunction _onRequest;
size_t _contentLength;
#if ARDUINOJSON_VERSION_MAJOR == 6 #if ARDUINOJSON_VERSION_MAJOR == 6
size_t maxJsonBufferSize; size_t maxJsonBufferSize;
#endif #endif

View file

@ -3,30 +3,32 @@
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
AsyncWebHeader::AsyncWebHeader(const String &data) { const AsyncWebHeader AsyncWebHeader::parse(const char *data) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers
// In HTTP/1.X, a header is a case-insensitive name followed by a colon, then optional whitespace which will be ignored, and finally by its value
if (!data) { if (!data) {
return; return AsyncWebHeader(); // nullptr
} }
int index = data.indexOf(':'); if (data[0] == '\0') {
if (index < 0) { return AsyncWebHeader(); // empty string
return;
} }
_name = data.substring(0, index); if (strchr(data, '\n') || strchr(data, '\r')) {
_value = data.substring(index + 2); return AsyncWebHeader(); // Invalid header format
} }
char *colon = strchr(data, ':');
String AsyncWebHeader::toString() const { if (!colon) {
String str; return AsyncWebHeader(); // separator not found
if (str.reserve(_name.length() + _value.length() + 2)) { }
str.concat(_name); if (colon == data) {
str.concat((char)0x3a); return AsyncWebHeader(); // Header name cannot be empty
str.concat((char)0x20); }
str.concat(_value); char *startOfValue = colon + 1; // Skip the colon
str.concat(asyncsrv::T_rn); // skip one optional whitespace after the colon
} else { if (*startOfValue == ' ') {
#ifdef ESP32 startOfValue++;
log_e("Failed to allocate"); }
#endif String name;
} name.reserve(colon - data);
return str; name.concat(data, colon - data);
return AsyncWebHeader(name, String(startOfValue));
} }

View file

@ -0,0 +1,85 @@
#include "ESPAsyncWebServer.h"
/**
* @brief Sends a file from the filesystem to the client, with optional gzip compression and ETag-based caching.
*
* This method serves files over HTTP from the provided filesystem. If a compressed version of the file
* (with a `.gz` extension) exists and uncompressed version does not exist, it serves the compressed file.
* It also handles ETag caching using the CRC32 value from the gzip trailer, responding with `304 Not Modified`
* if the client's `If-None-Match` header matches the generated ETag.
*
* @param fs Reference to the filesystem (SPIFFS, LittleFS, etc.).
* @param path Path to the file to be served.
* @param contentType Optional MIME type of the file to be sent.
* If contentType is "" it will be obtained from the file extension
* @param download If true, forces the file to be sent as a download.
* @param callback Optional template processor for dynamic content generation.
* Templates will not be processed in compressed files.
*
* @note If neither the file nor its compressed version exists, responds with `404 Not Found`.
*/
void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) {
// Check uncompressed file first
if (fs.exists(path)) {
send(beginResponse(fs, path, contentType, download, callback));
return;
}
// Handle compressed version
const String gzPath = path + asyncsrv::T__gz;
File gzFile = fs.open(gzPath, "r");
// Compressed file not found or invalid
if (!gzFile.seek(gzFile.size() - 8)) {
send(404);
gzFile.close();
return;
}
// ETag validation
if (this->hasHeader(asyncsrv::T_INM)) {
// Generate server ETag from CRC in gzip trailer
uint8_t crcInTrailer[4];
gzFile.read(crcInTrailer, 4);
char serverETag[9];
_getEtag(crcInTrailer, serverETag);
// Compare with client's ETag
const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM);
if (inmHeader && inmHeader->value() == serverETag) {
gzFile.close();
this->send(304); // Not Modified
return;
}
}
// Send compressed file response
gzFile.close();
send(beginResponse(fs, path, contentType, download, callback));
}
/**
* @brief Generates an ETag string from a 4-byte trailer
*
* This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes.
*
* @param trailer[4] Input array of 4 bytes to convert to hexadecimal
* @param serverETag Output buffer to store the ETag
* Must be pre-allocated with minimum 9 bytes (8 hex + 1 null terminator)
*/
void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) {
static constexpr char hexChars[] = "0123456789ABCDEF";
uint32_t data;
memcpy(&data, trailer, 4);
serverETag[0] = hexChars[(data >> 4) & 0x0F];
serverETag[1] = hexChars[data & 0x0F];
serverETag[2] = hexChars[(data >> 12) & 0x0F];
serverETag[3] = hexChars[(data >> 8) & 0x0F];
serverETag[4] = hexChars[(data >> 20) & 0x0F];
serverETag[5] = hexChars[(data >> 16) & 0x0F];
serverETag[6] = hexChars[(data >> 28)];
serverETag[7] = hexChars[(data >> 24) & 0x0F];
serverETag[8] = '\0';
}

View file

@ -12,7 +12,7 @@ extern "C" {
/** Minor version number (x.X.x) */ /** Minor version number (x.X.x) */
#define ASYNCWEBSERVER_VERSION_MINOR 7 #define ASYNCWEBSERVER_VERSION_MINOR 7
/** Patch version number (x.x.X) */ /** Patch version number (x.x.X) */
#define ASYNCWEBSERVER_VERSION_PATCH 2 #define ASYNCWEBSERVER_VERSION_PATCH 10
/** /**
* Macro to convert version number into an integer * Macro to convert version number into an integer

View file

@ -17,6 +17,8 @@
#include <rom/ets_sys.h> #include <rom/ets_sys.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266) #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266)
#include <Hash.h> #include <Hash.h>
#elif defined(LIBRETINY)
#include <mbedtls/sha1.h>
#endif #endif
using namespace asyncsrv; using namespace asyncsrv;
@ -333,7 +335,7 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async
AsyncWebSocketClient::~AsyncWebSocketClient() { AsyncWebSocketClient::~AsyncWebSocketClient() {
{ {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
_messageQueue.clear(); _messageQueue.clear();
_controlQueue.clear(); _controlQueue.clear();
@ -351,7 +353,7 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
_lastMessageTime = millis(); _lastMessageTime = millis();
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::unique_lock<std::recursive_mutex> lock(_lock);
#endif #endif
if (!_controlQueue.empty()) { if (!_controlQueue.empty()) {
@ -362,6 +364,14 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
_controlQueue.pop_front(); _controlQueue.pop_front();
_status = WS_DISCONNECTED; _status = WS_DISCONNECTED;
if (_client) { if (_client) {
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true); _client->close(true);
} }
return; return;
@ -385,7 +395,7 @@ void AsyncWebSocketClient::_onPoll() {
} }
#ifdef ESP32 #ifdef ESP32
std::unique_lock<std::mutex> lock(_lock); std::unique_lock<std::recursive_mutex> lock(_lock);
#endif #endif
if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) { if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) {
_runQueue(); _runQueue();
@ -415,21 +425,21 @@ void AsyncWebSocketClient::_runQueue() {
bool AsyncWebSocketClient::queueIsFull() const { bool AsyncWebSocketClient::queueIsFull() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED); return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED);
} }
size_t AsyncWebSocketClient::queueLen() const { size_t AsyncWebSocketClient::queueLen() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
return _messageQueue.size(); return _messageQueue.size();
} }
bool AsyncWebSocketClient::canSend() const { bool AsyncWebSocketClient::canSend() const {
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES; return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES;
} }
@ -440,7 +450,7 @@ bool AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t *data, si
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::lock_guard<std::recursive_mutex> lock(_lock);
#endif #endif
_controlQueue.emplace_back(opcode, data, len, mask); _controlQueue.emplace_back(opcode, data, len, mask);
@ -458,7 +468,7 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
} }
#ifdef ESP32 #ifdef ESP32
std::lock_guard<std::mutex> lock(_lock); std::unique_lock<std::recursive_mutex> lock(_lock);
#endif #endif
if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) { if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) {
@ -466,6 +476,14 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
_status = WS_DISCONNECTED; _status = WS_DISCONNECTED;
if (_client) { if (_client) {
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true); _client->close(true);
} }
@ -551,6 +569,7 @@ void AsyncWebSocketClient::_onTimeout(uint32_t time) {
void AsyncWebSocketClient::_onDisconnect() { void AsyncWebSocketClient::_onDisconnect() {
// Serial.println("onDis"); // Serial.println("onDis");
_client = nullptr; _client = nullptr;
_server->_handleDisconnect(this);
} }
void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
@ -857,6 +876,16 @@ AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request)
return &_clients.back(); return &_clients.back();
} }
void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) {
const auto client_id = client->id();
const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [client_id](const AsyncWebSocketClient &c) {
return c.id() == client_id;
});
if (iter != std::end(_clients)) {
_clients.erase(iter);
}
}
bool AsyncWebSocket::availableForWriteAll() { bool AsyncWebSocket::availableForWriteAll() {
return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) {
return c.queueIsFull(); return c.queueIsFull();
@ -1300,11 +1329,20 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket
} }
k.concat(key); k.concat(key);
k.concat(WS_STR_UUID); k.concat(WS_STR_UUID);
#ifdef LIBRETINY
mbedtls_sha1_context ctx;
mbedtls_sha1_init(&ctx);
mbedtls_sha1_starts(&ctx);
mbedtls_sha1_update(&ctx, (const uint8_t *)k.c_str(), k.length());
mbedtls_sha1_finish(&ctx, hash);
mbedtls_sha1_free(&ctx);
#else
SHA1Builder sha1; SHA1Builder sha1;
sha1.begin(); sha1.begin();
sha1.add((const uint8_t *)k.c_str(), k.length()); sha1.add((const uint8_t *)k.c_str(), k.length());
sha1.calculate(); sha1.calculate();
sha1.getBytes(hash); sha1.getBytes(hash);
#endif
#endif #endif
base64_encodestate _state; base64_encodestate _state;
base64_init_encodestate(&_state); base64_init_encodestate(&_state);

View file

@ -5,8 +5,14 @@
#define ASYNCWEBSOCKET_H_ #define ASYNCWEBSOCKET_H_
#include <Arduino.h> #include <Arduino.h>
#ifdef ESP32
#if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#ifdef LIBRETINY
#ifdef round
#undef round
#endif
#endif
#include <mutex> #include <mutex>
#ifndef WS_MAX_QUEUED_MESSAGES #ifndef WS_MAX_QUEUED_MESSAGES
#define WS_MAX_QUEUED_MESSAGES 32 #define WS_MAX_QUEUED_MESSAGES 32
@ -152,7 +158,7 @@ private:
uint32_t _clientId; uint32_t _clientId;
AwsClientStatus _status; AwsClientStatus _status;
#ifdef ESP32 #ifdef ESP32
mutable std::mutex _lock; mutable std::recursive_mutex _lock;
#endif #endif
std::deque<AsyncWebSocketControl> _controlQueue; std::deque<AsyncWebSocketControl> _controlQueue;
std::deque<AsyncWebSocketMessage> _messageQueue; std::deque<AsyncWebSocketMessage> _messageQueue;
@ -291,7 +297,7 @@ private:
String _url; String _url;
std::list<AsyncWebSocketClient> _clients; std::list<AsyncWebSocketClient> _clients;
uint32_t _cNextId; uint32_t _cNextId;
AwsEventHandler _eventHandler{nullptr}; AwsEventHandler _eventHandler;
AwsHandshakeHandler _handshakeHandler; AwsHandshakeHandler _handshakeHandler;
bool _enabled; bool _enabled;
#ifdef ESP32 #ifdef ESP32
@ -305,8 +311,8 @@ public:
PARTIALLY_ENQUEUED = 2, PARTIALLY_ENQUEUED = 2,
} SendStatus; } SendStatus;
explicit AsyncWebSocket(const char *url) : _url(url), _cNextId(1), _enabled(true) {} explicit AsyncWebSocket(const char *url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {}
AsyncWebSocket(const String &url) : _url(url), _cNextId(1), _enabled(true) {} AsyncWebSocket(const String &url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {}
~AsyncWebSocket(){}; ~AsyncWebSocket(){};
const char *url() const { const char *url() const {
return _url.c_str(); return _url.c_str();
@ -385,6 +391,7 @@ public:
return _cNextId++; return _cNextId++;
} }
AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request); AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request);
void _handleDisconnect(AsyncWebSocketClient *client);
void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
bool canHandle(AsyncWebServerRequest *request) const override final; bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final; void handleRequest(AsyncWebServerRequest *request) override final;
@ -413,4 +420,86 @@ public:
} }
}; };
class AsyncWebSocketMessageHandler {
public:
AwsEventHandler eventHandler() const {
return _handler;
}
void onConnect(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> onConnect) {
_onConnect = onConnect;
}
void onDisconnect(std::function<void(AsyncWebSocket *server, uint32_t clientId)> onDisconnect) {
_onDisconnect = onDisconnect;
}
/**
* Error callback
* @param reason null-terminated string
* @param len length of the string
*/
void onError(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> onError) {
_onError = onError;
}
/**
* Complete message callback
* @param data pointer to the data (binary or null-terminated string). This handler expects the user to know which data type he uses.
*/
void onMessage(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> onMessage) {
_onMessage = onMessage;
}
/**
* Fragmented message callback
* @param data pointer to the data (binary or null-terminated string), will be null-terminated. This handler expects the user to know which data type he uses.
*/
// clang-format off
void onFragment(std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> onFragment) {
_onFragment = onFragment;
}
// clang-format on
private:
// clang-format off
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client)> _onConnect;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len)> _onError;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len)> _onMessage;
std::function<void(AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len)> _onFragment;
std::function<void(AsyncWebSocket *server, uint32_t clientId)> _onDisconnect;
// clang-format on
// this handler is meant to only support 1-frame messages (== unfragmented messages)
AwsEventHandler _handler = [this](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_CONNECT) {
if (_onConnect) {
_onConnect(server, client);
}
} else if (type == WS_EVT_DISCONNECT) {
if (_onDisconnect) {
_onDisconnect(server, client->id());
}
} else if (type == WS_EVT_ERROR) {
if (_onError) {
_onError(server, client, *((uint16_t *)arg), (const char *)data, len);
}
} else if (type == WS_EVT_DATA) {
AwsFrameInfo *info = (AwsFrameInfo *)arg;
if (info->opcode == WS_TEXT) {
data[len] = 0;
}
if (info->final && info->index == 0 && info->len == len) {
if (_onMessage) {
_onMessage(server, client, data, len);
}
} else {
if (_onFragment) {
_onFragment(server, client, info, data, len);
}
}
}
};
};
#endif /* ASYNCWEBSOCKET_H_ */ #endif /* ASYNCWEBSOCKET_H_ */

View file

@ -4,9 +4,10 @@
#ifndef _ESPAsyncWebServer_H_ #ifndef _ESPAsyncWebServer_H_
#define _ESPAsyncWebServer_H_ #define _ESPAsyncWebServer_H_
#include "Arduino.h" #include <Arduino.h>
#include <FS.h>
#include <lwip/tcpbase.h>
#include "FS.h"
#include <algorithm> #include <algorithm>
#include <deque> #include <deque>
#include <functional> #include <functional>
@ -14,16 +15,13 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#ifdef ESP32 #if defined(ESP32) || defined(LIBRETINY)
#include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h"
#include <WiFi.h>
#elif defined(ESP8266) #elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h> #include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h> #include <RPAsyncTCP.h>
#include <HTTP_Method.h> #include <HTTP_Method.h>
#include <WiFi.h>
#include <http_parser.h> #include <http_parser.h>
#else #else
#error Platform not supported #error Platform not supported
@ -131,12 +129,20 @@ private:
String _value; String _value;
public: public:
AsyncWebHeader() {}
AsyncWebHeader(const AsyncWebHeader &) = default; AsyncWebHeader(const AsyncWebHeader &) = default;
AsyncWebHeader(AsyncWebHeader &&) = default;
AsyncWebHeader(const char *name, const char *value) : _name(name), _value(value) {} AsyncWebHeader(const char *name, const char *value) : _name(name), _value(value) {}
AsyncWebHeader(const String &name, const String &value) : _name(name), _value(value) {} AsyncWebHeader(const String &name, const String &value) : _name(name), _value(value) {}
AsyncWebHeader(const String &data);
#ifndef ESP8266
[[deprecated("Use AsyncWebHeader::parse(data) instead")]]
#endif
AsyncWebHeader(const String &data)
: AsyncWebHeader(parse(data)){};
AsyncWebHeader &operator=(const AsyncWebHeader &) = default; AsyncWebHeader &operator=(const AsyncWebHeader &) = default;
AsyncWebHeader &operator=(AsyncWebHeader &&other) = default;
const String &name() const { const String &name() const {
return _name; return _name;
@ -144,7 +150,18 @@ public:
const String &value() const { const String &value() const {
return _value; return _value;
} }
String toString() const; String toString() const;
// returns true if the header is valid
operator bool() const {
return _name.length();
}
static const AsyncWebHeader parse(const String &data) {
return parse(data.c_str());
}
static const AsyncWebHeader parse(const char *data);
}; };
/* /*
@ -180,6 +197,7 @@ class AsyncWebServerRequest {
using FS = fs::FS; using FS = fs::FS;
friend class AsyncWebServer; friend class AsyncWebServer;
friend class AsyncCallbackWebHandler; friend class AsyncCallbackWebHandler;
friend class AsyncFileResponse;
private: private:
AsyncClient *_client; AsyncClient *_client;
@ -251,6 +269,8 @@ private:
void _send(); void _send();
void _runMiddlewareChain(); void _runMiddlewareChain();
static void _getEtag(uint8_t trailer[4], char *serverETag);
public: public:
File _tempFile; File _tempFile;
void *_tempObject; void *_tempObject;
@ -363,13 +383,7 @@ public:
send(beginResponse(code, contentType, content, len, callback)); send(beginResponse(code, contentType, content, len, callback));
} }
void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) { void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr);
if (fs.exists(path) || (!download && fs.exists(path + asyncsrv::T__gz))) {
send(beginResponse(fs, path, contentType, download, callback));
} else {
send(404);
}
}
void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) { void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) {
send(fs, path, contentType.c_str(), download, callback); send(fs, path, contentType.c_str(), download, callback);
} }
@ -462,7 +476,9 @@ public:
} }
AsyncWebServerResponse *beginChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); AsyncWebServerResponse *beginChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncWebServerResponse *beginChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr); AsyncWebServerResponse *beginChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr) {
return beginChunkedResponse(contentType.c_str(), callback, templateCallback);
}
AsyncResponseStream *beginResponseStream(const char *contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE); AsyncResponseStream *beginResponseStream(const char *contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE);
AsyncResponseStream *beginResponseStream(const String &contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) { AsyncResponseStream *beginResponseStream(const String &contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) {
@ -531,6 +547,9 @@ public:
* @return const AsyncWebParameter* * @return const AsyncWebParameter*
*/ */
const AsyncWebParameter *getParam(size_t num) const; const AsyncWebParameter *getParam(size_t num) const;
const AsyncWebParameter *getParam(int num) const {
return num < 0 ? nullptr : getParam((size_t)num);
}
size_t args() const { size_t args() const {
return params(); return params();
@ -545,9 +564,15 @@ public:
#ifdef ESP8266 #ifdef ESP8266
const String &arg(const __FlashStringHelper *data) const; // get request argument value by F(name) const String &arg(const __FlashStringHelper *data) const; // get request argument value by F(name)
#endif #endif
const String &arg(size_t i) const; // get request argument value by number const String &arg(size_t i) const; // get request argument value by number
const String &arg(int i) const {
return i < 0 ? emptyString : arg((size_t)i);
};
const String &argName(size_t i) const; // get request argument name by number const String &argName(size_t i) const; // get request argument name by number
bool hasArg(const char *name) const; // check if argument exists const String &argName(int i) const {
return i < 0 ? emptyString : argName((size_t)i);
};
bool hasArg(const char *name) const; // check if argument exists
bool hasArg(const String &name) const { bool hasArg(const String &name) const {
return hasArg(name.c_str()); return hasArg(name.c_str());
}; };
@ -556,6 +581,9 @@ public:
#endif #endif
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const; const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const;
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(int i) const {
return i < 0 ? emptyString : pathArg((size_t)i);
}
// get request header value by name // get request header value by name
const String &header(const char *name) const; const String &header(const char *name) const;
@ -567,8 +595,14 @@ public:
const String &header(const __FlashStringHelper *data) const; // get request header value by F(name) const String &header(const __FlashStringHelper *data) const; // get request header value by F(name)
#endif #endif
const String &header(size_t i) const; // get request header value by number const String &header(size_t i) const; // get request header value by number
const String &header(int i) const {
return i < 0 ? emptyString : header((size_t)i);
};
const String &headerName(size_t i) const; // get request header name by number const String &headerName(size_t i) const; // get request header name by number
const String &headerName(int i) const {
return i < 0 ? emptyString : headerName((size_t)i);
};
size_t headers() const; // get header count size_t headers() const; // get header count
@ -590,6 +624,9 @@ public:
#endif #endif
const AsyncWebHeader *getHeader(size_t num) const; const AsyncWebHeader *getHeader(size_t num) const;
const AsyncWebHeader *getHeader(int num) const {
return num < 0 ? nullptr : getHeader((size_t)num);
};
const std::list<AsyncWebHeader> &getHeaders() const { const std::list<AsyncWebHeader> &getHeaders() const {
return _headers; return _headers;
@ -1011,6 +1048,10 @@ public:
setContentType(type.c_str()); setContentType(type.c_str());
} }
void setContentType(const char *type); void setContentType(const char *type);
bool addHeader(AsyncWebHeader &&header, bool replaceExisting = true);
bool addHeader(const AsyncWebHeader &header, bool replaceExisting = true) {
return header && addHeader(header.name(), header.value(), replaceExisting);
}
bool addHeader(const char *name, const char *value, bool replaceExisting = true); bool addHeader(const char *name, const char *value, bool replaceExisting = true);
bool addHeader(const String &name, const String &value, bool replaceExisting = true) { bool addHeader(const String &name, const String &value, bool replaceExisting = true) {
return addHeader(name.c_str(), value.c_str(), replaceExisting); return addHeader(name.c_str(), value.c_str(), replaceExisting);
@ -1069,6 +1110,15 @@ public:
void begin(); void begin();
void end(); void end();
tcp_state state() const {
#ifdef ESP8266
// ESPAsyncTCP and RPAsyncTCP methods are not corrected declared with const for immutable ones.
return static_cast<tcp_state>(const_cast<AsyncWebServer *>(this)->_server.status());
#else
return static_cast<tcp_state>(_server.status());
#endif
}
#if ASYNC_TCP_SSL_ENABLED #if ASYNC_TCP_SSL_ENABLED
void onSslFileRequest(AcSSlFileHandler cb, void *arg); void onSslFileRequest(AcSSlFileHandler cb, void *arg);
void beginSecure(const char *cert, const char *private_key_file, const char *password); void beginSecure(const char *cert, const char *private_key_file, const char *password);

View file

@ -172,7 +172,11 @@ void AsyncLoggingMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNex
return; return;
} }
_out->print(F("* Connection from ")); _out->print(F("* Connection from "));
#ifndef LIBRETINY
_out->print(request->client()->remoteIP().toString()); _out->print(request->client()->remoteIP().toString());
#else
_out->print(request->client()->remoteIP());
#endif
_out->print(':'); _out->print(':');
_out->println(request->client()->remotePort()); _out->println(request->client()->remotePort());
_out->print('>'); _out->print('>');

View file

@ -19,7 +19,6 @@ class AsyncStaticWebHandler : public AsyncWebHandler {
private: private:
bool _getFile(AsyncWebServerRequest *request) const; bool _getFile(AsyncWebServerRequest *request) const;
bool _searchFile(AsyncWebServerRequest *request, const String &path); bool _searchFile(AsyncWebServerRequest *request, const String &path);
uint8_t _countBits(const uint8_t value) const;
protected: protected:
FS _fs; FS _fs;

View file

@ -187,15 +187,6 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
return found; return found;
} }
uint8_t AsyncStaticWebHandler::_countBits(const uint8_t value) const {
uint8_t w = value;
uint8_t n;
for (n = 0; w != 0; n++) {
w &= w - 1;
}
return n;
}
void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
// Get the filename from request->_tempObject and free it // Get the filename from request->_tempObject and free it
String filename((char *)request->_tempObject); String filename((char *)request->_tempObject);
@ -218,11 +209,14 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
char buf[len]; char buf[len];
char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10); char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10);
etag = ret ? String(ret) : String(request->_tempFile.size()); etag = ret ? String(ret) : String(request->_tempFile.size());
#elif defined(LIBRETINY)
long val = lw ^ request->_tempFile.size();
etag = String(val);
#else #else
etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp
#endif #endif
} else { } else {
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
etag = String(request->_tempFile.size()); etag = String(request->_tempFile.size());
#else #else
etag = request->_tempFile.size(); etag = request->_tempFile.size();

View file

@ -22,10 +22,10 @@ enum {
}; };
AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c) AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c)
: _client(c), _server(s), _handler(NULL), _response(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), _url(), _host(), : _client(c), _server(s), _handler(NULL), _response(NULL), _onDisconnectfn(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY),
_contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false),
_expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0),
_itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) {
c->onError( c->onError(
[](void *r, AsyncClient *c, int8_t error) { [](void *r, AsyncClient *c, int8_t error) {
(void)c; (void)c;
@ -341,10 +341,10 @@ bool AsyncWebServerRequest::_parseReqHead() {
} }
bool AsyncWebServerRequest::_parseReqHeader() { bool AsyncWebServerRequest::_parseReqHeader() {
int index = _temp.indexOf(':'); AsyncWebHeader header = AsyncWebHeader::parse(_temp);
if (index) { if (header) {
String name(_temp.substring(0, index)); const String &name = header.name();
String value(_temp.substring(index + 2)); const String &value = header.value();
if (name.equalsIgnoreCase(T_Host)) { if (name.equalsIgnoreCase(T_Host)) {
_host = value; _host = value;
} else if (name.equalsIgnoreCase(T_Content_Type)) { } else if (name.equalsIgnoreCase(T_Content_Type)) {
@ -392,9 +392,9 @@ bool AsyncWebServerRequest::_parseReqHeader() {
_reqconntype = RCT_EVENT; _reqconntype = RCT_EVENT;
} }
} }
_headers.emplace_back(name, value); _headers.emplace_back(std::move(header));
} }
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
// Ancient PRI core does not have String::clear() method 8-() // Ancient PRI core does not have String::clear() method 8-()
_temp = emptyString; _temp = emptyString;
#else #else
@ -419,7 +419,7 @@ void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) {
_params.emplace_back(name, urlDecode(value), true); _params.emplace_back(name, urlDecode(value), true);
} }
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
// Ancient PRI core does not have String::clear() method 8-() // Ancient PRI core does not have String::clear() method 8-()
_temp = emptyString; _temp = emptyString;
#else #else

View file

@ -10,7 +10,7 @@
#undef max #undef max
#endif #endif
#include "literals.h" #include "literals.h"
#include <StreamString.h> #include <cbuf.h>
#include <memory> #include <memory>
#include <vector> #include <vector>
@ -157,7 +157,7 @@ public:
class AsyncResponseStream : public AsyncAbstractResponse, public Print { class AsyncResponseStream : public AsyncAbstractResponse, public Print {
private: private:
StreamString _content; std::unique_ptr<cbuf> _content;
public: public:
AsyncResponseStream(const char *contentType, size_t bufferSize); AsyncResponseStream(const char *contentType, size_t bufferSize);
@ -168,6 +168,12 @@ public:
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t write(const uint8_t *data, size_t len); size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data); size_t write(uint8_t data);
/**
* @brief Returns the number of bytes available in the stream.
*/
size_t available() const {
return _content->available();
}
using Print::write; using Print::write;
}; };

View file

@ -134,6 +134,30 @@ bool AsyncWebServerResponse::headerMustBePresentOnce(const String &name) {
return false; return false;
} }
bool AsyncWebServerResponse::addHeader(AsyncWebHeader &&header, bool replaceExisting) {
if (!header) {
return false; // invalid header
}
for (auto i = _headers.begin(); i != _headers.end(); ++i) {
if (i->name().equalsIgnoreCase(header.name())) {
// header already set
if (replaceExisting) {
// remove, break and add the new one
_headers.erase(i);
break;
} else if (headerMustBePresentOnce(i->name())) { // we can have only one header with that name
// do not update
return false;
} else {
break; // accept multiple headers with the same name
}
}
}
// header was not found found, or existing one was removed
_headers.emplace_back(std::move(header));
return true;
}
bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) { bool AsyncWebServerResponse::addHeader(const char *name, const char *value, bool replaceExisting) {
for (auto i = _headers.begin(); i != _headers.end(); ++i) { for (auto i = _headers.begin(); i != _headers.end(); ++i) {
if (i->name().equalsIgnoreCase(name)) { if (i->name().equalsIgnoreCase(name)) {
@ -595,6 +619,16 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size
* File Response * File Response
* */ * */
/**
* @brief Sets the content type based on the file path extension
*
* This method determines the appropriate MIME content type for a file based on its
* file extension. It supports both external content type functions (if available)
* and an internal mapping of common file extensions to their corresponding MIME types.
*
* @param path The file path string from which to extract the extension
* @note The method modifies the internal _contentType member variable
*/
void AsyncFileResponse::_setContentTypeFromPath(const String &path) { void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#if HAVE_EXTERN_GET_Content_Type_FUNCTION #if HAVE_EXTERN_GET_Content_Type_FUNCTION
#ifndef ESP8266 #ifndef ESP8266
@ -604,41 +638,47 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#endif #endif
_contentType = getContentType(path); _contentType = getContentType(path);
#else #else
if (path.endsWith(T__html)) { const char *cpath = path.c_str();
const char *dot = strrchr(cpath, '.');
if (!dot) {
_contentType = T_text_plain;
return;
}
if (strcmp(dot, T__html) == 0 || strcmp(dot, T__htm) == 0) {
_contentType = T_text_html; _contentType = T_text_html;
} else if (path.endsWith(T__htm)) { } else if (strcmp(dot, T__css) == 0) {
_contentType = T_text_html;
} else if (path.endsWith(T__css)) {
_contentType = T_text_css; _contentType = T_text_css;
} else if (path.endsWith(T__json)) { } else if (strcmp(dot, T__js) == 0) {
_contentType = T_application_json;
} else if (path.endsWith(T__js)) {
_contentType = T_application_javascript; _contentType = T_application_javascript;
} else if (path.endsWith(T__png)) { } else if (strcmp(dot, T__json) == 0) {
_contentType = T_application_json;
} else if (strcmp(dot, T__png) == 0) {
_contentType = T_image_png; _contentType = T_image_png;
} else if (path.endsWith(T__gif)) { } else if (strcmp(dot, T__ico) == 0) {
_contentType = T_image_gif;
} else if (path.endsWith(T__jpg)) {
_contentType = T_image_jpeg;
} else if (path.endsWith(T__ico)) {
_contentType = T_image_x_icon; _contentType = T_image_x_icon;
} else if (path.endsWith(T__svg)) { } else if (strcmp(dot, T__svg) == 0) {
_contentType = T_image_svg_xml; _contentType = T_image_svg_xml;
} else if (path.endsWith(T__eot)) { } else if (strcmp(dot, T__jpg) == 0) {
_contentType = T_font_eot; _contentType = T_image_jpeg;
} else if (path.endsWith(T__woff)) { } else if (strcmp(dot, T__gif) == 0) {
_contentType = T_font_woff; _contentType = T_image_gif;
} else if (path.endsWith(T__woff2)) { } else if (strcmp(dot, T__woff2) == 0) {
_contentType = T_font_woff2; _contentType = T_font_woff2;
} else if (path.endsWith(T__ttf)) { } else if (strcmp(dot, T__woff) == 0) {
_contentType = T_font_woff;
} else if (strcmp(dot, T__ttf) == 0) {
_contentType = T_font_ttf; _contentType = T_font_ttf;
} else if (path.endsWith(T__xml)) { } else if (strcmp(dot, T__eot) == 0) {
_contentType = T_font_eot;
} else if (strcmp(dot, T__xml) == 0) {
_contentType = T_text_xml; _contentType = T_text_xml;
} else if (path.endsWith(T__pdf)) { } else if (strcmp(dot, T__pdf) == 0) {
_contentType = T_application_pdf; _contentType = T_application_pdf;
} else if (path.endsWith(T__zip)) { } else if (strcmp(dot, T__zip) == 0) {
_contentType = T_application_zip; _contentType = T_application_zip;
} else if (path.endsWith(T__gz)) { } else if (strcmp(dot, T__gz) == 0) {
_contentType = T_application_x_gzip; _contentType = T_application_x_gzip;
} else { } else {
_contentType = T_text_plain; _contentType = T_text_plain;
@ -646,40 +686,73 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
#endif #endif
} }
/**
* @brief Constructor for AsyncFileResponse that handles file serving with compression support
*
* This constructor creates an AsyncFileResponse object that can serve files from a filesystem,
* with automatic fallback to gzip-compressed versions if the original file is not found.
* It also handles ETag generation for caching and supports both inline and download modes.
*
* @param fs Reference to the filesystem object used to open files
* @param path Path to the file to be served (without compression extension)
* @param contentType MIME type of the file content (empty string for auto-detection)
* @param download If true, file will be served as download attachment; if false, as inline content
* @param callback Template processor callback for dynamic content processing
*/
AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) { : AsyncAbstractResponse(callback) {
_code = 200; // Try to open the uncompressed version first
_path = path; _content = fs.open(path, fs::FileOpenMode::read);
if (_content.available()) {
_path = path;
_contentLength = _content.size();
} else {
// Try to open the compressed version (.gz)
_path = path + asyncsrv::T__gz;
_content = fs.open(_path, fs::FileOpenMode::read);
_contentLength = _content.size();
if (!download && !fs.exists(_path) && fs.exists(_path + T__gz)) { if (_content.seek(_contentLength - 8)) {
_path = _path + T__gz; addHeader(T_Content_Encoding, T_gzip, false);
addHeader(T_Content_Encoding, T_gzip, false); _callback = nullptr; // Unable to process zipped templates
_callback = nullptr; // Unable to process zipped templates _sendContentLength = true;
_sendContentLength = true; _chunked = false;
_chunked = false;
// Add ETag and cache headers
uint8_t crcInTrailer[4];
_content.read(crcInTrailer, sizeof(crcInTrailer));
char serverETag[9];
AsyncWebServerRequest::_getEtag(crcInTrailer, serverETag);
addHeader(T_ETag, serverETag, true);
addHeader(T_Cache_Control, T_no_cache, true);
_content.seek(0);
} else {
// File is corrupted or invalid
_code = 404;
return;
}
} }
_content = fs.open(_path, fs::FileOpenMode::read); if (*contentType != '\0') {
_contentLength = _content.size();
if (strlen(contentType) == 0) {
_setContentTypeFromPath(path); _setContentTypeFromPath(path);
} else { } else {
_contentType = contentType; _contentType = contentType;
} }
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26 + path.length() - filenameStart];
char *filename = (char *)path.c_str() + filenameStart;
if (download) { if (download) {
// set filename and force download // Extract filename from path and set as download attachment
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26 + path.length() - filenameStart];
char *filename = (char *)path.c_str() + filenameStart;
snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename);
addHeader(T_Content_Disposition, buf, false);
} else { } else {
// set filename and force rendering // Serve file inline (display in browser)
snprintf_P(buf, sizeof(buf), PSTR("inline")); addHeader(T_Content_Disposition, PSTR("inline"), false);
} }
addHeader(T_Content_Disposition, buf, false);
_code = 200;
} }
AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
@ -820,7 +893,9 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
_code = 200; _code = 200;
_contentLength = 0; _contentLength = 0;
_contentType = contentType; _contentType = contentType;
if (!_content.reserve(bufferSize)) { // internal buffer will be null on allocation failure
_content = std::unique_ptr<cbuf>(new cbuf(bufferSize));
if (bufferSize && _content->size() < bufferSize) {
#ifdef ESP32 #ifdef ESP32
log_e("Failed to allocate"); log_e("Failed to allocate");
#endif #endif
@ -828,14 +903,26 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
} }
size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) { size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) {
return _content.readBytes((char *)buf, maxLen); return _content->read((char *)buf, maxLen);
} }
size_t AsyncResponseStream::write(const uint8_t *data, size_t len) { size_t AsyncResponseStream::write(const uint8_t *data, size_t len) {
if (_started()) { if (_started()) {
return 0; return 0;
} }
size_t written = _content.write(data, len); if (len > _content->room()) {
size_t needed = len - _content->room();
_content->resizeAdd(needed);
// log a warning if allocation failed, but do not return: keep writing the bytes we can
// with _content->write: if len is more than the available size in the buffer, only
// the available size will be written
if (len > _content->room()) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
}
}
size_t written = _content->write((const char *)data, len);
_contentLength += written; _contentLength += written;
return written; return written;
} }

View file

@ -4,10 +4,18 @@
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#include "WebHandlerImpl.h" #include "WebHandlerImpl.h"
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#else
#error Platform not supported
#endif
using namespace asyncsrv; using namespace asyncsrv;
bool ON_STA_FILTER(AsyncWebServerRequest *request) { bool ON_STA_FILTER(AsyncWebServerRequest *request) {
#ifndef CONFIG_IDF_TARGET_ESP32H2 #if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
return WiFi.localIP() == request->client()->localIP(); return WiFi.localIP() == request->client()->localIP();
#else #else
return false; return false;
@ -15,7 +23,7 @@ bool ON_STA_FILTER(AsyncWebServerRequest *request) {
} }
bool ON_AP_FILTER(AsyncWebServerRequest *request) { bool ON_AP_FILTER(AsyncWebServerRequest *request) {
#ifndef CONFIG_IDF_TARGET_ESP32H2 #if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
return WiFi.localIP() != request->client()->localIP(); return WiFi.localIP() != request->client()->localIP();
#else #else
return false; return false;

View file

@ -300,7 +300,7 @@ class AsyncServer : public AsyncSocketBase
void setNoDelay(bool nodelay) { _noDelay = nodelay; } void setNoDelay(bool nodelay) { _noDelay = nodelay; }
bool getNoDelay() { return _noDelay; } bool getNoDelay() { return _noDelay; }
uint8_t status(); uint8_t status() const;
protected: protected:
uint16_t _port; uint16_t _port;