diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/README.md b/Software/src/lib/ESP32Async-ESPAsyncWebServer/README.md index 92fec339..1db7aee6 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/README.md +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/README.md @@ -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) -- 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) @@ -72,6 +72,19 @@ lib_deps = 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 **AsyncTCPSock** @@ -100,7 +113,7 @@ platform = https://github.com/maxgerhardt/platform-raspberrypi.git board = rpipicow board_build.core = earlephilhower lib_deps = - ayushsharma82/RPAsyncTCP@^1.3.1 + ayushsharma82/RPAsyncTCP@^1.3.2 ESP32Async/ESPAsyncWebServer lib_ignore = lwIP_ESPHost diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.json b/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.json index 22cb862a..836ea27c 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.json +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.json @@ -1,6 +1,6 @@ { "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.", "keywords": "http,async,websocket,webserver", "homepage": "https://github.com/ESP32Async/ESPAsyncWebServer", @@ -18,14 +18,18 @@ "platforms": [ "espressif32", "espressif8266", - "raspberrypi" + "raspberrypi", + "libretiny" ], "dependencies": [ { "owner": "ESP32Async", "name": "AsyncTCP", - "version": "^3.3.6", - "platforms": "espressif32" + "version": "^3.4.5", + "platforms": [ + "espressif32", + "libretiny" + ] }, { "owner": "ESP32Async", @@ -40,7 +44,7 @@ { "owner": "ayushsharma82", "name": "RPAsyncTCP", - "version": "^1.3.1", + "version": "^1.3.2", "platforms": "raspberrypi" } ], diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.properties b/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.properties index 0f7229af..f662ff1c 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.properties +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/library.properties @@ -1,6 +1,6 @@ name=ESP Async WebServer includes=ESPAsyncWebServer.h -version=3.7.2 +version=3.7.10 author=ESP32Async maintainer=ESP32Async sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.cpp index 797c5d13..2ebfa2d3 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.cpp @@ -193,7 +193,7 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A AsyncEventSourceClient::~AsyncEventSourceClient() { #ifdef ESP32 - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _messageQueue.clear(); close(); @@ -211,7 +211,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) { #ifdef ESP32 // length() is not thread-safe, thus acquiring the lock before this call.. - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _messageQueue.emplace_back(message, len); @@ -241,7 +241,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) { #ifdef ESP32 // length() is not thread-safe, thus acquiring the lock before this call.. - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _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))) { #ifdef ESP32 // Same here, acquiring the lock early - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif // adjust in-flight len @@ -290,7 +290,7 @@ void AsyncEventSourceClient::_onPoll() { if (_messageQueue.size()) { #ifdef ESP32 // Same here, acquiring the lock early - std::lock_guard lock(_lockmq); + std::lock_guard lock(_lockmq); #endif _runQueue(); } @@ -367,7 +367,7 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient *client) { return; } #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif _clients.emplace_back(client); if (_connectcb) { @@ -382,7 +382,7 @@ void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient *client) { _disconnectcb(client); } #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif for (auto i = _clients.begin(); i != _clients.end(); ++i) { if (i->get() == client) { @@ -398,10 +398,15 @@ void AsyncEventSource::close() { // iterator should remain valid even when AsyncEventSource::_handleDisconnect() // is called very early #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif for (const auto &c : _clients) { 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(); } } @@ -412,7 +417,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const { size_t aql = 0; uint32_t nConnectedClients = 0; #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif if (!_clients.size()) { 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) { AsyncEvent_SharedData_t shared_msg = std::make_shared(generateEventMessage(message, event, id, reconnect)); #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif size_t hits = 0; size_t miss = 0; @@ -446,7 +451,7 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c size_t AsyncEventSource::count() const { #ifdef ESP32 - std::lock_guard lock(_client_queue_lock); + std::lock_guard lock(_client_queue_lock); #endif size_t n_clients{0}; for (const auto &i : _clients) { diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h index 50f0c6f3..abd0338c 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncEventSource.h @@ -6,8 +6,13 @@ #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" +#ifdef LIBRETINY +#ifdef round +#undef round +#endif +#endif #include #ifndef SSE_MAX_QUEUED_MESSAGES #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 std::list _messageQueue; #ifdef ESP32 - mutable std::mutex _lockmq; + mutable std::recursive_mutex _lockmq; #endif bool _queueMessage(const char *message, size_t len); bool _queueMessage(AsyncEvent_SharedData_t &&msg); @@ -230,7 +235,7 @@ private: #ifdef ESP32 // Same as for individual messages, protect mutations of _clients list // since simultaneous access from different tasks is possible - mutable std::mutex _client_queue_lock; + mutable std::recursive_mutex _client_queue_lock; #endif ArEventHandlerFunction _connectcb = nullptr; ArEventHandlerFunction _disconnectcb = nullptr; diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.cpp index b8d014b2..8381f75e 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.cpp @@ -113,53 +113,77 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) { if (_onRequest) { + // GET request: if (request->method() == HTTP_GET) { JsonVariant json; _onRequest(request, json); 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 - DynamicJsonBuffer jsonBuffer; - JsonVariant json = jsonBuffer.parse((uint8_t *)(request->_tempObject)); - if (json.success()) { + DynamicJsonBuffer jsonBuffer; + JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject); + if (json.success()) { #elif ARDUINOJSON_VERSION_MAJOR == 6 - DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); + DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize); + DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject); + if (!error) { + JsonVariant json = jsonBuffer.as(); #else - JsonDocument jsonBuffer; - DeserializationError error = deserializeJson(jsonBuffer, (uint8_t *)(request->_tempObject)); - if (!error) { - JsonVariant json = jsonBuffer.as(); + JsonDocument jsonBuffer; + DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject); + if (!error) { + JsonVariant json = jsonBuffer.as(); #endif - _onRequest(request, json); - return; - } + _onRequest(request, json); + } 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) { if (_onRequest) { - _contentLength = total; - if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) { - request->_tempObject = malloc(total); + // ignore callback if size is larger than maxContentLength + if (total > _maxContentLength) { + return; + } + + if (index == 0) { + // this check allows request->_tempObject to be initialized from a middleware if (request->_tempObject == NULL) { + request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string + if (request->_tempObject == NULL) { #ifdef ESP32 - log_e("Failed to allocate"); + log_e("Failed to allocate"); #endif - request->abort(); - return; + request->abort(); + return; + } } } + if (request->_tempObject != NULL) { - memcpy((uint8_t *)(request->_tempObject) + index, data, len); + uint8_t *buffer = (uint8_t *)request->_tempObject; + memcpy(buffer + index, data, len); } } } diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.h index b5777d63..2194069d 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncJson.h @@ -79,7 +79,6 @@ protected: String _uri; WebRequestMethodComposite _method; ArJsonRequestHandlerFunction _onRequest; - size_t _contentLength; #if ARDUINOJSON_VERSION_MAJOR == 6 size_t maxJsonBufferSize; #endif diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebHeader.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebHeader.cpp index b35de815..0721fc5e 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebHeader.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebHeader.cpp @@ -3,30 +3,32 @@ #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) { - return; + return AsyncWebHeader(); // nullptr } - int index = data.indexOf(':'); - if (index < 0) { - return; + if (data[0] == '\0') { + return AsyncWebHeader(); // empty string } - _name = data.substring(0, index); - _value = data.substring(index + 2); -} - -String AsyncWebHeader::toString() const { - String str; - if (str.reserve(_name.length() + _value.length() + 2)) { - str.concat(_name); - str.concat((char)0x3a); - str.concat((char)0x20); - str.concat(_value); - str.concat(asyncsrv::T_rn); - } else { -#ifdef ESP32 - log_e("Failed to allocate"); -#endif - } - return str; + if (strchr(data, '\n') || strchr(data, '\r')) { + return AsyncWebHeader(); // Invalid header format + } + char *colon = strchr(data, ':'); + if (!colon) { + return AsyncWebHeader(); // separator not found + } + if (colon == data) { + return AsyncWebHeader(); // Header name cannot be empty + } + char *startOfValue = colon + 1; // Skip the colon + // skip one optional whitespace after the colon + if (*startOfValue == ' ') { + startOfValue++; + } + String name; + name.reserve(colon - data); + name.concat(data, colon - data); + return AsyncWebHeader(name, String(startOfValue)); } diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebServerRequest.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebServerRequest.cpp new file mode 100644 index 00000000..c04ba198 --- /dev/null +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebServerRequest.cpp @@ -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'; +} diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebServerVersion.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebServerVersion.h index 35aeba3d..b2494a3e 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebServerVersion.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebServerVersion.h @@ -12,7 +12,7 @@ extern "C" { /** Minor version number (x.X.x) */ #define ASYNCWEBSERVER_VERSION_MINOR 7 /** Patch version number (x.x.X) */ -#define ASYNCWEBSERVER_VERSION_PATCH 2 +#define ASYNCWEBSERVER_VERSION_PATCH 10 /** * Macro to convert version number into an integer diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.cpp index 3f91ae77..bc093b96 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.cpp @@ -17,6 +17,8 @@ #include #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(ESP8266) #include +#elif defined(LIBRETINY) +#include #endif using namespace asyncsrv; @@ -333,7 +335,7 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async AsyncWebSocketClient::~AsyncWebSocketClient() { { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif _messageQueue.clear(); _controlQueue.clear(); @@ -351,7 +353,7 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { _lastMessageTime = millis(); #ifdef ESP32 - std::lock_guard lock(_lock); + std::unique_lock lock(_lock); #endif if (!_controlQueue.empty()) { @@ -362,6 +364,14 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { _controlQueue.pop_front(); _status = WS_DISCONNECTED; 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); } return; @@ -385,7 +395,7 @@ void AsyncWebSocketClient::_onPoll() { } #ifdef ESP32 - std::unique_lock lock(_lock); + std::unique_lock lock(_lock); #endif if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) { _runQueue(); @@ -415,21 +425,21 @@ void AsyncWebSocketClient::_runQueue() { bool AsyncWebSocketClient::queueIsFull() const { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED); } size_t AsyncWebSocketClient::queueLen() const { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif return _messageQueue.size(); } bool AsyncWebSocketClient::canSend() const { #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES; } @@ -440,7 +450,7 @@ bool AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t *data, si } #ifdef ESP32 - std::lock_guard lock(_lock); + std::lock_guard lock(_lock); #endif _controlQueue.emplace_back(opcode, data, len, mask); @@ -458,7 +468,7 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint } #ifdef ESP32 - std::lock_guard lock(_lock); + std::unique_lock lock(_lock); #endif if (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) { @@ -466,6 +476,14 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint _status = WS_DISCONNECTED; 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); } @@ -551,6 +569,7 @@ void AsyncWebSocketClient::_onTimeout(uint32_t time) { void AsyncWebSocketClient::_onDisconnect() { // Serial.println("onDis"); _client = nullptr; + _server->_handleDisconnect(this); } void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { @@ -857,6 +876,16 @@ AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) 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() { return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { return c.queueIsFull(); @@ -1300,11 +1329,20 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket } k.concat(key); 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; sha1.begin(); sha1.add((const uint8_t *)k.c_str(), k.length()); sha1.calculate(); sha1.getBytes(hash); +#endif #endif base64_encodestate _state; base64_init_encodestate(&_state); diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h index 072239f7..bfc2293b 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/AsyncWebSocket.h @@ -5,8 +5,14 @@ #define ASYNCWEBSOCKET_H_ #include -#ifdef ESP32 + +#if defined(ESP32) || defined(LIBRETINY) #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" +#ifdef LIBRETINY +#ifdef round +#undef round +#endif +#endif #include #ifndef WS_MAX_QUEUED_MESSAGES #define WS_MAX_QUEUED_MESSAGES 32 @@ -152,7 +158,7 @@ private: uint32_t _clientId; AwsClientStatus _status; #ifdef ESP32 - mutable std::mutex _lock; + mutable std::recursive_mutex _lock; #endif std::deque _controlQueue; std::deque _messageQueue; @@ -291,7 +297,7 @@ private: String _url; std::list _clients; uint32_t _cNextId; - AwsEventHandler _eventHandler{nullptr}; + AwsEventHandler _eventHandler; AwsHandshakeHandler _handshakeHandler; bool _enabled; #ifdef ESP32 @@ -305,8 +311,8 @@ public: PARTIALLY_ENQUEUED = 2, } SendStatus; - explicit AsyncWebSocket(const char *url) : _url(url), _cNextId(1), _enabled(true) {} - AsyncWebSocket(const String &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, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {} ~AsyncWebSocket(){}; const char *url() const { return _url.c_str(); @@ -385,6 +391,7 @@ public: return _cNextId++; } AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request); + void _handleDisconnect(AsyncWebSocketClient *client); void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); bool canHandle(AsyncWebServerRequest *request) const 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 onConnect) { + _onConnect = onConnect; + } + + void onDisconnect(std::function onDisconnect) { + _onDisconnect = onDisconnect; + } + + /** + * Error callback + * @param reason null-terminated string + * @param len length of the string + */ + void onError(std::function 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 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 onFragment) { + _onFragment = onFragment; + } + // clang-format on + +private: + // clang-format off + std::function _onConnect; + std::function _onError; + std::function _onMessage; + std::function _onFragment; + std::function _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_ */ diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h index b1bfaf79..09703a91 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/ESPAsyncWebServer.h @@ -4,9 +4,10 @@ #ifndef _ESPAsyncWebServer_H_ #define _ESPAsyncWebServer_H_ -#include "Arduino.h" +#include +#include +#include -#include "FS.h" #include #include #include @@ -14,16 +15,13 @@ #include #include -#ifdef ESP32 +#if defined(ESP32) || defined(LIBRETINY) #include "../../mathieucarbou-AsyncTCPSock/src/AsyncTCP.h" -#include #elif defined(ESP8266) -#include #include #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) #include #include -#include #include #else #error Platform not supported @@ -131,12 +129,20 @@ private: String _value; public: + AsyncWebHeader() {} AsyncWebHeader(const AsyncWebHeader &) = default; + AsyncWebHeader(AsyncWebHeader &&) = default; 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 &data); + +#ifndef ESP8266 + [[deprecated("Use AsyncWebHeader::parse(data) instead")]] +#endif + AsyncWebHeader(const String &data) + : AsyncWebHeader(parse(data)){}; AsyncWebHeader &operator=(const AsyncWebHeader &) = default; + AsyncWebHeader &operator=(AsyncWebHeader &&other) = default; const String &name() const { return _name; @@ -144,7 +150,18 @@ public: const String &value() const { return _value; } + 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; friend class AsyncWebServer; friend class AsyncCallbackWebHandler; + friend class AsyncFileResponse; private: AsyncClient *_client; @@ -251,6 +269,8 @@ private: void _send(); void _runMiddlewareChain(); + static void _getEtag(uint8_t trailer[4], char *serverETag); + public: File _tempFile; void *_tempObject; @@ -363,13 +383,7 @@ public: 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) { - 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 char *contentType = asyncsrv::empty, 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); } @@ -462,7 +476,9 @@ public: } 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 String &contentType, size_t bufferSize = RESPONSE_STREAM_BUFFER_SIZE) { @@ -531,6 +547,9 @@ public: * @return const AsyncWebParameter* */ 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 { return params(); @@ -545,9 +564,15 @@ public: #ifdef ESP8266 const String &arg(const __FlashStringHelper *data) const; // get request argument value by F(name) #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 - 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 { return hasArg(name.c_str()); }; @@ -556,6 +581,9 @@ public: #endif 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 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) #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(int i) const { + return i < 0 ? emptyString : headerName((size_t)i); + }; size_t headers() const; // get header count @@ -590,6 +624,9 @@ public: #endif const AsyncWebHeader *getHeader(size_t num) const; + const AsyncWebHeader *getHeader(int num) const { + return num < 0 ? nullptr : getHeader((size_t)num); + }; const std::list &getHeaders() const { return _headers; @@ -1011,6 +1048,10 @@ public: setContentType(type.c_str()); } 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 String &name, const String &value, bool replaceExisting = true) { return addHeader(name.c_str(), value.c_str(), replaceExisting); @@ -1069,6 +1110,15 @@ public: void begin(); void end(); + tcp_state state() const { +#ifdef ESP8266 + // ESPAsyncTCP and RPAsyncTCP methods are not corrected declared with const for immutable ones. + return static_cast(const_cast(this)->_server.status()); +#else + return static_cast(_server.status()); +#endif + } + #if ASYNC_TCP_SSL_ENABLED void onSslFileRequest(AcSSlFileHandler cb, void *arg); void beginSecure(const char *cert, const char *private_key_file, const char *password); diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/Middleware.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/Middleware.cpp index 8598563e..f174f7bd 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/Middleware.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/Middleware.cpp @@ -172,7 +172,11 @@ void AsyncLoggingMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNex return; } _out->print(F("* Connection from ")); +#ifndef LIBRETINY _out->print(request->client()->remoteIP().toString()); +#else + _out->print(request->client()->remoteIP()); +#endif _out->print(':'); _out->println(request->client()->remotePort()); _out->print('>'); diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlerImpl.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlerImpl.h index 99a91114..1f68d627 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlerImpl.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlerImpl.h @@ -19,7 +19,6 @@ class AsyncStaticWebHandler : public AsyncWebHandler { private: bool _getFile(AsyncWebServerRequest *request) const; bool _searchFile(AsyncWebServerRequest *request, const String &path); - uint8_t _countBits(const uint8_t value) const; protected: FS _fs; diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlers.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlers.cpp index 516ffc28..21dba006 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlers.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebHandlers.cpp @@ -187,15 +187,6 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St 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) { // Get the filename from request->_tempObject and free it String filename((char *)request->_tempObject); @@ -218,11 +209,14 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { char buf[len]; char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10); etag = ret ? String(ret) : String(request->_tempFile.size()); +#elif defined(LIBRETINY) + long val = lw ^ request->_tempFile.size(); + etag = String(val); #else etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp #endif } 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()); #else etag = request->_tempFile.size(); diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebRequest.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebRequest.cpp index 8b735af3..ff4cf4cb 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebRequest.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebRequest.cpp @@ -22,10 +22,10 @@ enum { }; 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(), - _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), _isPlainPost(false), - _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), - _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { + : _client(c), _server(s), _handler(NULL), _response(NULL), _onDisconnectfn(NULL), _temp(), _parseState(PARSE_REQ_START), _version(0), _method(HTTP_ANY), + _url(), _host(), _contentType(), _boundary(), _authorization(), _reqconntype(RCT_HTTP), _authMethod(AsyncAuthType::AUTH_NONE), _isMultipart(false), + _isPlainPost(false), _expectingContinue(false), _contentLength(0), _parsedLength(0), _multiParseState(0), _boundaryPosition(0), _itemStartIndex(0), + _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false), _tempObject(NULL) { c->onError( [](void *r, AsyncClient *c, int8_t error) { (void)c; @@ -341,10 +341,10 @@ bool AsyncWebServerRequest::_parseReqHead() { } bool AsyncWebServerRequest::_parseReqHeader() { - int index = _temp.indexOf(':'); - if (index) { - String name(_temp.substring(0, index)); - String value(_temp.substring(index + 2)); + AsyncWebHeader header = AsyncWebHeader::parse(_temp); + if (header) { + const String &name = header.name(); + const String &value = header.value(); if (name.equalsIgnoreCase(T_Host)) { _host = value; } else if (name.equalsIgnoreCase(T_Content_Type)) { @@ -392,9 +392,9 @@ bool AsyncWebServerRequest::_parseReqHeader() { _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-() _temp = emptyString; #else @@ -419,7 +419,7 @@ void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) { _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-() _temp = emptyString; #else diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponseImpl.h b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponseImpl.h index 80dbca8f..64086258 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponseImpl.h +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponseImpl.h @@ -10,7 +10,7 @@ #undef max #endif #include "literals.h" -#include +#include #include #include @@ -157,7 +157,7 @@ public: class AsyncResponseStream : public AsyncAbstractResponse, public Print { private: - StreamString _content; + std::unique_ptr _content; public: 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 write(const uint8_t *data, size_t len); 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; }; diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponses.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponses.cpp index f5d46530..3b3fb6ce 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponses.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebResponses.cpp @@ -134,6 +134,30 @@ bool AsyncWebServerResponse::headerMustBePresentOnce(const String &name) { 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) { for (auto i = _headers.begin(); i != _headers.end(); ++i) { if (i->name().equalsIgnoreCase(name)) { @@ -595,6 +619,16 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size * 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) { #if HAVE_EXTERN_GET_Content_Type_FUNCTION #ifndef ESP8266 @@ -604,41 +638,47 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) { #endif _contentType = getContentType(path); #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; - } else if (path.endsWith(T__htm)) { - _contentType = T_text_html; - } else if (path.endsWith(T__css)) { + } else if (strcmp(dot, T__css) == 0) { _contentType = T_text_css; - } else if (path.endsWith(T__json)) { - _contentType = T_application_json; - } else if (path.endsWith(T__js)) { + } else if (strcmp(dot, T__js) == 0) { _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; - } else if (path.endsWith(T__gif)) { - _contentType = T_image_gif; - } else if (path.endsWith(T__jpg)) { - _contentType = T_image_jpeg; - } else if (path.endsWith(T__ico)) { + } else if (strcmp(dot, T__ico) == 0) { _contentType = T_image_x_icon; - } else if (path.endsWith(T__svg)) { + } else if (strcmp(dot, T__svg) == 0) { _contentType = T_image_svg_xml; - } else if (path.endsWith(T__eot)) { - _contentType = T_font_eot; - } else if (path.endsWith(T__woff)) { - _contentType = T_font_woff; - } else if (path.endsWith(T__woff2)) { + } else if (strcmp(dot, T__jpg) == 0) { + _contentType = T_image_jpeg; + } else if (strcmp(dot, T__gif) == 0) { + _contentType = T_image_gif; + } else if (strcmp(dot, T__woff2) == 0) { _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; - } 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; - } else if (path.endsWith(T__pdf)) { + } else if (strcmp(dot, T__pdf) == 0) { _contentType = T_application_pdf; - } else if (path.endsWith(T__zip)) { + } else if (strcmp(dot, T__zip) == 0) { _contentType = T_application_zip; - } else if (path.endsWith(T__gz)) { + } else if (strcmp(dot, T__gz) == 0) { _contentType = T_application_x_gzip; } else { _contentType = T_text_plain; @@ -646,40 +686,73 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) { #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) : AsyncAbstractResponse(callback) { - _code = 200; - _path = path; + // Try to open the uncompressed version first + _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)) { - _path = _path + T__gz; - addHeader(T_Content_Encoding, T_gzip, false); - _callback = nullptr; // Unable to process zipped templates - _sendContentLength = true; - _chunked = false; + if (_content.seek(_contentLength - 8)) { + addHeader(T_Content_Encoding, T_gzip, false); + _callback = nullptr; // Unable to process zipped templates + _sendContentLength = true; + _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); - _contentLength = _content.size(); - - if (strlen(contentType) == 0) { + if (*contentType != '\0') { _setContentTypeFromPath(path); } else { _contentType = contentType; } - int filenameStart = path.lastIndexOf('/') + 1; - char buf[26 + path.length() - filenameStart]; - char *filename = (char *)path.c_str() + filenameStart; - 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); + addHeader(T_Content_Disposition, buf, false); } else { - // set filename and force rendering - snprintf_P(buf, sizeof(buf), PSTR("inline")); + // Serve file inline (display in browser) + 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) @@ -820,7 +893,9 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS _code = 200; _contentLength = 0; _contentType = contentType; - if (!_content.reserve(bufferSize)) { + // internal buffer will be null on allocation failure + _content = std::unique_ptr(new cbuf(bufferSize)); + if (bufferSize && _content->size() < bufferSize) { #ifdef ESP32 log_e("Failed to allocate"); #endif @@ -828,14 +903,26 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS } 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) { if (_started()) { 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; return written; } diff --git a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebServer.cpp b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebServer.cpp index 7fc54bf1..c3c3ee73 100644 --- a/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebServer.cpp +++ b/Software/src/lib/ESP32Async-ESPAsyncWebServer/src/WebServer.cpp @@ -4,10 +4,18 @@ #include "ESPAsyncWebServer.h" #include "WebHandlerImpl.h" +#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) +#include +#elif defined(ESP8266) +#include +#else +#error Platform not supported +#endif + using namespace asyncsrv; 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(); #else return false; @@ -15,7 +23,7 @@ bool ON_STA_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(); #else return false; diff --git a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h index 47a6f2d4..96d02c96 100644 --- a/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h +++ b/Software/src/lib/mathieucarbou-AsyncTCPSock/src/AsyncTCP.h @@ -300,7 +300,7 @@ class AsyncServer : public AsyncSocketBase void setNoDelay(bool nodelay) { _noDelay = nodelay; } bool getNoDelay() { return _noDelay; } - uint8_t status(); + uint8_t status() const; protected: uint16_t _port;