diff --git a/src/AsyncWebServerRequest.cpp b/src/AsyncWebServerRequest.cpp new file mode 100644 index 00000000..9c87fed0 --- /dev/null +++ b/src/AsyncWebServerRequest.cpp @@ -0,0 +1,65 @@ + #include + + void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) { + const String gzPath = path + asyncsrv::T__gz; + const bool useCompressedVersion = !download && fs.exists(gzPath); + + // If-None-Match header + if (useCompressedVersion && this->hasHeader(asyncsrv::T_INM)) { + // CRC32-based ETag of the trailer, bytes 4-7 from the end + File file = fs.open(gzPath, fs::FileOpenMode::read); + if (file && file.size() >= 18) { // 18 is the minimum size of valid gzip file + file.seek(file.size() - 8); + + uint8_t crcFromGzipTrailer[4]; + if (file.read(crcFromGzipTrailer, sizeof(crcFromGzipTrailer)) == sizeof(crcFromGzipTrailer)) { + char serverETag[11]; // " + 8 hex chars + " + '\0' + _getEtag(crcFromGzipTrailer, serverETag); + + // Compare with client's If-None-Match header + const AsyncWebHeader* inmHeader = this->getHeader(asyncsrv::T_INM); + if (inmHeader && inmHeader->value().equals(serverETag)) { + file.close(); + this->send(304); // Not Modified + return; + } + } + file.close(); + } + } + + // If we get here, create and send the normal response + if (fs.exists(path) || useCompressedVersion) { + send(beginResponse(fs, path, contentType, download, callback)); + } else { + send(404); + } + } + +/** + * @brief Generates an ETag string from a 4-byte trailer using basic loop implementation + * + * 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 that must be at least 11 bytes long to store the ETag + * Must be pre-allocated with minimum 11 bytes (8 hex + 2 quotes + 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] = '"'; + serverETag[1] = hexChars[(data >> 4) & 0x0F]; + serverETag[2] = hexChars[data & 0x0F]; + serverETag[3] = hexChars[(data >> 12) & 0x0F]; + serverETag[4] = hexChars[(data >> 8) & 0x0F]; + serverETag[5] = hexChars[(data >> 20) & 0x0F]; + serverETag[6] = hexChars[(data >> 16) & 0x0F]; + serverETag[7] = hexChars[(data >> 28)]; + serverETag[8] = hexChars[(data >> 24) & 0x0F]; + serverETag[9] = '"'; + serverETag[10] = '\0'; +} \ No newline at end of file diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index ae45d131..800a4df7 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -334,6 +334,8 @@ class AsyncWebServerRequest { void addInterestingHeader(__unused const String &name) { } + static void _getEtag(uint8_t trailer[4], char* serverETag); + /** * @brief issue HTTP redirect response with Location header * @@ -367,13 +369,7 @@ class AsyncWebServerRequest { 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); } diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 3de8f329..74fbf846 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -649,22 +649,41 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) { AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) : AsyncAbstractResponse(callback) { _code = 200; - _path = path; + const String gzPath = path + asyncsrv::T__gz; - if (!download && !fs.exists(_path) && fs.exists(_path + T__gz)) { - _path = _path + T__gz; + if (!download && !fs.exists(path) && fs.exists(gzPath)) { + _path = gzPath; + _content = fs.open(gzPath, fs::FileOpenMode::read); + _contentLength = _content.size(); addHeader(T_Content_Encoding, T_gzip, false); - _callback = nullptr; // Unable to process zipped templates + _callback = nullptr; // Unable to process zipped templates _sendContentLength = true; _chunked = false; + + // CRC32-based ETag of the trailer, bytes 4-7 from the end + _content.seek(_contentLength - 8); + uint8_t crcInTrailer[4]; + if (_content.read(crcInTrailer, sizeof(crcInTrailer)) == sizeof(crcInTrailer)) { + char serverETag[11]; + AsyncWebServerRequest::_getEtag(crcInTrailer, serverETag); + addHeader(T_ETag, serverETag, false); + addHeader(T_Cache_Control, T_no_cache, false); + } + + // Return to the beginning of the file + _content.seek(0); } - _content = fs.open(_path, fs::FileOpenMode::read); - _contentLength = _content.size(); + if (!_content) { + _path = path; + _content = fs.open(path, fs::FileOpenMode::read); + _contentLength = _content.size(); + } - if (strlen(contentType) == 0) { + if (*contentType != '\0') { _setContentTypeFromPath(path); - } else { + } + else { _contentType = contentType; } @@ -675,7 +694,8 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con if (download) { // set filename and force download snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename); - } else { + } + else { // set filename and force rendering snprintf_P(buf, sizeof(buf), PSTR("inline")); }