| 2 |
raymond |
1 |
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
2 |
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
|
|
3 |
|
|
|
4 |
#include <ESPAsyncWebServer.h>
|
|
|
5 |
|
|
|
6 |
/**
|
|
|
7 |
* @brief Sends a file from the filesystem to the client, with optional gzip compression and ETag-based caching.
|
|
|
8 |
*
|
|
|
9 |
* This method serves files over HTTP from the provided filesystem. If a compressed version of the file
|
|
|
10 |
* (with a `.gz` extension) exists and uncompressed version does not exist, it serves the compressed file.
|
|
|
11 |
* It also handles ETag caching using the CRC32 value from the gzip trailer, responding with `304 Not Modified`
|
|
|
12 |
* if the client's `If-None-Match` header matches the generated ETag.
|
|
|
13 |
*
|
|
|
14 |
* @param fs Reference to the filesystem (SPIFFS, LittleFS, etc.).
|
|
|
15 |
* @param path Path to the file to be served.
|
|
|
16 |
* @param contentType Optional MIME type of the file to be sent.
|
|
|
17 |
* If contentType is "" it will be obtained from the file extension
|
|
|
18 |
* @param download If true, forces the file to be sent as a download.
|
|
|
19 |
* @param callback Optional template processor for dynamic content generation.
|
|
|
20 |
* Templates will not be processed in compressed files.
|
|
|
21 |
*
|
|
|
22 |
* @note If neither the file nor its compressed version exists, responds with `404 Not Found`.
|
|
|
23 |
*/
|
|
|
24 |
void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) {
|
|
|
25 |
// Check uncompressed file first
|
|
|
26 |
if (fs.exists(path)) {
|
|
|
27 |
send(beginResponse(fs, path, contentType, download, callback));
|
|
|
28 |
return;
|
|
|
29 |
}
|
|
|
30 |
|
|
|
31 |
// Handle compressed version
|
|
|
32 |
const String gzPath = path + asyncsrv::T__gz;
|
|
|
33 |
File gzFile = fs.open(gzPath, fs::FileOpenMode::read);
|
|
|
34 |
|
|
|
35 |
// ETag validation
|
|
|
36 |
if (this->hasHeader(asyncsrv::T_INM)) {
|
|
|
37 |
// Generate server ETag from CRC in gzip trailer
|
|
|
38 |
char serverETag[11];
|
|
|
39 |
if (!_getEtag(gzFile, serverETag)) {
|
|
|
40 |
// Compressed file not found or invalid
|
|
|
41 |
send(404);
|
|
|
42 |
gzFile.close();
|
|
|
43 |
return;
|
|
|
44 |
}
|
|
|
45 |
|
|
|
46 |
// Compare with client's ETag
|
|
|
47 |
const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM);
|
|
|
48 |
if (inmHeader && inmHeader->value() == serverETag) {
|
|
|
49 |
gzFile.close();
|
|
|
50 |
this->send(304); // Not Modified
|
|
|
51 |
return;
|
|
|
52 |
}
|
|
|
53 |
}
|
|
|
54 |
|
|
|
55 |
// Send compressed file response
|
|
|
56 |
gzFile.close();
|
|
|
57 |
send(beginResponse(fs, path, contentType, download, callback));
|
|
|
58 |
}
|
|
|
59 |
|
|
|
60 |
/**
|
|
|
61 |
* @brief Generates an ETag string (enclosed into quotes) from the CRC32 trailer of a GZIP file.
|
|
|
62 |
*
|
|
|
63 |
* This function reads the CRC32 checksum (4 bytes) located at the end of a GZIP-compressed file
|
|
|
64 |
* and converts it into an 8-character hexadecimal ETag string (enclosed in double quotes and null-terminated).
|
|
|
65 |
* Double quotes for ETag value are required by RFC9110 section 8.8.3.
|
|
|
66 |
*
|
|
|
67 |
* @param gzFile Opened file handle pointing to the GZIP file.
|
|
|
68 |
* @param eTag Output buffer to store the generated ETag.
|
|
|
69 |
* Must be pre-allocated with at least 11 bytes (8 for hex digits + 2 for quotes + 1 for null terminator).
|
|
|
70 |
*
|
|
|
71 |
* @return true if the ETag was successfully generated, false otherwise (e.g., file too short or seek failed).
|
|
|
72 |
*/
|
|
|
73 |
bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) {
|
|
|
74 |
static constexpr char hexChars[] = "0123456789ABCDEF";
|
|
|
75 |
|
|
|
76 |
if (!gzFile.seek(gzFile.size() - 8)) {
|
|
|
77 |
return false;
|
|
|
78 |
}
|
|
|
79 |
|
|
|
80 |
uint32_t crc;
|
|
|
81 |
gzFile.read(reinterpret_cast<uint8_t *>(&crc), sizeof(crc));
|
|
|
82 |
|
|
|
83 |
etag[0] = '"';
|
|
|
84 |
etag[1] = hexChars[(crc >> 4) & 0x0F];
|
|
|
85 |
etag[2] = hexChars[crc & 0x0F];
|
|
|
86 |
etag[3] = hexChars[(crc >> 12) & 0x0F];
|
|
|
87 |
etag[4] = hexChars[(crc >> 8) & 0x0F];
|
|
|
88 |
etag[5] = hexChars[(crc >> 20) & 0x0F];
|
|
|
89 |
etag[6] = hexChars[(crc >> 16) & 0x0F];
|
|
|
90 |
etag[7] = hexChars[(crc >> 28)];
|
|
|
91 |
etag[8] = hexChars[(crc >> 24) & 0x0F];
|
|
|
92 |
etag[9] = '"';
|
|
|
93 |
etag[10] = '\0';
|
|
|
94 |
|
|
|
95 |
return true;
|
|
|
96 |
}
|