diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2023-09-27 00:54:57 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2023-09-27 00:54:57 +0300 |
commit | d3a295872c49defb55fc8e4e43e55550991e0927 (patch) | |
tree | b9dca15454f9027d5a9dad0d4443a20de04dbc5d /include/pio/libs/http_server | |
parent | b7cbc2571c1870b4582ead45277d0aa7f961bec8 (diff) | |
parent | bdbb296697f55f4c3a07af43c9aaf7a9ea86f3d0 (diff) |
Merge branch 'master' of ch1p.io:homekit
Diffstat (limited to 'include/pio/libs/http_server')
-rw-r--r-- | include/pio/libs/http_server/homekit/http_server.cpp | 282 | ||||
-rw-r--r-- | include/pio/libs/http_server/homekit/http_server.h | 62 | ||||
-rw-r--r-- | include/pio/libs/http_server/library.json | 8 |
3 files changed, 352 insertions, 0 deletions
diff --git a/include/pio/libs/http_server/homekit/http_server.cpp b/include/pio/libs/http_server/homekit/http_server.cpp new file mode 100644 index 0000000..ea81f5b --- /dev/null +++ b/include/pio/libs/http_server/homekit/http_server.cpp @@ -0,0 +1,282 @@ +#include "http_server.h" + +#include <Arduino.h> +#include <string.h> + +#include <homekit/static.h> +#include <homekit/config.h> +#include <homekit/logging.h> +#include <homekit/macros.h> +#include <homekit/util.h> + +namespace homekit { + +using files::StaticFile; + +static const char CONTENT_TYPE_HTML[] PROGMEM = "text/html; charset=utf-8"; +static const char CONTENT_TYPE_CSS[] PROGMEM = "text/css"; +static const char CONTENT_TYPE_JS[] PROGMEM = "application/javascript"; +static const char CONTENT_TYPE_JSON[] PROGMEM = "application/json"; +static const char CONTENT_TYPE_FAVICON[] PROGMEM = "image/x-icon"; + +static const char JSON_UPDATE_FMT[] PROGMEM = "{\"result\":%d}"; +static const char JSON_STATUS_FMT[] PROGMEM = "{\"node_id\":\"%s\"" +#ifdef DEBUG + ",\"configured\":%d" + ",\"crc\":%u" + ",\"fl_n\":%d" + ",\"fl_w\":%d" +#endif + "}"; +static const size_t JSON_BUF_SIZE = 192; + +static const char JSON_SCAN_FIRST_LIST[] PROGMEM = "{\"list\":["; + +static const char MSG_IS_INVALID[] PROGMEM = " is invalid"; +static const char MSG_IS_MISSING[] PROGMEM = " is missing"; + +static const char GZIP[] PROGMEM = "gzip"; +static const char CONTENT_ENCODING[] PROGMEM = "Content-Encoding"; +static const char NOT_FOUND[] PROGMEM = "Not Found"; + +static const char ROUTE_STYLE_CSS[] PROGMEM = "/style.css"; +static const char ROUTE_APP_JS[] PROGMEM = "/app.js"; +static const char ROUTE_MD5_JS[] PROGMEM = "/md5.js"; +static const char ROUTE_FAVICON_ICO[] PROGMEM = "/favicon.ico"; +static const char ROUTE_STATUS[] PROGMEM = "/status"; +static const char ROUTE_SCAN[] PROGMEM = "/scan"; +static const char ROUTE_RESET[] PROGMEM = "/reset"; +// #ifdef DEBUG +static const char ROUTE_HEAP[] PROGMEM = "/heap"; +// #endif +static const char ROUTE_UPDATE[] PROGMEM = "/update"; + +void HttpServer::start() { + server.on(FPSTR(ROUTE_STYLE_CSS), HTTP_GET, [&]() { sendGzip(files::style_css, CONTENT_TYPE_CSS); }); + server.on(FPSTR(ROUTE_APP_JS), HTTP_GET, [&]() { sendGzip(files::app_js, CONTENT_TYPE_JS); }); + server.on(FPSTR(ROUTE_MD5_JS), HTTP_GET, [&]() { sendGzip(files::md5_js, CONTENT_TYPE_JS); }); + server.on(FPSTR(ROUTE_FAVICON_ICO), HTTP_GET, [&]() { sendGzip(files::favicon_ico, CONTENT_TYPE_FAVICON); }); + + server.on("/", HTTP_GET, [&]() { sendGzip(files::index_html, CONTENT_TYPE_HTML); }); + server.on(FPSTR(ROUTE_STATUS), HTTP_GET, [&]() { + char json_buf[JSON_BUF_SIZE]; + auto cfg = config::read(); + + if (!isValid(cfg) || !cfg.flags.node_configured) { + sprintf_P(json_buf, JSON_STATUS_FMT + , CONFIG_NODE_ID +#ifdef DEBUG + , 0 + , cfg.crc + , cfg.flags.node_configured + , cfg.flags.wifi_configured +#endif + ); + } else { + char escaped_node_id[32]; + char *escaped_node_id_res = cfg.escapeHomeId(escaped_node_id, 32); + sprintf_P(json_buf, JSON_STATUS_FMT + , escaped_node_id_res == nullptr ? "?" : escaped_node_id +#ifdef DEBUG + , 1 + , cfg.crc + , cfg.flags.node_configured + , cfg.flags.wifi_configured +#endif + ); + } + server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf); + }); + server.on(FPSTR(ROUTE_STATUS), HTTP_POST, [&]() { + auto cfg = config::read(); + String s; + + if (!getInputParam("ssid", 32, s)) return; + strncpy(cfg.wifi_ssid, s.c_str(), 32); + PRINTF("saving ssid: %s\n", cfg.wifi_ssid); + + if (!getInputParam("psk", 63, s)) return; + strncpy(cfg.wifi_psk, s.c_str(), 63); + PRINTF("saving psk: %s\n", cfg.wifi_psk); + + if (!getInputParam("hid", 16, s)) return; + strcpy(cfg.node_id, s.c_str()); + PRINTF("saving home id: %s\n", cfg.node_id); + + cfg.flags.node_configured = 1; + cfg.flags.wifi_configured = 1; + + config::write(cfg); + + restartTimer.once(0, restart); + }); + + server.on(FPSTR(ROUTE_RESET), HTTP_POST, [&]() { + config::erase(); + restartTimer.once(1, restart); + }); + + server.on(FPSTR(ROUTE_HEAP), HTTP_GET, [&]() { + server.send(200, FPSTR(CONTENT_TYPE_HTML), String(ESP.getFreeHeap())); + }); + + server.on(FPSTR(ROUTE_SCAN), HTTP_GET, [&]() { + size_t i = 0; + size_t len; + const char* ssid; + bool enough = false; + + bzero(reinterpret_cast<uint8_t*>(scanBuf), scanBufSize); + char* cur = scanBuf; + + strncpy_P(cur, JSON_SCAN_FIRST_LIST, scanBufSize); + cur += 9; + + for (auto& res: *scanResults) { + ssid = res.ssid.c_str(); + len = res.ssid.length(); + + // new item (array with 2 items) + *cur++ = '['; + + // 1. ssid (string) + *cur++ = '"'; + for (size_t j = 0; j < len; j++) { + if (*(ssid+j) == '"') + *cur++ = '\\'; + *cur++ = *(ssid+j); + } + *cur++ = '"'; + *cur++ = ','; + + // 2. rssi (number) + cur += sprintf(cur, "%d", res.rssi); + + // close array + *cur++ = ']'; + + if ((size_t)(cur - scanBuf) >= (size_t) ARRAY_SIZE(scanBuf) - 40) + enough = true; + + if (i < scanResults->size() - 1 || enough) + *cur++ = ','; + + if (enough) + break; + + i++; + } + + *cur++ = ']'; + *cur++ = '}'; + *cur++ = '\0'; + + server.send(200, FPSTR(CONTENT_TYPE_JSON), scanBuf); + }); + + server.on(FPSTR(ROUTE_UPDATE), HTTP_POST, [&]() { + char json_buf[16]; + bool should_reboot = !Update.hasError() && !ota.invalidMd5; + Update.clearError(); + + sprintf_P(json_buf, JSON_UPDATE_FMT, should_reboot ? 1 : 0); + + server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf); + + if (should_reboot) + restartTimer.once(1, restart); + }, [&]() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + ota.clean(); + + String s; + if (!getInputParam("md5", 0, s)) { + ota.invalidMd5 = true; + PRINTLN("http/ota: md5 not found"); + return; + } + + if (!Update.setMD5(s.c_str())) { + ota.invalidMd5 = true; + PRINTLN("http/ota: setMD5() failed"); + return; + } + + Serial.printf("http/ota: starting, filename=%s\n", upload.filename.c_str()); + if (!Update.begin(otaGetMaxUpdateSize())) { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (!Update.isRunning()) + return; + + PRINTF("http/ota: writing %ul\n", upload.currentSize); + ota_led(); + + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } else if (upload.status == UPLOAD_FILE_END) { + if (!Update.isRunning()) + return; + + if (Update.end(true)) { + PRINTF("http/ota: ok, total size %ul\n", upload.totalSize); + } else { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } + }); + + server.onNotFound([&]() { + server.send(404, FPSTR(CONTENT_TYPE_HTML), NOT_FOUND); + }); + + server.begin(); +} + +void HttpServer::loop() { + server.handleClient(); +} + +void HttpServer::sendGzip(const StaticFile& file, PGM_P content_type) { + server.sendHeader(FPSTR(CONTENT_ENCODING), FPSTR(GZIP)); + server.send_P(200, content_type, (const char*)file.content, file.size); +} + +void HttpServer::sendError(const String& message) { + char buf[32]; + if (snprintf_P(buf, 32, PSTR("error: %s"), message.c_str()) == 32) + buf[31] = '\0'; + server.send(400, FPSTR(CONTENT_TYPE_HTML), buf); +} + +bool HttpServer::getInputParam(const char *field_name, + size_t max_len, + String& dst) { + if (!server.hasArg(field_name)) { + sendError(String(field_name) + String(MSG_IS_MISSING)); + return false; + } + + String field = server.arg(field_name); + if (!field.length() || (max_len != 0 && field.length() > max_len)) { + sendError(String(field_name) + String(MSG_IS_INVALID)); + return false; + } + + dst = field; + return true; +} + +void HttpServer::ota_led() const {} + +} diff --git a/include/pio/libs/http_server/homekit/http_server.h b/include/pio/libs/http_server/homekit/http_server.h new file mode 100644 index 0000000..8725a88 --- /dev/null +++ b/include/pio/libs/http_server/homekit/http_server.h @@ -0,0 +1,62 @@ +#ifndef COMMON_HOMEKIT_HTTP_SERVER_H +#define COMMON_HOMEKIT_HTTP_SERVER_H + +#include <ESP8266WebServer.h> +#include <Ticker.h> +#include <memory> +#include <list> +#include <utility> + +#include <homekit/config.h> +#include <homekit/wifi.h> +#include <homekit/static.h> + +namespace homekit { + +struct OTAStatus { + bool invalidMd5; + + OTAStatus() : invalidMd5(false) {} + + inline void clean() { + invalidMd5 = false; + } +}; + +using files::StaticFile; + +class HttpServer { +private: + ESP8266WebServer server; + Ticker restartTimer; + std::shared_ptr<std::list<wifi::ScanResult>> scanResults; + OTAStatus ota; + + char* scanBuf; + size_t scanBufSize; + + void sendGzip(const StaticFile& file, PGM_P content_type); + void sendError(const String& message); + + bool getInputParam(const char* field_name, size_t max_len, String& dst); + virtual void ota_led() const; + +public: + explicit HttpServer(std::shared_ptr<std::list<wifi::ScanResult>> scanResults) + : server(80) + , scanResults(std::move(scanResults)) + , scanBufSize(512) { + scanBuf = new char[scanBufSize]; + }; + + ~HttpServer() { + delete[] scanBuf; + } + + void start(); + void loop(); +}; + +} + +#endif //COMMON_HOMEKIT_HTTP_SERVER_H diff --git a/include/pio/libs/http_server/library.json b/include/pio/libs/http_server/library.json new file mode 100644 index 0000000..ee2d369 --- /dev/null +++ b/include/pio/libs/http_server/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_http_server", + "version": "1.0.3", + "build": { + "flags": "-I../../include" + } +} + |