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
#include "WebHandlerImpl.h"
6
#include "AsyncWebServerLogging.h"
7
 
8
#include <cstdio>
9
#include <utility>
10
 
11
using namespace asyncsrv;
12
 
13
AsyncWebHandler &AsyncWebHandler::setFilter(ArRequestFilterFunction fn) {
14
  _filter = fn;
15
  return *this;
16
}
17
AsyncWebHandler &AsyncWebHandler::setAuthentication(const char *username, const char *password, AsyncAuthType authMethod) {
18
  if (!_authMiddleware) {
19
    _authMiddleware = new AsyncAuthenticationMiddleware();
20
    _authMiddleware->_freeOnRemoval = true;
21
    addMiddleware(_authMiddleware);
22
  }
23
  _authMiddleware->setUsername(username);
24
  _authMiddleware->setPassword(password);
25
  _authMiddleware->setAuthType(authMethod);
26
  return *this;
27
};
28
 
29
AsyncStaticWebHandler::AsyncStaticWebHandler(const char *uri, FS &fs, const char *path, const char *cache_control)
30
  : _fs(fs), _uri(uri), _path(path), _default_file(F("index.htm")), _cache_control(cache_control), _last_modified(), _callback(nullptr) {
31
  // Ensure leading '/'
32
  if (_uri.length() == 0 || _uri[0] != '/') {
33
    _uri = String('/') + _uri;
34
  }
35
  if (_path.length() == 0 || _path[0] != '/') {
36
    _path = String('/') + _path;
37
  }
38
 
39
  // If path ends with '/' we assume a hint that this is a directory to improve performance.
40
  // However - if it does not end with '/' we, can't assume a file, path can still be a directory.
41
  _isDir = _path[_path.length() - 1] == '/';
42
 
43
  // Remove the trailing '/' so we can handle default file
44
  // Notice that root will be "" not "/"
45
  if (_uri[_uri.length() - 1] == '/') {
46
    _uri = _uri.substring(0, _uri.length() - 1);
47
  }
48
  if (_path[_path.length() - 1] == '/') {
49
    _path = _path.substring(0, _path.length() - 1);
50
  }
51
}
52
 
53
AsyncStaticWebHandler &AsyncStaticWebHandler::setTryGzipFirst(bool value) {
54
  _tryGzipFirst = value;
55
  return *this;
56
}
57
 
58
AsyncStaticWebHandler &AsyncStaticWebHandler::setIsDir(bool isDir) {
59
  _isDir = isDir;
60
  return *this;
61
}
62
 
63
AsyncStaticWebHandler &AsyncStaticWebHandler::setDefaultFile(const char *filename) {
64
  _default_file = filename;
65
  return *this;
66
}
67
 
68
AsyncStaticWebHandler &AsyncStaticWebHandler::setCacheControl(const char *cache_control) {
69
  _cache_control = cache_control;
70
  return *this;
71
}
72
 
73
AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(const char *last_modified) {
74
  _last_modified = last_modified;
75
  return *this;
76
}
77
 
78
AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(struct tm *last_modified) {
79
  char result[30];
80
#ifdef ESP8266
81
  auto formatP = PSTR("%a, %d %b %Y %H:%M:%S GMT");
82
  char format[strlen_P(formatP) + 1];  // NOLINT(runtime/arrays)
83
  strcpy_P(format, formatP);
84
#else
85
  static constexpr const char *format = "%a, %d %b %Y %H:%M:%S GMT";
86
#endif
87
 
88
  strftime(result, sizeof(result), format, last_modified);
89
  _last_modified = result;
90
  return *this;
91
}
92
 
93
AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(time_t last_modified) {
94
  return setLastModified((struct tm *)gmtime(&last_modified));
95
}
96
 
97
AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified() {
98
  time_t last_modified;
99
  if (time(&last_modified) == 0) {  // time is not yet set
100
    return *this;
101
  }
102
  return setLastModified(last_modified);
103
}
104
 
105
bool AsyncStaticWebHandler::canHandle(AsyncWebServerRequest *request) const {
106
  return request->isHTTP() && request->method() == AsyncWebRequestMethod::HTTP_GET && request->url().startsWith(_uri) && _getFile(request);
107
}
108
 
109
bool AsyncStaticWebHandler::_getFile(AsyncWebServerRequest *request) const {
110
  // Remove the found uri
111
  String path = request->url().substring(_uri.length());
112
 
113
  // We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/'
114
  bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length() - 1] == '/');
115
 
116
  path = _path + path;
117
 
118
  // Do we have a file or .gz file
119
  if (!canSkipFileCheck && const_cast<AsyncStaticWebHandler *>(this)->_searchFile(request, path)) {
120
    return true;
121
  }
122
 
123
  // Can't handle if not default file
124
  if (_default_file.length() == 0) {
125
    return false;
126
  }
127
 
128
  // Try to add default file, ensure there is a trailing '/' to the path.
129
  if (path.length() == 0 || path[path.length() - 1] != '/') {
130
    path += String('/');
131
  }
132
  path += _default_file;
133
 
134
  return const_cast<AsyncStaticWebHandler *>(this)->_searchFile(request, path);
135
}
136
 
137
#ifdef ESP32
138
#define FILE_IS_REAL(f) (f == true && !f.isDirectory())
139
#else
140
#define FILE_IS_REAL(f) (f == true)
141
#endif
142
 
143
bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const String &path) {
144
  bool fileFound = false;
145
  bool gzipFound = false;
146
 
147
  String gzip = path + T__gz;
148
 
149
  if (_tryGzipFirst) {
150
    if (_fs.exists(gzip)) {
151
      request->_tempFile = _fs.open(gzip, fs::FileOpenMode::read);
152
      gzipFound = FILE_IS_REAL(request->_tempFile);
153
    }
154
    if (!gzipFound) {
155
      if (_fs.exists(path)) {
156
        request->_tempFile = _fs.open(path, fs::FileOpenMode::read);
157
        fileFound = FILE_IS_REAL(request->_tempFile);
158
      }
159
    }
160
  } else {
161
    if (_fs.exists(path)) {
162
      request->_tempFile = _fs.open(path, fs::FileOpenMode::read);
163
      fileFound = FILE_IS_REAL(request->_tempFile);
164
    }
165
    if (!fileFound) {
166
      if (_fs.exists(gzip)) {
167
        request->_tempFile = _fs.open(gzip, fs::FileOpenMode::read);
168
        gzipFound = FILE_IS_REAL(request->_tempFile);
169
      }
170
    }
171
  }
172
 
173
  bool found = fileFound || gzipFound;
174
 
175
  if (found) {
176
    // Extract the file name from the path and keep it in _tempObject
177
    size_t pathLen = path.length();
178
    char *_tempPath = (char *)malloc(pathLen + 1);
179
    if (_tempPath == NULL) {
180
      async_ws_log_e("Failed to allocate");
181
      request->abort();
182
      request->_tempFile.close();
183
      return false;
184
    }
185
    snprintf_P(_tempPath, pathLen + 1, PSTR("%s"), path.c_str());
186
    request->_tempObject = (void *)_tempPath;
187
  }
188
 
189
  return found;
190
}
191
 
192
/**
193
 * @brief Handles an incoming HTTP request for a static file.
194
 *
195
 * This method processes a request for serving static files asynchronously.
196
 * It determines the correct ETag (entity tag) for caching, checks if the file
197
 * has been modified, and prepares the appropriate response (file response or 304 Not Modified).
198
 *
199
 * @param request Pointer to the incoming AsyncWebServerRequest object.
200
 */
201
void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
202
  // Get the filename from request->_tempObject and free it
203
  String filename((char *)request->_tempObject);
204
  free(request->_tempObject);
205
  request->_tempObject = nullptr;
206
 
207
  if (request->_tempFile != true) {
208
    request->send(404);
209
    return;
210
  }
211
 
212
  // Get server ETag. If file is not GZ and we have a Template Processor, ETag is set to an empty string
213
  char etag[11];
214
  const char *tempFileName = request->_tempFile.name();
215
  const size_t lenFilename = strlen(tempFileName);
216
 
217
  if (lenFilename > T__GZ_LEN && memcmp(tempFileName + lenFilename - T__GZ_LEN, T__gz, T__GZ_LEN) == 0) {
218
    //File is a gz, get etag from CRC in trailer
219
    if (!AsyncWebServerRequest::_getEtag(request->_tempFile, etag)) {
220
      // File is corrupted or invalid
221
      async_ws_log_w("File is corrupted or invalid: %s", tempFileName);
222
      request->send(404);
223
      return;
224
    }
225
 
226
    // Reset file position to the beginning so the file can be served from the start.
227
    request->_tempFile.seek(0);
228
  } else if (_callback == nullptr) {
229
    // We don't have a Template processor
230
    uint32_t etagValue;
231
    time_t lastWrite = request->_tempFile.getLastWrite();
232
    if (lastWrite > 0) {
233
      // Use timestamp-based ETag
234
      etagValue = static_cast<uint32_t>(lastWrite);
235
    } else {
236
      // No timestamp available, use filesize-based ETag
237
      size_t fileSize = request->_tempFile.size();
238
      etagValue = static_cast<uint32_t>(fileSize);
239
    }
240
    // RFC9110 Section-8.8.3: Value of the ETag response must be enclosed in double quotes
241
    snprintf(etag, sizeof(etag), "\"%08" PRIx32 "\"", etagValue);
242
  } else {
243
    etag[0] = '\0';
244
  }
245
 
246
  AsyncWebServerResponse *response;
247
 
248
  bool notModified = false;
249
  // 1. If the client sent If-None-Match and we have an ETag → compare
250
  if (*etag != '\0' && request->header(T_INM) == etag) {
251
    notModified = true;
252
  }
253
  // 2. Otherwise, if there is no ETag but we have Last-Modified and Last-Modified matches
254
  else if (*etag == '\0' && _last_modified.length() > 0 && request->header(T_IMS) == _last_modified) {
255
    async_ws_log_d("_last_modified: %s", _last_modified.c_str());
256
    notModified = true;
257
  }
258
 
259
  if (notModified) {
260
    request->_tempFile.close();
261
    response = new AsyncBasicResponse(304);  // Not modified
262
  } else {
263
    response = new AsyncFileResponse(request->_tempFile, filename, asyncsrv::emptyString, false, _callback);
264
  }
265
 
266
  if (!response) {
267
    async_ws_log_e("Failed to allocate");
268
    request->abort();
269
    return;
270
  }
271
 
272
  if (!notModified) {
273
    // Set ETag header
274
    if (*etag != '\0') {
275
      response->addHeader(T_ETag, etag, true);
276
    }
277
    // Set Last-Modified header
278
    if (_last_modified.length()) {
279
      response->addHeader(T_Last_Modified, _last_modified.c_str(), true);
280
    }
281
  }
282
 
283
  // Set cache control
284
  if (_cache_control.length()) {
285
    response->addHeader(T_Cache_Control, _cache_control.c_str(), false);
286
  } else {
287
    response->addHeader(T_Cache_Control, T_no_cache, false);
288
  }
289
 
290
  request->send(response);
291
}
292
 
293
AsyncStaticWebHandler &AsyncStaticWebHandler::setTemplateProcessor(AwsTemplateProcessor newCallback) {
294
  _callback = newCallback;
295
  return *this;
296
}
297
 
298
void AsyncCallbackWebHandler::setUri(AsyncURIMatcher uri) {
299
  _uri = std::move(uri);
300
}
301
 
302
bool AsyncCallbackWebHandler::canHandle(AsyncWebServerRequest *request) const {
303
  if (!_onRequest || !request->isHTTP() || !_method.matches(request->method())) {
304
    return false;
305
  }
306
  return _uri.matches(request);
307
}
308
 
309
void AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest *request) {
310
  if (_onRequest) {
311
    _onRequest(request);
312
  } else {
313
    request->send(404, T_text_plain, "Not found");
314
  }
315
}
316
void AsyncCallbackWebHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) {
317
  if (_onUpload) {
318
    _onUpload(request, filename, index, data, len, final);
319
  }
320
}
321
void AsyncCallbackWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
322
  // ESP_LOGD("AsyncWebServer", "AsyncCallbackWebHandler::handleBody");
323
  if (_onBody) {
324
    _onBody(request, data, len, index, total);
325
  }
326
}