Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

607 lines
18 KiB

8 years ago
8 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
8 years ago
5 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
8 years ago
6 years ago
6 years ago
6 years ago
Rework settings (#2282) * wip based on early draft. todo benchmarking * fixup eraser, assume keys are unique * fix cursor copy, test removal at random * small benchmark via permutations. todo lambdas and novirtual * fix empty condition / reset * overwrite optimizations, fix move offsets overflows * ...erase using 0xff instead of 0 * test the theory with code, different length kv were bugged * try to check for out-of-bounds writes / reads * style * trying to fix mover again * clarify length, defend against reading len on edge * fix uncommited rewind change * prove space assumptions * more concise traces, fix move condition (agrh!!!) * slightly more internal knowledge (estimates API?) * make sure cursor is only valid within the range * ensure 0 does not blow things * go back up * cursor comments * comments * rewrite writes through cursor * in del too * estimate kv storage requirements, return available size * move raw erase / move into a method, allow ::set to avoid scanning storage twice * refactor naming, use in code * amend storage slicing test * fix crash handler offsets, cleanup configuration * start -> begin * eeprom readiness * dependencies * unused * SPI_FLASH constants for older Core * vtables -> templates * less include dependencies * gcov help, move estimate outside of the class * writer position can never match, use begin + offset * tweak save_crash to trigger only once in a serious crash * doh, header function should be inline * foreach api, tweak structs for public api * use test helper class * when not using foreach, move cursor reset closer to the loop using read_kv * coverage comments, fix typo in tests decltype * ensure set() does not break with offset * make codacy happy again
4 years ago
api: rework plain and JSON implementations (#2405) - match paths through a custom AsyncWebHandler instead of using generic not-found fallback handler - allow MQTT-like patterns when registering paths (`simple/path`, `path/+/something`, `path/#`) Replaces `relay/0`, `relay/1` etc. with `relay/+`. Magnitudes are plain paths, but using `/+` in case there's more than 1 magnitude of the same type. - restore `std::function` as callback container (no more single-byte arg nonsense). Still, limit to 1 type per handler type - adds JSON handlers which will receive JsonObject root as both input and output. Same logic as plain - GET returns resource data, PUT updates it. - breaking change to `apiAuthenticate(request)`, it no longer will do `request->send(403)` and expect this to be handled externally. - allow `Api-Key` header containing the key, works for both GET & PUT plain requests. The only way to set apikey for JSON. - add `ApiRequest::param` to retrieve both GET and PUT params (aka args), remove ApiBuffer - remove `API_BUFFER_SIZE`. Allow custom form-data key=value pairs for requests, allow to send basic `String`. - add `API_JSON_BUFFER_SIZE` for the JSON buffer (both input and output) - `/apis` replaced with `/api/list`, no longer uses custom handler and is an `apiRegister` callback - `/api/rpc` custom handler replaced with an `apiRegister` callback WIP further down: - no more `webLog` for API requests, unless `webAccessLog` / `WEB_ACCESS_LOG` is set to `1`. This also needs to happen to the other handlers. - migrate to ArduinoJson v6, since it become apparent it is actually a good upgrade :) - actually make use of JSON endpoints more, right now it's just existing GET for sensors and relays - fork ESPAsyncWebServer to cleanup path parsing and temporary objects attached to the request (also, fix things a lot of things based on PRs there...)
4 years ago
6 years ago
6 years ago
api: rework plain and JSON implementations (#2405) - match paths through a custom AsyncWebHandler instead of using generic not-found fallback handler - allow MQTT-like patterns when registering paths (`simple/path`, `path/+/something`, `path/#`) Replaces `relay/0`, `relay/1` etc. with `relay/+`. Magnitudes are plain paths, but using `/+` in case there's more than 1 magnitude of the same type. - restore `std::function` as callback container (no more single-byte arg nonsense). Still, limit to 1 type per handler type - adds JSON handlers which will receive JsonObject root as both input and output. Same logic as plain - GET returns resource data, PUT updates it. - breaking change to `apiAuthenticate(request)`, it no longer will do `request->send(403)` and expect this to be handled externally. - allow `Api-Key` header containing the key, works for both GET & PUT plain requests. The only way to set apikey for JSON. - add `ApiRequest::param` to retrieve both GET and PUT params (aka args), remove ApiBuffer - remove `API_BUFFER_SIZE`. Allow custom form-data key=value pairs for requests, allow to send basic `String`. - add `API_JSON_BUFFER_SIZE` for the JSON buffer (both input and output) - `/apis` replaced with `/api/list`, no longer uses custom handler and is an `apiRegister` callback - `/api/rpc` custom handler replaced with an `apiRegister` callback WIP further down: - no more `webLog` for API requests, unless `webAccessLog` / `WEB_ACCESS_LOG` is set to `1`. This also needs to happen to the other handlers. - migrate to ArduinoJson v6, since it become apparent it is actually a good upgrade :) - actually make use of JSON endpoints more, right now it's just existing GET for sensors and relays - fork ESPAsyncWebServer to cleanup path parsing and temporary objects attached to the request (also, fix things a lot of things based on PRs there...)
4 years ago
api: rework plain and JSON implementations (#2405) - match paths through a custom AsyncWebHandler instead of using generic not-found fallback handler - allow MQTT-like patterns when registering paths (`simple/path`, `path/+/something`, `path/#`) Replaces `relay/0`, `relay/1` etc. with `relay/+`. Magnitudes are plain paths, but using `/+` in case there's more than 1 magnitude of the same type. - restore `std::function` as callback container (no more single-byte arg nonsense). Still, limit to 1 type per handler type - adds JSON handlers which will receive JsonObject root as both input and output. Same logic as plain - GET returns resource data, PUT updates it. - breaking change to `apiAuthenticate(request)`, it no longer will do `request->send(403)` and expect this to be handled externally. - allow `Api-Key` header containing the key, works for both GET & PUT plain requests. The only way to set apikey for JSON. - add `ApiRequest::param` to retrieve both GET and PUT params (aka args), remove ApiBuffer - remove `API_BUFFER_SIZE`. Allow custom form-data key=value pairs for requests, allow to send basic `String`. - add `API_JSON_BUFFER_SIZE` for the JSON buffer (both input and output) - `/apis` replaced with `/api/list`, no longer uses custom handler and is an `apiRegister` callback - `/api/rpc` custom handler replaced with an `apiRegister` callback WIP further down: - no more `webLog` for API requests, unless `webAccessLog` / `WEB_ACCESS_LOG` is set to `1`. This also needs to happen to the other handlers. - migrate to ArduinoJson v6, since it become apparent it is actually a good upgrade :) - actually make use of JSON endpoints more, right now it's just existing GET for sensors and relays - fork ESPAsyncWebServer to cleanup path parsing and temporary objects attached to the request (also, fix things a lot of things based on PRs there...)
4 years ago
api: rework plain and JSON implementations (#2405) - match paths through a custom AsyncWebHandler instead of using generic not-found fallback handler - allow MQTT-like patterns when registering paths (`simple/path`, `path/+/something`, `path/#`) Replaces `relay/0`, `relay/1` etc. with `relay/+`. Magnitudes are plain paths, but using `/+` in case there's more than 1 magnitude of the same type. - restore `std::function` as callback container (no more single-byte arg nonsense). Still, limit to 1 type per handler type - adds JSON handlers which will receive JsonObject root as both input and output. Same logic as plain - GET returns resource data, PUT updates it. - breaking change to `apiAuthenticate(request)`, it no longer will do `request->send(403)` and expect this to be handled externally. - allow `Api-Key` header containing the key, works for both GET & PUT plain requests. The only way to set apikey for JSON. - add `ApiRequest::param` to retrieve both GET and PUT params (aka args), remove ApiBuffer - remove `API_BUFFER_SIZE`. Allow custom form-data key=value pairs for requests, allow to send basic `String`. - add `API_JSON_BUFFER_SIZE` for the JSON buffer (both input and output) - `/apis` replaced with `/api/list`, no longer uses custom handler and is an `apiRegister` callback - `/api/rpc` custom handler replaced with an `apiRegister` callback WIP further down: - no more `webLog` for API requests, unless `webAccessLog` / `WEB_ACCESS_LOG` is set to `1`. This also needs to happen to the other handlers. - migrate to ArduinoJson v6, since it become apparent it is actually a good upgrade :) - actually make use of JSON endpoints more, right now it's just existing GET for sensors and relays - fork ESPAsyncWebServer to cleanup path parsing and temporary objects attached to the request (also, fix things a lot of things based on PRs there...)
4 years ago
6 years ago
api: rework plain and JSON implementations (#2405) - match paths through a custom AsyncWebHandler instead of using generic not-found fallback handler - allow MQTT-like patterns when registering paths (`simple/path`, `path/+/something`, `path/#`) Replaces `relay/0`, `relay/1` etc. with `relay/+`. Magnitudes are plain paths, but using `/+` in case there's more than 1 magnitude of the same type. - restore `std::function` as callback container (no more single-byte arg nonsense). Still, limit to 1 type per handler type - adds JSON handlers which will receive JsonObject root as both input and output. Same logic as plain - GET returns resource data, PUT updates it. - breaking change to `apiAuthenticate(request)`, it no longer will do `request->send(403)` and expect this to be handled externally. - allow `Api-Key` header containing the key, works for both GET & PUT plain requests. The only way to set apikey for JSON. - add `ApiRequest::param` to retrieve both GET and PUT params (aka args), remove ApiBuffer - remove `API_BUFFER_SIZE`. Allow custom form-data key=value pairs for requests, allow to send basic `String`. - add `API_JSON_BUFFER_SIZE` for the JSON buffer (both input and output) - `/apis` replaced with `/api/list`, no longer uses custom handler and is an `apiRegister` callback - `/api/rpc` custom handler replaced with an `apiRegister` callback WIP further down: - no more `webLog` for API requests, unless `webAccessLog` / `WEB_ACCESS_LOG` is set to `1`. This also needs to happen to the other handlers. - migrate to ArduinoJson v6, since it become apparent it is actually a good upgrade :) - actually make use of JSON endpoints more, right now it's just existing GET for sensors and relays - fork ESPAsyncWebServer to cleanup path parsing and temporary objects attached to the request (also, fix things a lot of things based on PRs there...)
4 years ago
api: rework plain and JSON implementations (#2405) - match paths through a custom AsyncWebHandler instead of using generic not-found fallback handler - allow MQTT-like patterns when registering paths (`simple/path`, `path/+/something`, `path/#`) Replaces `relay/0`, `relay/1` etc. with `relay/+`. Magnitudes are plain paths, but using `/+` in case there's more than 1 magnitude of the same type. - restore `std::function` as callback container (no more single-byte arg nonsense). Still, limit to 1 type per handler type - adds JSON handlers which will receive JsonObject root as both input and output. Same logic as plain - GET returns resource data, PUT updates it. - breaking change to `apiAuthenticate(request)`, it no longer will do `request->send(403)` and expect this to be handled externally. - allow `Api-Key` header containing the key, works for both GET & PUT plain requests. The only way to set apikey for JSON. - add `ApiRequest::param` to retrieve both GET and PUT params (aka args), remove ApiBuffer - remove `API_BUFFER_SIZE`. Allow custom form-data key=value pairs for requests, allow to send basic `String`. - add `API_JSON_BUFFER_SIZE` for the JSON buffer (both input and output) - `/apis` replaced with `/api/list`, no longer uses custom handler and is an `apiRegister` callback - `/api/rpc` custom handler replaced with an `apiRegister` callback WIP further down: - no more `webLog` for API requests, unless `webAccessLog` / `WEB_ACCESS_LOG` is set to `1`. This also needs to happen to the other handlers. - migrate to ArduinoJson v6, since it become apparent it is actually a good upgrade :) - actually make use of JSON endpoints more, right now it's just existing GET for sensors and relays - fork ESPAsyncWebServer to cleanup path parsing and temporary objects attached to the request (also, fix things a lot of things based on PRs there...)
4 years ago
6 years ago
6 years ago
6 years ago
8 years ago
  1. /*
  2. WEBSERVER MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #include "web.h"
  6. #if WEB_SUPPORT
  7. #include <algorithm>
  8. #include <functional>
  9. #include <memory>
  10. #include "system.h"
  11. #include "utils.h"
  12. #include "ntp.h"
  13. #include <Schedule.h>
  14. #include <Print.h>
  15. #include <Hash.h>
  16. #include <FS.h>
  17. #include <ArduinoJson.h>
  18. #include <ESPAsyncWebServer.h>
  19. #include <AsyncJson.h>
  20. #if WEB_EMBEDDED
  21. #if WEBUI_IMAGE == WEBUI_IMAGE_SMALL
  22. #include "static/index.small.html.gz.h"
  23. #elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHT
  24. #include "static/index.light.html.gz.h"
  25. #elif WEBUI_IMAGE == WEBUI_IMAGE_SENSOR
  26. #include "static/index.sensor.html.gz.h"
  27. #elif WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE
  28. #include "static/index.rfbridge.html.gz.h"
  29. #elif WEBUI_IMAGE == WEBUI_IMAGE_RFM69
  30. #include "static/index.rfm69.html.gz.h"
  31. #elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX
  32. #include "static/index.lightfox.html.gz.h"
  33. #elif WEBUI_IMAGE == WEBUI_IMAGE_GARLAND
  34. #include "static/index.garland.html.gz.h"
  35. #elif WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT
  36. #include "static/index.thermostat.html.gz.h"
  37. #elif WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN
  38. #include "static/index.curtain.html.gz.h"
  39. #elif WEBUI_IMAGE == WEBUI_IMAGE_FULL
  40. #include "static/index.all.html.gz.h"
  41. #endif
  42. #endif // WEB_EMBEDDED
  43. #if WEB_SSL_ENABLED
  44. #include "static/server.cer.h"
  45. #include "static/server.key.h"
  46. #endif // WEB_SSL_ENABLED
  47. AsyncWebPrint::AsyncWebPrint(const AsyncWebPrintConfig& config, AsyncWebServerRequest* request) :
  48. mimeType(config.mimeType),
  49. backlogCountMax(config.backlogCountMax),
  50. backlogSizeMax(config.backlogSizeMax),
  51. backlogTimeout(config.backlogTimeout),
  52. _request(request),
  53. _state(State::None)
  54. {}
  55. bool AsyncWebPrint::_addBuffer() {
  56. if ((_buffers.size() + 1) > backlogCountMax) {
  57. if (!_exhaustBuffers()) {
  58. _state = State::Error;
  59. return false;
  60. }
  61. }
  62. // Note: c++17, emplace returns created object reference
  63. // c++11, we need to use .back()
  64. _buffers.emplace_back(backlogSizeMax, 0);
  65. _buffers.back().clear();
  66. return true;
  67. }
  68. // Creates response object that will handle the data written into the Print& interface.
  69. //
  70. // This API expects a **very** careful approach to context switching between SYS and CONT:
  71. // - Returning RESPONSE_TRY_AGAIN before buffers are filled will result in invalid size marker being sent on the wire.
  72. // HTTP client (curl, python requests etc., as discovered in testing) will then drop the connection
  73. // - Returning 0 will immediatly close the connection from our side
  74. // - Calling _prepareRequest() **before** _buffers are filled will result in returning 0
  75. // - Calling yield() / delay() while request AsyncWebPrint is active **may** trigger this callback out of sequence
  76. // (e.g. Serial.print(..), DEBUG_MSG(...), or any other API trying to switch contexts)
  77. // - Receiving data (tcp ack from the previous packet) **will** trigger the callback when switching contexts.
  78. void AsyncWebPrint::_prepareRequest() {
  79. _state = State::Sending;
  80. auto *response = _request->beginChunkedResponse(mimeType, [this](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
  81. switch (_state) {
  82. case State::None:
  83. return RESPONSE_TRY_AGAIN;
  84. case State::Error:
  85. case State::Done:
  86. return 0;
  87. case State::Sending:
  88. break;
  89. }
  90. size_t written = 0;
  91. while ((written < maxLen) && !_buffers.empty()) {
  92. auto& chunk =_buffers.front();
  93. auto have = maxLen - written;
  94. if (chunk.size() > have) {
  95. std::copy(chunk.data(), chunk.data() + have, buffer + written);
  96. chunk.erase(chunk.begin(), chunk.begin() + have);
  97. written += have;
  98. } else {
  99. std::copy(chunk.data(), chunk.data() + chunk.size(), buffer + written);
  100. _buffers.pop_front();
  101. written += chunk.size();
  102. }
  103. }
  104. return written;
  105. });
  106. response->addHeader("Connection", "close");
  107. _request->send(response);
  108. }
  109. void AsyncWebPrint::setState(State state) {
  110. _state = state;
  111. }
  112. AsyncWebPrint::State AsyncWebPrint::getState() {
  113. return _state;
  114. }
  115. size_t AsyncWebPrint::write(uint8_t b) {
  116. const uint8_t tmp[1] {b};
  117. return write(tmp, 1);
  118. }
  119. bool AsyncWebPrint::_exhaustBuffers() {
  120. // XXX: espasyncwebserver will trigger write callback if we setup response too early
  121. // exploring code, callback handler responds to a special return value RESPONSE_TRY_AGAIN
  122. // but, it seemingly breaks chunked response logic
  123. // XXX: this should be **the only place** that can trigger yield() while we stay in CONT
  124. if (_state == State::None) {
  125. _prepareRequest();
  126. }
  127. const auto start = millis();
  128. do {
  129. if (millis() - start > 5000) {
  130. _buffers.clear();
  131. break;
  132. }
  133. yield();
  134. } while (!_buffers.empty());
  135. return _buffers.empty();
  136. }
  137. void AsyncWebPrint::flush() {
  138. _exhaustBuffers();
  139. _state = State::Done;
  140. }
  141. size_t AsyncWebPrint::write(const uint8_t* data, size_t size) {
  142. if (_state == State::Error) {
  143. return 0;
  144. }
  145. size_t full_size = size;
  146. auto* data_ptr = data;
  147. while (size) {
  148. if (_buffers.empty() && !_addBuffer()) {
  149. full_size = 0;
  150. break;
  151. }
  152. auto& current = _buffers.back();
  153. const auto have = current.capacity() - current.size();
  154. if (have >= size) {
  155. current.insert(current.end(), data_ptr, data_ptr + size);
  156. size = 0;
  157. } else {
  158. current.insert(current.end(), data_ptr, data_ptr + have);
  159. if (!_addBuffer()) {
  160. full_size = 0;
  161. break;
  162. }
  163. data_ptr += have;
  164. size -= have;
  165. }
  166. }
  167. return full_size;
  168. }
  169. // -----------------------------------------------------------------------------
  170. AsyncWebServer* _server;
  171. char _last_modified[50];
  172. std::vector<uint8_t> * _webConfigBuffer;
  173. bool _webConfigSuccess = false;
  174. std::vector<web_request_callback_f> _web_request_callbacks;
  175. std::vector<web_body_callback_f> _web_body_callbacks;
  176. constexpr const size_t WEB_CONFIG_BUFFER_MAX = 4096;
  177. // -----------------------------------------------------------------------------
  178. // HOOKS
  179. // -----------------------------------------------------------------------------
  180. void _onReset(AsyncWebServerRequest *request) {
  181. webLog(request);
  182. if (!webAuthenticate(request)) {
  183. return request->requestAuthentication(getSetting("hostname").c_str());
  184. }
  185. deferredReset(100, CUSTOM_RESET_HTTP);
  186. request->send(200);
  187. }
  188. void _onDiscover(AsyncWebServerRequest *request) {
  189. webLog(request);
  190. const String device = getBoardName();
  191. const String hostname = getSetting("hostname");
  192. StaticJsonBuffer<JSON_OBJECT_SIZE(4)> jsonBuffer;
  193. JsonObject &root = jsonBuffer.createObject();
  194. root["app"] = APP_NAME;
  195. root["version"] = getVersion().c_str();
  196. root["device"] = device.c_str();
  197. root["hostname"] = hostname.c_str();
  198. AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1);
  199. root.printTo(*response);
  200. request->send(response);
  201. }
  202. void _onGetConfig(AsyncWebServerRequest *request) {
  203. webLog(request);
  204. if (!webAuthenticate(request)) {
  205. return request->requestAuthentication(getSetting("hostname").c_str());
  206. }
  207. AsyncResponseStream *response = request->beginResponseStream("application/json");
  208. char buffer[100];
  209. snprintf_P(buffer, sizeof(buffer), PSTR("attachment; filename=\"%s-backup.json\""), (char *) getSetting("hostname").c_str());
  210. response->addHeader("Content-Disposition", buffer);
  211. response->addHeader("X-XSS-Protection", "1; mode=block");
  212. response->addHeader("X-Content-Type-Options", "nosniff");
  213. response->addHeader("X-Frame-Options", "deny");
  214. response->printf("{\n\"app\": \"" APP_NAME "\"");
  215. response->printf(",\n\"version\": \"%s\"", getVersion().c_str());
  216. response->printf(",\n\"backup\": \"1\"");
  217. #if NTP_SUPPORT
  218. response->printf(",\n\"timestamp\": \"%s\"", ntpDateTime().c_str());
  219. #endif
  220. // Write the keys line by line (not sorted)
  221. auto keys = settingsKeys();
  222. for (auto& key : keys) {
  223. String value = getSetting(key);
  224. response->printf(",\n\"%s\": \"%s\"", key.c_str(), value.c_str());
  225. }
  226. response->printf("\n}");
  227. request->send(response);
  228. }
  229. void _onPostConfig(AsyncWebServerRequest *request) {
  230. webLog(request);
  231. if (!webAuthenticate(request)) {
  232. return request->requestAuthentication(getSetting("hostname").c_str());
  233. }
  234. request->send(_webConfigSuccess ? 200 : 400);
  235. }
  236. void _onPostConfigFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
  237. if (!webAuthenticate(request)) {
  238. return request->requestAuthentication(getSetting("hostname").c_str());
  239. }
  240. // No buffer
  241. if (final && (index == 0)) {
  242. _webConfigSuccess = settingsRestoreJson((char*) data);
  243. return;
  244. }
  245. // Buffer start => reset
  246. if (index == 0) if (_webConfigBuffer) delete _webConfigBuffer;
  247. // init buffer if it doesn't exist
  248. if (!_webConfigBuffer) {
  249. _webConfigBuffer = new std::vector<uint8_t>();
  250. _webConfigSuccess = false;
  251. }
  252. // Copy
  253. if (len > 0) {
  254. if ((_webConfigBuffer->size() + len) > std::min(WEB_CONFIG_BUFFER_MAX, getFreeHeap() - sizeof(std::vector<uint8_t>))) {
  255. delete _webConfigBuffer;
  256. _webConfigBuffer = nullptr;
  257. request->send(500);
  258. return;
  259. }
  260. _webConfigBuffer->reserve(_webConfigBuffer->size() + len);
  261. _webConfigBuffer->insert(_webConfigBuffer->end(), data, data + len);
  262. }
  263. // Ending
  264. if (final) {
  265. _webConfigBuffer->push_back(0);
  266. _webConfigSuccess = settingsRestoreJson((char*) _webConfigBuffer->data());
  267. delete _webConfigBuffer;
  268. }
  269. }
  270. #if WEB_EMBEDDED
  271. void _onHome(AsyncWebServerRequest *request) {
  272. webLog(request);
  273. if (!webAuthenticate(request)) {
  274. return request->requestAuthentication(getSetting("hostname").c_str());
  275. }
  276. if (request->header("If-Modified-Since").equals(_last_modified)) {
  277. request->send(304);
  278. } else {
  279. #if WEB_SSL_ENABLED
  280. // Chunked response, we calculate the chunks based on free heap (in multiples of 32)
  281. // This is necessary when a TLS connection is open since it sucks too much memory
  282. DEBUG_MSG_P(PSTR("[MAIN] Free heap: %d bytes\n"), getFreeHeap());
  283. size_t max = (getFreeHeap() / 3) & 0xFFE0;
  284. AsyncWebServerResponse *response = request->beginChunkedResponse("text/html", [max](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
  285. // Get the chunk based on the index and maxLen
  286. size_t len = webui_image_len - index;
  287. if (len > maxLen) len = maxLen;
  288. if (len > max) len = max;
  289. if (len > 0) memcpy_P(buffer, webui_image + index, len);
  290. DEBUG_MSG_P(PSTR("[WEB] Sending %d%%%% (max chunk size: %4d)\r"), int(100 * index / webui_image_len), max);
  291. if (len == 0) DEBUG_MSG_P(PSTR("\n"));
  292. // Return the actual length of the chunk (0 for end of file)
  293. return len;
  294. });
  295. #else
  296. AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", webui_image, webui_image_len);
  297. #endif
  298. response->addHeader("Content-Encoding", "gzip");
  299. response->addHeader("Last-Modified", _last_modified);
  300. response->addHeader("X-XSS-Protection", "1; mode=block");
  301. response->addHeader("X-Content-Type-Options", "nosniff");
  302. response->addHeader("X-Frame-Options", "deny");
  303. request->send(response);
  304. }
  305. }
  306. #endif
  307. #if WEB_SSL_ENABLED
  308. int _onCertificate(void * arg, const char *filename, uint8_t **buf) {
  309. #if WEB_EMBEDDED
  310. if (strcmp(filename, "server.cer") == 0) {
  311. uint8_t * nbuf = (uint8_t*) malloc(server_cer_len);
  312. memcpy_P(nbuf, server_cer, server_cer_len);
  313. *buf = nbuf;
  314. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  315. return server_cer_len;
  316. }
  317. if (strcmp(filename, "server.key") == 0) {
  318. uint8_t * nbuf = (uint8_t*) malloc(server_key_len);
  319. memcpy_P(nbuf, server_key, server_key_len);
  320. *buf = nbuf;
  321. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  322. return server_key_len;
  323. }
  324. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
  325. *buf = 0;
  326. return 0;
  327. #else
  328. File file = SPIFFS.open(filename, "r");
  329. if (file) {
  330. size_t size = file.size();
  331. uint8_t * nbuf = (uint8_t*) malloc(size);
  332. if (nbuf) {
  333. size = file.read(nbuf, size);
  334. file.close();
  335. *buf = nbuf;
  336. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  337. return size;
  338. }
  339. file.close();
  340. }
  341. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
  342. *buf = 0;
  343. return 0;
  344. #endif // WEB_EMBEDDED == 1
  345. }
  346. #endif // WEB_SSL_ENABLED
  347. bool _onAPModeRequest(AsyncWebServerRequest *request) {
  348. if ((WiFi.getMode() & WIFI_AP) > 0) {
  349. const String domain = getSetting("hostname") + ".";
  350. const String host = request->header("Host");
  351. const String ip = WiFi.softAPIP().toString();
  352. // Only allow requests that use our hostname or ip
  353. if (host.equals(ip)) return true;
  354. if (host.startsWith(domain)) return true;
  355. // Immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
  356. // Not doing so will cause memory exhaustion, because the connection will linger
  357. request->send(404);
  358. request->client()->close();
  359. return false;
  360. }
  361. return true;
  362. }
  363. void _onRequest(AsyncWebServerRequest *request){
  364. if (!_onAPModeRequest(request)) return;
  365. // Send request to subscribers, break when request is 'handled' by the callback
  366. for (auto& callback : _web_request_callbacks) {
  367. if (callback(request)) {
  368. return;
  369. }
  370. }
  371. // No subscriber handled the request, return a 404 with implicit "Connection: close"
  372. request->send(404);
  373. // And immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
  374. // Not doing so will cause memory exhaustion, because the connection will linger
  375. request->client()->close();
  376. }
  377. void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
  378. if (!_onAPModeRequest(request)) return;
  379. // Send request to subscribers
  380. for (unsigned char i = 0; i < _web_body_callbacks.size(); i++) {
  381. bool response = (_web_body_callbacks[i])(request, data, len, index, total);
  382. if (response) return;
  383. }
  384. // Same as _onAPModeRequest(...)
  385. request->send(404);
  386. request->client()->close();
  387. }
  388. // -----------------------------------------------------------------------------
  389. bool webAuthenticate(AsyncWebServerRequest *request) {
  390. #if USE_PASSWORD
  391. return request->authenticate(WEB_USERNAME, getAdminPass().c_str());
  392. #else
  393. return true;
  394. #endif
  395. }
  396. // -----------------------------------------------------------------------------
  397. AsyncWebServer& webServer() {
  398. return *_server;
  399. }
  400. void webBodyRegister(web_body_callback_f callback) {
  401. _web_body_callbacks.push_back(callback);
  402. }
  403. void webRequestRegister(web_request_callback_f callback) {
  404. _web_request_callbacks.push_back(callback);
  405. }
  406. uint16_t webPort() {
  407. #if WEB_SSL_ENABLED
  408. return 443;
  409. #else
  410. constexpr const uint16_t defaultValue(WEB_PORT);
  411. return getSetting("webPort", defaultValue);
  412. #endif
  413. }
  414. void webLog(AsyncWebServerRequest *request) {
  415. DEBUG_MSG_P(PSTR("[WEBSERVER] %s %s\n"), request->methodToString(), request->url().c_str());
  416. }
  417. class WebAccessLogHandler : public AsyncWebHandler {
  418. bool canHandle(AsyncWebServerRequest* request) override {
  419. webLog(request);
  420. return false;
  421. }
  422. };
  423. void webSetup() {
  424. // Cache the Last-Modifier header value
  425. snprintf_P(_last_modified, sizeof(_last_modified), PSTR("%s %s GMT"), __DATE__, __TIME__);
  426. // Create server and install global URL debug handler
  427. // (since we don't want to forcibly add it to each instance)
  428. unsigned int port = webPort();
  429. _server = new AsyncWebServer(port);
  430. #if DEBUG_SUPPORT
  431. if (getSetting("webAccessLog", (1 == WEB_ACCESS_LOG))) {
  432. static WebAccessLogHandler log;
  433. _server->addHandler(&log);
  434. }
  435. #endif
  436. // Rewrites
  437. _server->rewrite("/", "/index.html");
  438. // Serve home (basic authentication protection is done manually b/c the handler is installed through callback functions)
  439. #if WEB_EMBEDDED
  440. _server->on("/index.html", HTTP_GET, _onHome);
  441. #endif
  442. // Serve static files (not supported, yet)
  443. #if SPIFFS_SUPPORT
  444. _server->serveStatic("/", SPIFFS, "/")
  445. .setLastModified(_last_modified)
  446. .setFilter([](AsyncWebServerRequest *request) -> bool {
  447. webLog(request);
  448. return true;
  449. });
  450. #endif
  451. _server->on("/reset", HTTP_GET, _onReset);
  452. _server->on("/config", HTTP_GET, _onGetConfig);
  453. _server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigFile);
  454. _server->on("/discover", HTTP_GET, _onDiscover);
  455. // Handle every other request, including 404
  456. _server->onRequestBody(_onBody);
  457. _server->onNotFound(_onRequest);
  458. // Run server
  459. #if WEB_SSL_ENABLED
  460. _server->onSslFileRequest(_onCertificate, NULL);
  461. _server->beginSecure("server.cer", "server.key", NULL);
  462. #else
  463. _server->begin();
  464. #endif
  465. DEBUG_MSG_P(PSTR("[WEBSERVER] Webserver running on port %u\n"), port);
  466. }
  467. #endif // WEB_SUPPORT