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.

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