Subversion Repositories ESP8266_P1_Meter

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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
}