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.

583 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
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. unsigned long count = settingsKeyCount();
  214. for (unsigned int i=0; i<count; i++) {
  215. String key = settingsKeyName(i);
  216. String value = getSetting(key);
  217. response->printf(",\n\"%s\": \"%s\"", key.c_str(), value.c_str());
  218. }
  219. response->printf("\n}");
  220. request->send(response);
  221. }
  222. void _onPostConfig(AsyncWebServerRequest *request) {
  223. webLog(request);
  224. if (!webAuthenticate(request)) {
  225. return request->requestAuthentication(getSetting("hostname").c_str());
  226. }
  227. request->send(_webConfigSuccess ? 200 : 400);
  228. }
  229. void _onPostConfigFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
  230. if (!webAuthenticate(request)) {
  231. return request->requestAuthentication(getSetting("hostname").c_str());
  232. }
  233. // No buffer
  234. if (final && (index == 0)) {
  235. _webConfigSuccess = settingsRestoreJson((char*) data);
  236. return;
  237. }
  238. // Buffer start => reset
  239. if (index == 0) if (_webConfigBuffer) delete _webConfigBuffer;
  240. // init buffer if it doesn't exist
  241. if (!_webConfigBuffer) {
  242. _webConfigBuffer = new std::vector<uint8_t>();
  243. _webConfigSuccess = false;
  244. }
  245. // Copy
  246. if (len > 0) {
  247. if ((_webConfigBuffer->size() + len) > std::min(WEB_CONFIG_BUFFER_MAX, getFreeHeap() - sizeof(std::vector<uint8_t>))) {
  248. delete _webConfigBuffer;
  249. _webConfigBuffer = nullptr;
  250. request->send(500);
  251. return;
  252. }
  253. _webConfigBuffer->reserve(_webConfigBuffer->size() + len);
  254. _webConfigBuffer->insert(_webConfigBuffer->end(), data, data + len);
  255. }
  256. // Ending
  257. if (final) {
  258. _webConfigBuffer->push_back(0);
  259. _webConfigSuccess = settingsRestoreJson((char*) _webConfigBuffer->data());
  260. delete _webConfigBuffer;
  261. }
  262. }
  263. #if WEB_EMBEDDED
  264. void _onHome(AsyncWebServerRequest *request) {
  265. webLog(request);
  266. if (!webAuthenticate(request)) {
  267. return request->requestAuthentication(getSetting("hostname").c_str());
  268. }
  269. if (request->header("If-Modified-Since").equals(_last_modified)) {
  270. request->send(304);
  271. } else {
  272. #if WEB_SSL_ENABLED
  273. // Chunked response, we calculate the chunks based on free heap (in multiples of 32)
  274. // This is necessary when a TLS connection is open since it sucks too much memory
  275. DEBUG_MSG_P(PSTR("[MAIN] Free heap: %d bytes\n"), getFreeHeap());
  276. size_t max = (getFreeHeap() / 3) & 0xFFE0;
  277. AsyncWebServerResponse *response = request->beginChunkedResponse("text/html", [max](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
  278. // Get the chunk based on the index and maxLen
  279. size_t len = webui_image_len - index;
  280. if (len > maxLen) len = maxLen;
  281. if (len > max) len = max;
  282. if (len > 0) memcpy_P(buffer, webui_image + index, len);
  283. DEBUG_MSG_P(PSTR("[WEB] Sending %d%%%% (max chunk size: %4d)\r"), int(100 * index / webui_image_len), max);
  284. if (len == 0) DEBUG_MSG_P(PSTR("\n"));
  285. // Return the actual length of the chunk (0 for end of file)
  286. return len;
  287. });
  288. #else
  289. AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", webui_image, webui_image_len);
  290. #endif
  291. response->addHeader("Content-Encoding", "gzip");
  292. response->addHeader("Last-Modified", _last_modified);
  293. response->addHeader("X-XSS-Protection", "1; mode=block");
  294. response->addHeader("X-Content-Type-Options", "nosniff");
  295. response->addHeader("X-Frame-Options", "deny");
  296. request->send(response);
  297. }
  298. }
  299. #endif
  300. #if WEB_SSL_ENABLED
  301. int _onCertificate(void * arg, const char *filename, uint8_t **buf) {
  302. #if WEB_EMBEDDED
  303. if (strcmp(filename, "server.cer") == 0) {
  304. uint8_t * nbuf = (uint8_t*) malloc(server_cer_len);
  305. memcpy_P(nbuf, server_cer, server_cer_len);
  306. *buf = nbuf;
  307. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  308. return server_cer_len;
  309. }
  310. if (strcmp(filename, "server.key") == 0) {
  311. uint8_t * nbuf = (uint8_t*) malloc(server_key_len);
  312. memcpy_P(nbuf, server_key, server_key_len);
  313. *buf = nbuf;
  314. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  315. return server_key_len;
  316. }
  317. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
  318. *buf = 0;
  319. return 0;
  320. #else
  321. File file = SPIFFS.open(filename, "r");
  322. if (file) {
  323. size_t size = file.size();
  324. uint8_t * nbuf = (uint8_t*) malloc(size);
  325. if (nbuf) {
  326. size = file.read(nbuf, size);
  327. file.close();
  328. *buf = nbuf;
  329. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
  330. return size;
  331. }
  332. file.close();
  333. }
  334. DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
  335. *buf = 0;
  336. return 0;
  337. #endif // WEB_EMBEDDED == 1
  338. }
  339. #endif // WEB_SSL_ENABLED
  340. bool _onAPModeRequest(AsyncWebServerRequest *request) {
  341. if ((WiFi.getMode() & WIFI_AP) > 0) {
  342. const String domain = getSetting("hostname") + ".";
  343. const String host = request->header("Host");
  344. const String ip = WiFi.softAPIP().toString();
  345. // Only allow requests that use our hostname or ip
  346. if (host.equals(ip)) return true;
  347. if (host.startsWith(domain)) return true;
  348. // Immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
  349. // Not doing so will cause memory exhaustion, because the connection will linger
  350. request->send(404);
  351. request->client()->close();
  352. return false;
  353. }
  354. return true;
  355. }
  356. void _onRequest(AsyncWebServerRequest *request){
  357. if (!_onAPModeRequest(request)) return;
  358. // Send request to subscribers
  359. for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) {
  360. bool response = (_web_request_callbacks[i])(request);
  361. if (response) return;
  362. }
  363. // No subscriber handled the request, return a 404 with implicit "Connection: close"
  364. request->send(404);
  365. // And immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
  366. // Not doing so will cause memory exhaustion, because the connection will linger
  367. request->client()->close();
  368. }
  369. void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
  370. if (!_onAPModeRequest(request)) return;
  371. // Send request to subscribers
  372. for (unsigned char i = 0; i < _web_body_callbacks.size(); i++) {
  373. bool response = (_web_body_callbacks[i])(request, data, len, index, total);
  374. if (response) return;
  375. }
  376. // Same as _onAPModeRequest(...)
  377. request->send(404);
  378. request->client()->close();
  379. }
  380. // -----------------------------------------------------------------------------
  381. bool webAuthenticate(AsyncWebServerRequest *request) {
  382. #if USE_PASSWORD
  383. return request->authenticate(WEB_USERNAME, getAdminPass().c_str());
  384. #else
  385. return true;
  386. #endif
  387. }
  388. // -----------------------------------------------------------------------------
  389. AsyncWebServer * webServer() {
  390. return _server;
  391. }
  392. void webBodyRegister(web_body_callback_f callback) {
  393. _web_body_callbacks.push_back(callback);
  394. }
  395. void webRequestRegister(web_request_callback_f callback) {
  396. _web_request_callbacks.push_back(callback);
  397. }
  398. uint16_t webPort() {
  399. #if WEB_SSL_ENABLED
  400. return 443;
  401. #else
  402. constexpr const uint16_t defaultValue(WEB_PORT);
  403. return getSetting("webPort", defaultValue);
  404. #endif
  405. }
  406. void webLog(AsyncWebServerRequest *request) {
  407. DEBUG_MSG_P(PSTR("[WEBSERVER] Request: %s %s\n"), request->methodToString(), request->url().c_str());
  408. }
  409. void webSetup() {
  410. // Cache the Last-Modifier header value
  411. snprintf_P(_last_modified, sizeof(_last_modified), PSTR("%s %s GMT"), __DATE__, __TIME__);
  412. // Create server
  413. unsigned int port = webPort();
  414. _server = new AsyncWebServer(port);
  415. // Rewrites
  416. _server->rewrite("/", "/index.html");
  417. // Serve home (basic authentication protection)
  418. #if WEB_EMBEDDED
  419. _server->on("/index.html", HTTP_GET, _onHome);
  420. #endif
  421. // Serve static files (not supported, yet)
  422. #if SPIFFS_SUPPORT
  423. _server->serveStatic("/", SPIFFS, "/")
  424. .setLastModified(_last_modified)
  425. .setFilter([](AsyncWebServerRequest *request) -> bool {
  426. webLog(request);
  427. return true;
  428. });
  429. #endif
  430. _server->on("/reset", HTTP_GET, _onReset);
  431. _server->on("/config", HTTP_GET, _onGetConfig);
  432. _server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigFile);
  433. _server->on("/discover", HTTP_GET, _onDiscover);
  434. // Handle every other request, including 404
  435. _server->onRequestBody(_onBody);
  436. _server->onNotFound(_onRequest);
  437. // Run server
  438. #if WEB_SSL_ENABLED
  439. _server->onSslFileRequest(_onCertificate, NULL);
  440. _server->beginSecure("server.cer", "server.key", NULL);
  441. #else
  442. _server->begin();
  443. #endif
  444. DEBUG_MSG_P(PSTR("[WEBSERVER] Webserver running on port %u\n"), port);
  445. }
  446. #endif // WEB_SUPPORT