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.

589 lines
17 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
6 years ago
6 years ago
6 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_THERMOSTAT
  34. #include "static/index.thermostat.html.gz.h"
  35. #elif WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN
  36. #include "static/index.curtain.html.gz.h"
  37. #elif WEBUI_IMAGE == WEBUI_IMAGE_FULL
  38. #include "static/index.all.html.gz.h"
  39. #endif
  40. #endif // WEB_EMBEDDED
  41. #if WEB_SSL_ENABLED
  42. #include "static/server.cer.h"
  43. #include "static/server.key.h"
  44. #endif // WEB_SSL_ENABLED
  45. AsyncWebPrint::AsyncWebPrint(const AsyncWebPrintConfig& config, AsyncWebServerRequest* request) :
  46. mimeType(config.mimeType),
  47. backlogCountMax(config.backlogCountMax),
  48. backlogSizeMax(config.backlogSizeMax),
  49. backlogTimeout(config.backlogTimeout),
  50. _request(request),
  51. _state(State::None)
  52. {}
  53. bool AsyncWebPrint::_addBuffer() {
  54. if ((_buffers.size() + 1) > backlogCountMax) {
  55. if (!_exhaustBuffers()) {
  56. _state = State::Error;
  57. return false;
  58. }
  59. }
  60. // Note: c++17, emplace returns created object reference
  61. // c++11, we need to use .back()
  62. _buffers.emplace_back(backlogSizeMax, 0);
  63. _buffers.back().clear();
  64. return true;
  65. }
  66. // Creates response object that will handle the data written into the Print& interface.
  67. //
  68. // This API expects a **very** careful approach to context switching between SYS and CONT:
  69. // - Returning RESPONSE_TRY_AGAIN before buffers are filled will result in invalid size marker being sent on the wire.
  70. // HTTP client (curl, python requests etc., as discovered in testing) will then drop the connection
  71. // - Returning 0 will immediatly close the connection from our side
  72. // - Calling _prepareRequest() **before** _buffers are filled will result in returning 0
  73. // - Calling yield() / delay() while request AsyncWebPrint is active **may** trigger this callback out of sequence
  74. // (e.g. Serial.print(..), DEBUG_MSG(...), or any other API trying to switch contexts)
  75. // - Receiving data (tcp ack from the previous packet) **will** trigger the callback when switching contexts.
  76. void AsyncWebPrint::_prepareRequest() {
  77. _state = State::Sending;
  78. auto *response = _request->beginChunkedResponse(mimeType, [this](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
  79. switch (_state) {
  80. case State::None:
  81. return RESPONSE_TRY_AGAIN;
  82. case State::Error:
  83. case State::Done:
  84. return 0;
  85. case State::Sending:
  86. break;
  87. }
  88. size_t written = 0;
  89. while ((written < maxLen) && !_buffers.empty()) {
  90. auto& chunk =_buffers.front();
  91. auto have = maxLen - written;
  92. if (chunk.size() > have) {
  93. std::copy(chunk.data(), chunk.data() + have, buffer + written);
  94. chunk.erase(chunk.begin(), chunk.begin() + have);
  95. written += have;
  96. } else {
  97. std::copy(chunk.data(), chunk.data() + chunk.size(), buffer + written);
  98. _buffers.pop_front();
  99. written += chunk.size();
  100. }
  101. }
  102. return written;
  103. });
  104. response->addHeader("Connection", "close");
  105. _request->send(response);
  106. }
  107. void AsyncWebPrint::setState(State state) {
  108. _state = state;
  109. }
  110. AsyncWebPrint::State AsyncWebPrint::getState() {
  111. return _state;
  112. }
  113. size_t AsyncWebPrint::write(uint8_t b) {
  114. const uint8_t tmp[1] {b};
  115. return write(tmp, 1);
  116. }
  117. bool AsyncWebPrint::_exhaustBuffers() {
  118. // XXX: espasyncwebserver will trigger write callback if we setup response too early
  119. // exploring code, callback handler responds to a special return value RESPONSE_TRY_AGAIN
  120. // but, it seemingly breaks chunked response logic
  121. // XXX: this should be **the only place** that can trigger yield() while we stay in CONT
  122. if (_state == State::None) {
  123. _prepareRequest();
  124. }
  125. const auto start = millis();
  126. do {
  127. if (millis() - start > 5000) {
  128. _buffers.clear();
  129. break;
  130. }
  131. yield();
  132. } while (!_buffers.empty());
  133. return _buffers.empty();
  134. }
  135. void AsyncWebPrint::flush() {
  136. _exhaustBuffers();
  137. _state = State::Done;
  138. }
  139. size_t AsyncWebPrint::write(const uint8_t* data, size_t size) {
  140. if (_state == State::Error) {
  141. return 0;
  142. }
  143. size_t full_size = size;
  144. auto* data_ptr = data;
  145. while (size) {
  146. if (_buffers.empty() && !_addBuffer()) {
  147. full_size = 0;
  148. break;
  149. }
  150. auto& current = _buffers.back();
  151. const auto have = current.capacity() - current.size();
  152. if (have >= size) {
  153. current.insert(current.end(), data_ptr, data_ptr + size);
  154. size = 0;
  155. } else {
  156. current.insert(current.end(), data_ptr, data_ptr + have);
  157. if (!_addBuffer()) {
  158. full_size = 0;
  159. break;
  160. }
  161. data_ptr += have;
  162. size -= have;
  163. }
  164. }
  165. return full_size;
  166. }
  167. // -----------------------------------------------------------------------------
  168. AsyncWebServer * _server;
  169. char _last_modified[50];
  170. std::vector<uint8_t> * _webConfigBuffer;
  171. bool _webConfigSuccess = false;
  172. std::vector<web_request_callback_f> _web_request_callbacks;
  173. std::vector<web_body_callback_f> _web_body_callbacks;
  174. constexpr const size_t WEB_CONFIG_BUFFER_MAX = 4096;
  175. // -----------------------------------------------------------------------------
  176. // HOOKS
  177. // -----------------------------------------------------------------------------
  178. void _onReset(AsyncWebServerRequest *request) {
  179. webLog(request);
  180. if (!webAuthenticate(request)) {
  181. return request->requestAuthentication(getSetting("hostname").c_str());
  182. }
  183. deferredReset(100, CUSTOM_RESET_HTTP);
  184. request->send(200);
  185. }
  186. void _onDiscover(AsyncWebServerRequest *request) {
  187. webLog(request);
  188. const String device = getBoardName();
  189. const String hostname = getSetting("hostname");
  190. StaticJsonBuffer<JSON_OBJECT_SIZE(4)> jsonBuffer;
  191. JsonObject &root = jsonBuffer.createObject();
  192. root["app"] = APP_NAME;
  193. root["version"] = APP_VERSION;
  194. root["device"] = device.c_str();
  195. root["hostname"] = hostname.c_str();
  196. AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1);
  197. root.printTo(*response);
  198. request->send(response);
  199. }
  200. void _onGetConfig(AsyncWebServerRequest *request) {
  201. webLog(request);
  202. if (!webAuthenticate(request)) {
  203. return request->requestAuthentication(getSetting("hostname").c_str());
  204. }
  205. AsyncResponseStream *response = request->beginResponseStream("application/json");
  206. char buffer[100];
  207. snprintf_P(buffer, sizeof(buffer), PSTR("attachment; filename=\"%s-backup.json\""), (char *) getSetting("hostname").c_str());
  208. response->addHeader("Content-Disposition", buffer);
  209. response->addHeader("X-XSS-Protection", "1; mode=block");
  210. response->addHeader("X-Content-Type-Options", "nosniff");
  211. response->addHeader("X-Frame-Options", "deny");
  212. response->printf("{\n\"app\": \"%s\"", APP_NAME);
  213. response->printf(",\n\"version\": \"%s\"", APP_VERSION);
  214. response->printf(",\n\"backup\": \"1\"");
  215. #if NTP_SUPPORT
  216. response->printf(",\n\"timestamp\": \"%s\"", ntpDateTime().c_str());
  217. #endif
  218. // Write the keys line by line (not sorted)
  219. auto keys = settingsKeys();
  220. for (auto& key : keys) {
  221. String value = getSetting(key);
  222. response->printf(",\n\"%s\": \"%s\"", key.c_str(), value.c_str());
  223. }
  224. response->printf("\n}");
  225. request->send(response);
  226. }
  227. void _onPostConfig(AsyncWebServerRequest *request) {
  228. webLog(request);
  229. if (!webAuthenticate(request)) {
  230. return request->requestAuthentication(getSetting("hostname").c_str());
  231. }
  232. request->send(_webConfigSuccess ? 200 : 400);
  233. }
  234. void _onPostConfigFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
  235. if (!webAuthenticate(request)) {
  236. return request->requestAuthentication(getSetting("hostname").c_str());
  237. }
  238. // No buffer
  239. if (final && (index == 0)) {
  240. _webConfigSuccess = settingsRestoreJson((char*) data);
  241. return;
  242. }
  243. // Buffer start => reset
  244. if (index == 0) if (_webConfigBuffer) delete _webConfigBuffer;
  245. // init buffer if it doesn't exist
  246. if (!_webConfigBuffer) {
  247. _webConfigBuffer = new std::vector<uint8_t>();
  248. _webConfigSuccess = false;
  249. }
  250. // Copy
  251. if (len > 0) {
  252. if ((_webConfigBuffer->size() + len) > std::min(WEB_CONFIG_BUFFER_MAX, getFreeHeap() - sizeof(std::vector<uint8_t>))) {
  253. delete _webConfigBuffer;
  254. _webConfigBuffer = nullptr;
  255. request->send(500);
  256. return;
  257. }
  258. _webConfigBuffer->reserve(_webConfigBuffer->size() + len);
  259. _webConfigBuffer->insert(_webConfigBuffer->end(), data, data + len);
  260. }
  261. // Ending
  262. if (final) {
  263. _webConfigBuffer->push_back(0);
  264. _webConfigSuccess = settingsRestoreJson((char*) _webConfigBuffer->data());
  265. delete _webConfigBuffer;
  266. }
  267. }
  268. #if WEB_EMBEDDED
  269. void _onHome(AsyncWebServerRequest *request) {
  270. webLog(request);
  271. if (!webAuthenticate(request)) {
  272. return request->requestAuthentication(getSetting("hostname").c_str());
  273. }
  274. if (request->header("If-Modified-Since").equals(_last_modified)) {
  275. request->send(304);
  276. } else {
  277. #if WEB_SSL_ENABLED
  278. // Chunked response, we calculate the chunks based on free heap (in multiples of 32)
  279. // This is necessary when a TLS connection is open since it sucks too much memory
  280. DEBUG_MSG_P(PSTR("[MAIN] Free heap: %d bytes\n"), getFreeHeap());
  281. size_t max = (getFreeHeap() / 3) & 0xFFE0;
  282. AsyncWebServerResponse *response = request->beginChunkedResponse("text/html", [max](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
  283. // Get the chunk based on the index and maxLen
  284. size_t len = webui_image_len - index;
  285. if (len > maxLen) len = maxLen;
  286. if (len > max) len = max;
  287. if (len > 0) memcpy_P(buffer, webui_image + index, len);
  288. DEBUG_MSG_P(PSTR("[WEB] Sending %d%%%% (max chunk size: %4d)\r"), int(100 * index / webui_image_len), max);
  289. if (len == 0) DEBUG_MSG_P(PSTR("\n"));
  290. // Return the actual length of the chunk (0 for end of file)
  291. return len;
  292. });
  293. #else
  294. AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", webui_image, webui_image_len);
  295. #endif
  296. response->addHeader("Content-Encoding", "gzip");
  297. response->addHeader("Last-Modified", _last_modified);
  298. response->addHeader("X-XSS-Protection", "1; mode=block");
  299. response->addHeader("X-Content-Type-Options", "nosniff");
  300. response->addHeader("X-Frame-Options", "deny");
  301. request->send(response);
  302. }
  303. }
  304. #endif
  305. #if WEB_SSL_ENABLED
  306. int _onCertificate(void * arg, const char *filename, uint8_t **buf) {
  307. #if WEB_EMBEDDED
  308. if (strcmp(filename, "server.cer") == 0) {
  309. uint8_t * nbuf = (uint8_t*) malloc(server_cer_len);
  310. memcpy_P(nbuf, server_cer, server_cer_len);
  311. *buf = nbuf;
  312. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  313. return server_cer_len;
  314. }
  315. if (strcmp(filename, "server.key") == 0) {
  316. uint8_t * nbuf = (uint8_t*) malloc(server_key_len);
  317. memcpy_P(nbuf, server_key, server_key_len);
  318. *buf = nbuf;
  319. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  320. return server_key_len;
  321. }
  322. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
  323. *buf = 0;
  324. return 0;
  325. #else
  326. File file = SPIFFS.open(filename, "r");
  327. if (file) {
  328. size_t size = file.size();
  329. uint8_t * nbuf = (uint8_t*) malloc(size);
  330. if (nbuf) {
  331. size = file.read(nbuf, size);
  332. file.close();
  333. *buf = nbuf;
  334. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  335. return size;
  336. }
  337. file.close();
  338. }
  339. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
  340. *buf = 0;
  341. return 0;
  342. #endif // WEB_EMBEDDED == 1
  343. }
  344. #endif // WEB_SSL_ENABLED
  345. bool _onAPModeRequest(AsyncWebServerRequest *request) {
  346. if ((WiFi.getMode() & WIFI_AP) > 0) {
  347. const String domain = getSetting("hostname") + ".";
  348. const String host = request->header("Host");
  349. const String ip = WiFi.softAPIP().toString();
  350. // Only allow requests that use our hostname or ip
  351. if (host.equals(ip)) return true;
  352. if (host.startsWith(domain)) return true;
  353. // Immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
  354. // Not doing so will cause memory exhaustion, because the connection will linger
  355. request->send(404);
  356. request->client()->close();
  357. return false;
  358. }
  359. return true;
  360. }
  361. void _onRequest(AsyncWebServerRequest *request){
  362. if (!_onAPModeRequest(request)) return;
  363. // Send request to subscribers
  364. for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) {
  365. bool response = (_web_request_callbacks[i])(request);
  366. if (response) return;
  367. }
  368. // No subscriber handled the request, return a 404 with implicit "Connection: close"
  369. request->send(404);
  370. // And immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
  371. // Not doing so will cause memory exhaustion, because the connection will linger
  372. request->client()->close();
  373. }
  374. void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
  375. if (!_onAPModeRequest(request)) return;
  376. // Send request to subscribers
  377. for (unsigned char i = 0; i < _web_body_callbacks.size(); i++) {
  378. bool response = (_web_body_callbacks[i])(request, data, len, index, total);
  379. if (response) return;
  380. }
  381. // Same as _onAPModeRequest(...)
  382. request->send(404);
  383. request->client()->close();
  384. }
  385. // -----------------------------------------------------------------------------
  386. bool webAuthenticate(AsyncWebServerRequest *request) {
  387. #if USE_PASSWORD
  388. return request->authenticate(WEB_USERNAME, getAdminPass().c_str());
  389. #else
  390. return true;
  391. #endif
  392. }
  393. // -----------------------------------------------------------------------------
  394. AsyncWebServer * webServer() {
  395. return _server;
  396. }
  397. void webBodyRegister(web_body_callback_f callback) {
  398. _web_body_callbacks.push_back(callback);
  399. }
  400. void webRequestRegister(web_request_callback_f callback) {
  401. _web_request_callbacks.push_back(callback);
  402. }
  403. uint16_t webPort() {
  404. #if WEB_SSL_ENABLED
  405. return 443;
  406. #else
  407. constexpr const uint16_t defaultValue(WEB_PORT);
  408. return getSetting("webPort", defaultValue);
  409. #endif
  410. }
  411. void webLog(AsyncWebServerRequest *request) {
  412. DEBUG_MSG_P(PSTR("[WEBSERVER] Request: %s %s\n"), request->methodToString(), request->url().c_str());
  413. }
  414. void webSetup() {
  415. // Cache the Last-Modifier header value
  416. snprintf_P(_last_modified, sizeof(_last_modified), PSTR("%s %s GMT"), __DATE__, __TIME__);
  417. // Create server
  418. unsigned int port = webPort();
  419. _server = new AsyncWebServer(port);
  420. // Rewrites
  421. _server->rewrite("/", "/index.html");
  422. // Serve home (basic authentication protection)
  423. #if WEB_EMBEDDED
  424. _server->on("/index.html", HTTP_GET, _onHome);
  425. #endif
  426. // Serve static files (not supported, yet)
  427. #if SPIFFS_SUPPORT
  428. _server->serveStatic("/", SPIFFS, "/")
  429. .setLastModified(_last_modified)
  430. .setFilter([](AsyncWebServerRequest *request) -> bool {
  431. webLog(request);
  432. return true;
  433. });
  434. #endif
  435. _server->on("/reset", HTTP_GET, _onReset);
  436. _server->on("/config", HTTP_GET, _onGetConfig);
  437. _server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigFile);
  438. _server->on("/discover", HTTP_GET, _onDiscover);
  439. // Handle every other request, including 404
  440. _server->onRequestBody(_onBody);
  441. _server->onNotFound(_onRequest);
  442. // Run server
  443. #if WEB_SSL_ENABLED
  444. _server->onSslFileRequest(_onCertificate, NULL);
  445. _server->beginSecure("server.cer", "server.key", NULL);
  446. #else
  447. _server->begin();
  448. #endif
  449. DEBUG_MSG_P(PSTR("[WEBSERVER] Webserver running on port %u\n"), port);
  450. }
  451. #endif // WEB_SUPPORT