From 96afd0652a75829da762b9c900e8ade34e2761f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20M=2E=20Pi=C3=B1eiro?= Date: Wed, 20 Aug 2025 21:45:42 +0200 Subject: [PATCH 1/2] Refactor _getEtag to get etag from gzip instead of buffer This PR updates the _getEtag implementation in AsyncWebServerRequest to improve how ETags are generated. Previously, the method relied on a 4-byte trailer buffer passed as a parameter. This has been replaced with a more robust approach: the function now reads the CRC directly from the last 8 bytes of the gzip file. This ensures that the ETag is derived from the actual file contents rather than depending on an external buffer. Key changes: _getEtag now accepts a File handle instead of a trailer array. The method seeks to the gzip footer (size - 8) and extracts the CRC. Returns bool to indicate success/failure. ETag generation logic remains the same, ensuring backward compatibility for clients relying on ETags. This change improves correctness, reduces external dependencies, and makes ETag generation more consistent with the gzip specification. --- src/AsyncWebServerRequest.cpp | 58 +++++++++++++++++++---------------- src/ESPAsyncWebServer.h | 2 +- src/WebResponses.cpp | 16 ++++------ 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/AsyncWebServerRequest.cpp b/src/AsyncWebServerRequest.cpp index 18b4da05..24d83012 100644 --- a/src/AsyncWebServerRequest.cpp +++ b/src/AsyncWebServerRequest.cpp @@ -29,20 +29,16 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content const String gzPath = path + asyncsrv::T__gz; File gzFile = fs.open(gzPath, fs::FileOpenMode::read); - // 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); + if (!_getEtag(gzFile, serverETag)) { + // Compressed file not found or invalid + send(404); + gzFile.close(); + return; + } // Compare with client's ETag const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM); @@ -59,27 +55,35 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content } /** - * @brief Generates an ETag string from a 4-byte trailer + * @brief Generates an ETag string from the CRC32 trailer of a GZIP file. + * + * This function reads the CRC32 checksum (4 bytes) located at the end of a GZIP-compressed file + * and converts it into an 8-character hexadecimal ETag string (null-terminated). * - * This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes. + * @param gzFile Opened file handle pointing to the GZIP file. + * @param eTag Output buffer to store the generated ETag. + * Must be pre-allocated with at least 9 bytes (8 for hex digits + 1 for null terminator). * - * @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) + * @return true if the ETag was successfully generated, false otherwise (e.g., file too short or seek failed). */ -void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) { +bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) { static constexpr char hexChars[] = "0123456789ABCDEF"; - uint32_t data; - memcpy(&data, trailer, 4); + if (!gzFile.seek(gzFile.size() - 8)) + return false; + + uint32_t crc; + gzFile.read(reinterpret_cast(&crc), sizeof(crc)); + + etag[0] = hexChars[(crc >> 4) & 0x0F]; + etag[1] = hexChars[crc & 0x0F]; + etag[2] = hexChars[(crc >> 12) & 0x0F]; + etag[3] = hexChars[(crc >> 8) & 0x0F]; + etag[4] = hexChars[(crc >> 20) & 0x0F]; + etag[5] = hexChars[(crc >> 16) & 0x0F]; + etag[6] = hexChars[(crc >> 28)]; + etag[7] = hexChars[(crc >> 24) & 0x0F]; + etag[8] = '\0'; - 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'; + return true; } diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 0cf13238..7d5eea17 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -275,7 +275,7 @@ class AsyncWebServerRequest { void _send(); void _runMiddlewareChain(); - static void _getEtag(uint8_t trailer[4], char *serverETag); + static bool _getEtag(File gzFile, char *eTag); public: File _tempFile; diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 2878a2c1..f5c4eef0 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -699,29 +699,23 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con // Try to open the uncompressed version first _content = fs.open(path, fs::FileOpenMode::read); - if (_content.available()) { - _contentLength = _content.size(); - } else { - // Try to open the compressed version (.gz) + if (!_content.available()) { + // If not available try to open the compressed version (.gz) String gzPath; uint16_t pathLen = path.length(); gzPath.reserve(pathLen + 3); gzPath.concat(path); gzPath.concat(asyncsrv::T__gz); _content = fs.open(gzPath, fs::FileOpenMode::read); - _contentLength = _content.size(); - if (_content.seek(_contentLength - 8)) { + char serverETag[9]; + if (AsyncWebServerRequest::_getEtag(_content, serverETag)) { 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); @@ -733,6 +727,8 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con } } + _contentLength = _content.size(); + if (*contentType == '\0') { _setContentTypeFromPath(path); } else { From 66a369fb9932aa8b9723d25b353fc8d855eb6712 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:04:11 +0000 Subject: [PATCH 2/2] ci(pre-commit): Apply automatic fixes --- src/AsyncWebServerRequest.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/AsyncWebServerRequest.cpp b/src/AsyncWebServerRequest.cpp index 24d83012..b2be0e8b 100644 --- a/src/AsyncWebServerRequest.cpp +++ b/src/AsyncWebServerRequest.cpp @@ -69,11 +69,12 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) { static constexpr char hexChars[] = "0123456789ABCDEF"; - if (!gzFile.seek(gzFile.size() - 8)) + if (!gzFile.seek(gzFile.size() - 8)) { return false; + } uint32_t crc; - gzFile.read(reinterpret_cast(&crc), sizeof(crc)); + gzFile.read(reinterpret_cast(&crc), sizeof(crc)); etag[0] = hexChars[(crc >> 4) & 0x0F]; etag[1] = hexChars[crc & 0x0F];