- /*
-
- WEBSERVER MODULE
-
- Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
-
- */
-
- #if WEB_SUPPORT
-
- #include "system.h"
- #include "utils.h"
- #include "ota.h"
-
- #include <ESPAsyncTCP.h>
- #include <ESPAsyncWebServer.h>
- #include <Hash.h>
- #include <FS.h>
- #include <AsyncJson.h>
- #include <ArduinoJson.h>
-
- #if WEB_EMBEDDED
-
- #if WEBUI_IMAGE == WEBUI_IMAGE_SMALL
- #include "static/index.small.html.gz.h"
- #elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHT
- #include "static/index.light.html.gz.h"
- #elif WEBUI_IMAGE == WEBUI_IMAGE_SENSOR
- #include "static/index.sensor.html.gz.h"
- #elif WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE
- #include "static/index.rfbridge.html.gz.h"
- #elif WEBUI_IMAGE == WEBUI_IMAGE_RFM69
- #include "static/index.rfm69.html.gz.h"
- #elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX
- #include "static/index.lightfox.html.gz.h"
- #elif WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT
- #include "static/index.thermostat.html.gz.h"
- #elif WEBUI_IMAGE == WEBUI_IMAGE_FULL
- #include "static/index.all.html.gz.h"
- #endif
-
- #endif // WEB_EMBEDDED
-
- #if WEB_SSL_ENABLED
- #include "static/server.cer.h"
- #include "static/server.key.h"
- #endif // WEB_SSL_ENABLED
-
- // -----------------------------------------------------------------------------
-
- AsyncWebServer * _server;
- char _last_modified[50];
- std::vector<uint8_t> * _webConfigBuffer;
- bool _webConfigSuccess = false;
-
- std::vector<web_request_callback_f> _web_request_callbacks;
- std::vector<web_body_callback_f> _web_body_callbacks;
-
- constexpr const size_t WEB_CONFIG_BUFFER_MAX = 4096;
-
- // -----------------------------------------------------------------------------
- // HOOKS
- // -----------------------------------------------------------------------------
-
- void _onReset(AsyncWebServerRequest *request) {
-
- webLog(request);
- if (!webAuthenticate(request)) {
- return request->requestAuthentication(getSetting("hostname").c_str());
- }
-
- deferredReset(100, CUSTOM_RESET_HTTP);
- request->send(200);
- }
-
- void _onDiscover(AsyncWebServerRequest *request) {
-
- webLog(request);
-
- const String device = getBoardName();
- const String hostname = getSetting("hostname");
-
- StaticJsonBuffer<JSON_OBJECT_SIZE(4)> jsonBuffer;
- JsonObject &root = jsonBuffer.createObject();
- root["app"] = APP_NAME;
- root["version"] = APP_VERSION;
- root["device"] = device.c_str();
- root["hostname"] = hostname.c_str();
-
- AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1);
- root.printTo(*response);
-
- request->send(response);
-
- }
-
- void _onGetConfig(AsyncWebServerRequest *request) {
-
- webLog(request);
- if (!webAuthenticate(request)) {
- return request->requestAuthentication(getSetting("hostname").c_str());
- }
-
- AsyncResponseStream *response = request->beginResponseStream("application/json");
-
- char buffer[100];
- snprintf_P(buffer, sizeof(buffer), PSTR("attachment; filename=\"%s-backup.json\""), (char *) getSetting("hostname").c_str());
- response->addHeader("Content-Disposition", buffer);
- response->addHeader("X-XSS-Protection", "1; mode=block");
- response->addHeader("X-Content-Type-Options", "nosniff");
- response->addHeader("X-Frame-Options", "deny");
-
- response->printf("{\n\"app\": \"%s\"", APP_NAME);
- response->printf(",\n\"version\": \"%s\"", APP_VERSION);
- response->printf(",\n\"backup\": \"1\"");
- #if NTP_SUPPORT
- response->printf(",\n\"timestamp\": \"%s\"", ntpDateTime().c_str());
- #endif
-
- // Write the keys line by line (not sorted)
- unsigned long count = settingsKeyCount();
- for (unsigned int i=0; i<count; i++) {
- String key = settingsKeyName(i);
- String value = getSetting(key);
- response->printf(",\n\"%s\": \"%s\"", key.c_str(), value.c_str());
- }
- response->printf("\n}");
-
- request->send(response);
-
- }
-
- void _onPostConfig(AsyncWebServerRequest *request) {
- webLog(request);
- if (!webAuthenticate(request)) {
- return request->requestAuthentication(getSetting("hostname").c_str());
- }
- request->send(_webConfigSuccess ? 200 : 400);
- }
-
- void _onPostConfigFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
-
- if (!webAuthenticate(request)) {
- return request->requestAuthentication(getSetting("hostname").c_str());
- }
-
- // No buffer
- if (final && (index == 0)) {
- _webConfigSuccess = settingsRestoreJson((char*) data);
- return;
- }
-
- // Buffer start => reset
- if (index == 0) if (_webConfigBuffer) delete _webConfigBuffer;
-
- // init buffer if it doesn't exist
- if (!_webConfigBuffer) {
- _webConfigBuffer = new std::vector<uint8_t>();
- _webConfigSuccess = false;
- }
-
- // Copy
- if (len > 0) {
- if ((_webConfigBuffer->size() + len) > std::min(WEB_CONFIG_BUFFER_MAX, getFreeHeap() - sizeof(std::vector<uint8_t>))) {
- delete _webConfigBuffer;
- _webConfigBuffer = nullptr;
- request->send(500);
- return;
- }
- _webConfigBuffer->reserve(_webConfigBuffer->size() + len);
- _webConfigBuffer->insert(_webConfigBuffer->end(), data, data + len);
- }
-
- // Ending
- if (final) {
-
- _webConfigBuffer->push_back(0);
- _webConfigSuccess = settingsRestoreJson((char*) _webConfigBuffer->data());
- delete _webConfigBuffer;
-
- }
-
- }
-
- #if WEB_EMBEDDED
- void _onHome(AsyncWebServerRequest *request) {
-
- webLog(request);
- if (!webAuthenticate(request)) {
- return request->requestAuthentication(getSetting("hostname").c_str());
- }
-
- if (request->header("If-Modified-Since").equals(_last_modified)) {
-
- request->send(304);
-
- } else {
-
- #if WEB_SSL_ENABLED
-
- // Chunked response, we calculate the chunks based on free heap (in multiples of 32)
- // This is necessary when a TLS connection is open since it sucks too much memory
- DEBUG_MSG_P(PSTR("[MAIN] Free heap: %d bytes\n"), getFreeHeap());
- size_t max = (getFreeHeap() / 3) & 0xFFE0;
-
- AsyncWebServerResponse *response = request->beginChunkedResponse("text/html", [max](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
-
- // Get the chunk based on the index and maxLen
- size_t len = webui_image_len - index;
- if (len > maxLen) len = maxLen;
- if (len > max) len = max;
- if (len > 0) memcpy_P(buffer, webui_image + index, len);
-
- DEBUG_MSG_P(PSTR("[WEB] Sending %d%%%% (max chunk size: %4d)\r"), int(100 * index / webui_image_len), max);
- if (len == 0) DEBUG_MSG_P(PSTR("\n"));
-
- // Return the actual length of the chunk (0 for end of file)
- return len;
-
- });
-
- #else
-
- AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", webui_image, webui_image_len);
-
- #endif
-
- response->addHeader("Content-Encoding", "gzip");
- response->addHeader("Last-Modified", _last_modified);
- response->addHeader("X-XSS-Protection", "1; mode=block");
- response->addHeader("X-Content-Type-Options", "nosniff");
- response->addHeader("X-Frame-Options", "deny");
- request->send(response);
-
- }
-
- }
- #endif
-
- #if WEB_SSL_ENABLED
-
- int _onCertificate(void * arg, const char *filename, uint8_t **buf) {
-
- #if WEB_EMBEDDED
-
- if (strcmp(filename, "server.cer") == 0) {
- uint8_t * nbuf = (uint8_t*) malloc(server_cer_len);
- memcpy_P(nbuf, server_cer, server_cer_len);
- *buf = nbuf;
- DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
- return server_cer_len;
- }
-
- if (strcmp(filename, "server.key") == 0) {
- uint8_t * nbuf = (uint8_t*) malloc(server_key_len);
- memcpy_P(nbuf, server_key, server_key_len);
- *buf = nbuf;
- DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
- return server_key_len;
- }
-
- DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
- *buf = 0;
- return 0;
-
- #else
-
- File file = SPIFFS.open(filename, "r");
- if (file) {
- size_t size = file.size();
- uint8_t * nbuf = (uint8_t*) malloc(size);
- if (nbuf) {
- size = file.read(nbuf, size);
- file.close();
- *buf = nbuf;
- DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - OK\n"), filename);
- return size;
- }
- file.close();
- }
- DEBUG_MSG_P(PSTR("[WEB] SSL File: %s - ERROR\n"), filename);
- *buf = 0;
- return 0;
-
- #endif
-
- }
-
- #endif
-
- void _onUpgradeResponse(AsyncWebServerRequest *request, int code, const String& payload = "") {
-
- auto *response = request->beginResponseStream("text/plain", 256);
- response->addHeader("Connection", "close");
- response->addHeader("X-XSS-Protection", "1; mode=block");
- response->addHeader("X-Content-Type-Options", "nosniff");
- response->addHeader("X-Frame-Options", "deny");
-
- response->setCode(code);
-
- if (payload.length()) {
- response->printf("%s", payload.c_str());
- } else {
- if (!Update.hasError()) {
- response->print("OK");
- } else {
- #if defined(ARDUINO_ESP8266_RELEASE_2_3_0)
- Update.printError(reinterpret_cast<Stream&>(response));
- #else
- Update.printError(*response);
- #endif
- }
- }
-
- request->send(response);
-
- }
-
- void _onUpgradeStatusSet(AsyncWebServerRequest *request, int code, const String& payload = "") {
- _onUpgradeResponse(request, code, payload);
- request->_tempObject = malloc(sizeof(bool));
- }
-
- void _onUpgrade(AsyncWebServerRequest *request) {
-
- webLog(request);
- if (!webAuthenticate(request)) {
- return request->requestAuthentication(getSetting("hostname").c_str());
- }
-
- if (request->_tempObject) {
- return;
- }
-
- _onUpgradeResponse(request, 200);
-
- }
-
- void _onUpgradeFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
-
- if (!webAuthenticate(request)) {
- return request->requestAuthentication(getSetting("hostname").c_str());
- }
-
- // We set this after we are done with the request
- // It is still possible to re-enter this callback even after connection is already closed
- // 1.14.2: TODO: see https://github.com/me-no-dev/ESPAsyncWebServer/pull/660
- // remote close or request sending some data before finishing parsing of the body will leak 1460 bytes
- // waiting a bit for upstream. fork and point to the fixed version if not resolved before 1.14.2
- if (request->_tempObject) {
- return;
- }
-
- if (!index) {
-
- // TODO: stop network activity completely when handling Update through ArduinoOTA or `ota` command?
- if (Update.isRunning()) {
- _onUpgradeStatusSet(request, 400, F("ERROR: Upgrade in progress"));
- return;
- }
-
- // Check that header is correct and there is more data before anything is written to the flash
- if (final || !len) {
- _onUpgradeStatusSet(request, 400, F("ERROR: Invalid request"));
- return;
- }
-
- if (!otaVerifyHeader(data, len)) {
- _onUpgradeStatusSet(request, 400, F("ERROR: No magic byte / invalid flash config"));
- return;
- }
-
- // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade
- eepromRotate(false);
-
- DEBUG_MSG_P(PSTR("[UPGRADE] Start: %s\n"), filename.c_str());
- Update.runAsync(true);
-
- // Note: cannot use request->contentLength() for multipart/form-data
- if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
- _onUpgradeStatusSet(request, 500);
- eepromRotate(true);
- return;
- }
-
- }
-
- if (request->_tempObject) {
- return;
- }
-
- // Any error will cancel the update, but request may still be alive
- if (!Update.isRunning()) {
- return;
- }
-
- if (Update.write(data, len) != len) {
- _onUpgradeStatusSet(request, 500);
- Update.end();
- eepromRotate(true);
- return;
- }
-
- if (final) {
- otaFinalize(index + len, CUSTOM_RESET_UPGRADE, true);
- } else {
- otaProgress(index + len);
- }
-
- }
-
- bool _onAPModeRequest(AsyncWebServerRequest *request) {
-
- if ((WiFi.getMode() & WIFI_AP) > 0) {
- const String domain = getSetting("hostname") + ".";
- const String host = request->header("Host");
- const String ip = WiFi.softAPIP().toString();
-
- // Only allow requests that use our hostname or ip
- if (host.equals(ip)) return true;
- if (host.startsWith(domain)) return true;
-
- // Immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
- // Not doing so will cause memory exhaustion, because the connection will linger
- request->send(404);
- request->client()->close();
-
- return false;
- }
-
- return true;
-
- }
-
- void _onRequest(AsyncWebServerRequest *request){
-
- if (!_onAPModeRequest(request)) return;
-
- // Send request to subscribers
- for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) {
- bool response = (_web_request_callbacks[i])(request);
- if (response) return;
- }
-
- // No subscriber handled the request, return a 404 with implicit "Connection: close"
- request->send(404);
-
- // And immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660
- // Not doing so will cause memory exhaustion, because the connection will linger
- request->client()->close();
-
- }
-
- void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
-
- if (!_onAPModeRequest(request)) return;
-
- // Send request to subscribers
- for (unsigned char i = 0; i < _web_body_callbacks.size(); i++) {
- bool response = (_web_body_callbacks[i])(request, data, len, index, total);
- if (response) return;
- }
-
- // Same as _onAPModeRequest(...)
- request->send(404);
- request->client()->close();
-
- }
-
-
- // -----------------------------------------------------------------------------
-
- bool webAuthenticate(AsyncWebServerRequest *request) {
- #if USE_PASSWORD
- return request->authenticate(WEB_USERNAME, getAdminPass().c_str());
- #else
- return true;
- #endif
- }
-
- // -----------------------------------------------------------------------------
-
- AsyncWebServer * webServer() {
- return _server;
- }
-
- void webBodyRegister(web_body_callback_f callback) {
- _web_body_callbacks.push_back(callback);
- }
-
- void webRequestRegister(web_request_callback_f callback) {
- _web_request_callbacks.push_back(callback);
- }
-
- uint16_t webPort() {
- #if WEB_SSL_ENABLED
- return 443;
- #else
- constexpr const uint16_t defaultValue(WEB_PORT);
- return getSetting("webPort", defaultValue);
- #endif
- }
-
- void webLog(AsyncWebServerRequest *request) {
- DEBUG_MSG_P(PSTR("[WEBSERVER] Request: %s %s\n"), request->methodToString(), request->url().c_str());
- }
-
- void webSetup() {
-
- // Cache the Last-Modifier header value
- snprintf_P(_last_modified, sizeof(_last_modified), PSTR("%s %s GMT"), __DATE__, __TIME__);
-
- // Create server
- unsigned int port = webPort();
- _server = new AsyncWebServer(port);
-
- // Rewrites
- _server->rewrite("/", "/index.html");
-
- // Serve home (basic authentication protection)
- #if WEB_EMBEDDED
- _server->on("/index.html", HTTP_GET, _onHome);
- #endif
-
- // Other entry points
- _server->on("/reset", HTTP_GET, _onReset);
- _server->on("/config", HTTP_GET, _onGetConfig);
- _server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigFile);
- _server->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeFile);
- _server->on("/discover", HTTP_GET, _onDiscover);
-
- // Serve static files
- #if SPIFFS_SUPPORT
- _server->serveStatic("/", SPIFFS, "/")
- .setLastModified(_last_modified)
- .setFilter([](AsyncWebServerRequest *request) -> bool {
- webLog(request);
- return true;
- });
- #endif
-
-
- // Handle other requests, including 404
- _server->onRequestBody(_onBody);
- _server->onNotFound(_onRequest);
-
- // Run server
- #if WEB_SSL_ENABLED
- _server->onSslFileRequest(_onCertificate, NULL);
- _server->beginSecure("server.cer", "server.key", NULL);
- #else
- _server->begin();
- #endif
-
- DEBUG_MSG_P(PSTR("[WEBSERVER] Webserver running on port %u\n"), port);
-
- }
-
- #endif // WEB_SUPPORT
|