From 5339e0876e7c094b07e87f5fadad37ea92ba2b74 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 2 May 2021 22:08:53 +0200 Subject: [PATCH] feat: Adopt the OpenAPI 3.0 standard for the REST API #50 --- CHANGELOG_LATEST.md | 1 + doc/EMS-ESP32 API.md | 48 +-- interface/src/project/EMSESPSettingsForm.tsx | 8 +- interface/src/project/EMSESPtypes.ts | 2 +- lib_standalone/AsyncJson.h | 54 +++ lib_standalone/ESP8266React.h | 2 +- lib_standalone/ESPAsyncWebServer.h | 28 +- mock-api/server.js | 4 +- src/WebAPIService.cpp | 338 ++++++++++++++++--- src/WebAPIService.h | 40 ++- src/WebDevicesService.cpp | 2 +- src/WebSettingsService.cpp | 4 +- src/WebSettingsService.h | 2 +- src/default_settings.h | 4 +- src/emsesp.cpp | 40 ++- src/emsesp.h | 1 + src/version.h | 2 +- 17 files changed, 460 insertions(+), 120 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 0581d30d0..63fff129d 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -21,5 +21,6 @@ - Re-enabled Shower Alert (still experimental) - lowercased Flow temp in commands - system console commands to main +- new secure API ([#50](https://github.com/emsesp/EMS-ESP32/issues/50)) ## Removed diff --git a/doc/EMS-ESP32 API.md b/doc/EMS-ESP32 API.md index 90d51b0f2..1a8e6a6f6 100644 --- a/doc/EMS-ESP32 API.md +++ b/doc/EMS-ESP32 API.md @@ -15,8 +15,8 @@ OpenAPI is an open standard specification for describing REST APIs. From the [Op - Unless explicitly bypassed in the WebUI some operations required admin privileges in the form of an Access Token which can be generated from the Web UI's Security tab. An Access Token is a string 152 characters long. Token's do not expire. The token needs to be either embedded into the HTTP Header as `"Authorization: Bearer {ACCESS_TOKEN}"` or as query parameter `?access_token={ACCESS_TOKEN}`. To test you can use a command line instruction like ```bash - curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -X POST http://ems-esp/api/system/settings - curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -d '{ "name": "wwtemp", "value":60}' http://ems-esp/api/boiler + curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -X GET http://ems-esp/api/system/settings + curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{ "name": "wwtemp", "value":60}' http://ems-esp/api/boiler ``` ## Error handling @@ -36,26 +36,11 @@ OpenAPI is an open standard specification for describing REST APIs. From the [Op {"message":"Problems parsing JSON"} ``` -- Sending the wrong type of JSON values will result in a `400 Bad Request` response. - - ```html - HTTP/1.1 400 Bad Request - {"message":"Body should be a JSON object"} - ``` - -- Sending invalid fields will result in a `422 Unprocessable Entity` response. `code` can be missing, missing_field, invalid or unprocessable. For example when selecting an invalid heating circuit number. +- Sending invalid fields will result in a `422 Unprocessable Entity` response. ```html HTTP/1.1 422 Unprocessable Entity - { - "message": "Validation Failed", - "errors": [ - { - "field": "title", - "code": "missing_field" - } - ] - } + {"message":"Invalid command"} ``` ## Endpoints @@ -80,27 +65,16 @@ OpenAPI is an open standard specification for describing REST APIs. From the [Op | - | - | - | - | - | | GET | `/{device}` | return all device details and values | | | | GET | `/{device}/{name}` | return a specific parameter and all its properties (name, fullname, value, type, min, max, unit, writeable) | | | -| GET | `/device={device}?cmd={name}?data={value}[?id={hc}` | to keep compatibility with v2. Unless bypassed in the EMS-ESP settings make sure you include `access_token={ACCESS_TOKEN}` | x | -| POST/PUT | `/{device}[/{hc}][/{name}]` | sets a specific value to a parameter name. If no hc is selected and one is required for the device, the default will be used | x | `{ "value": [, "hc": {hc}] }` | +| GET | `/device={device}?cmd={name}?data={value}[?hc=` | Using HTTP query parameters. This is to keep compatibility with v2. Unless bypassed in the EMS-ESP settings make sure you include `access_token={ACCESS_TOKEN}` | x | +| POST/PUT | `/{device}[/{hc}][/{name}]` | sets a specific value to a parameter name. If no hc is selected and one is required for the device, the default will be used | x | `{ ["name" : ] , ["hc": ], "value": }` | ## System Endpoints | Method | Endpoint | Description | Access Token required | JSON body data | | - | - | - | - | - | | GET | `/system/info` | list system information | | | | -| GET | `/system/settings` | list all settings, except passwords | x | -| POST/PUT | `/system/pin` | switch a GPIO state to HIGH or LOW | x | `{ "pin":, "value": }` | -| POST/PUT | `/system/send` | send a telegram to the EMS bus | x | `{ "telegram" : }` | -| POST/PUT | `/system/publish` | force an MQTT publish | x | `[{ "name" : \| "ha" }]` | -| POST/PUT | `/system/fetch` | fetch all EMS data from all devices | x | | -| POST/PUT | `/system/restart` | restarts the EMS-ESP | x | | - -## To Do - -- add restart command -- update EMS-ESP wiki/documentation for v3 -- make adjustments to the command line -- change the URLs in the web UI help page to call system commands directly instead of via URLs -- add long name to value query (only shortname is shown) -- create Postman schema -- rename setting "Enable API write commands" to something like "Use non-authenticated API" with a disclaimer that its insecure and not recommended +| GET | `/system/settings` | list all settings, except passwords | | +| POST/PUT | `/system/pin` | switch a GPIO state to HIGH or LOW | x | `{ "id":, "value": }` | +| POST/PUT | `/system/send` | send a telegram to the EMS bus | x | `{ "value" : }` | +| POST/PUT | `/system/publish` | force an MQTT publish | x | `{ "value" : \| "ha" }` | +| POST/PUT | `/system/fetch` | fetch all EMS data from all devices | x | `{ "value" : \| "all" }` | diff --git a/interface/src/project/EMSESPSettingsForm.tsx b/interface/src/project/EMSESPSettingsForm.tsx index 03dace618..d5306e1d3 100644 --- a/interface/src/project/EMSESPSettingsForm.tsx +++ b/interface/src/project/EMSESPSettingsForm.tsx @@ -396,12 +396,12 @@ class EMSESPSettingsForm extends React.Component { } - label="Enable API write commands" + label="Bypass Access Token authorization on API calls" /> ArJsonRequestHandlerFunction; diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index e452a3b1b..d6913e319 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -25,7 +25,7 @@ class DummySettings { bool shower_timer = true; bool shower_alert = false; bool hide_led = false; - bool api_enabled = true; + bool notoken_api = false; // MQTT uint16_t publish_time = 10; // seconds diff --git a/lib_standalone/ESPAsyncWebServer.h b/lib_standalone/ESPAsyncWebServer.h index b5472799e..8237c44f3 100644 --- a/lib_standalone/ESPAsyncWebServer.h +++ b/lib_standalone/ESPAsyncWebServer.h @@ -11,6 +11,7 @@ class AsyncWebServer; class AsyncWebServerRequest; class AsyncWebServerResponse; class AsyncJsonResponse; +class PrettyAsyncJsonResponse; class AsyncWebParameter { private: @@ -68,11 +69,13 @@ class AsyncWebServerRequest { AsyncWebServer * _server; WebRequestMethodComposite _method; + String _url; + public: void * _tempObject; - AsyncWebServerRequest(AsyncWebServer *, AsyncClient *); - ~AsyncWebServerRequest(); + AsyncWebServerRequest(AsyncWebServer *, AsyncClient *){}; + ~AsyncWebServerRequest(){}; AsyncClient * client() { return _client; @@ -82,13 +85,30 @@ class AsyncWebServerRequest { return _method; } + void method(WebRequestMethodComposite method_s) { + _method = method_s; + } + void addInterestingHeader(const String & name){}; + size_t args() const { + return 0; + } + void send(AsyncWebServerResponse * response){}; void send(AsyncJsonResponse * response){}; + void send(PrettyAsyncJsonResponse * response){}; void send(int code, const String & contentType = String(), const String & content = String()){}; void send(int code, const String & contentType, const __FlashStringHelper *){}; + const String & url() const { + return _url; + } + + void url(const String & url_s) { + _url = url_s; + } + bool hasParam(const String & name, bool post, bool file) const { return false; } @@ -174,9 +194,9 @@ class AsyncWebServerResponse { virtual ~AsyncWebServerResponse(); }; -typedef std::function ArRequestHandlerFunction; +typedef std::function ArRequestHandlerFunction; typedef std::function ArUploadHandlerFunction; -typedef std::function ArBodyHandlerFunction; +typedef std::function ArBodyHandlerFunction; class AsyncWebServer { protected: diff --git a/mock-api/server.js b/mock-api/server.js index 3403d362b..07e77e26e 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -103,7 +103,7 @@ const verify_authentication = { access_token: '1234' }; const signin = { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInZlcnNpb24iOiIzLjAuMmIwIn0.MsHSgoJKI1lyYz77EiT5ZN3ECMrb4mPv9FNy3udq0TU" }; -const generate_token = { token: '1234' }; +const generate_token = { token: '1234' }; // EMS-ESP Project specific const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings"; @@ -117,7 +117,7 @@ const emsesp_settings = { "tx_mode": 1, "tx_delay": 0, "ems_bus_id": 11, "syslog_enabled": false, "syslog_level": 3, "trace_raw": false, "syslog_mark_interval": 0, "syslog_host": "192.168.1.4", "syslog_port": 514, "master_thermostat": 0, "shower_timer": true, "shower_alert": false, "rx_gpio": 23, "tx_gpio": 5, - "dallas_gpio": 3, "dallas_parasite": false, "led_gpio": 2, "hide_led": false, "api_enabled": true, + "dallas_gpio": 3, "dallas_parasite": false, "led_gpio": 2, "hide_led": false, "notoken_api": false, "analog_enabled": false, "pbutton_gpio": 0, "board_profile": "S32" }; const emsesp_alldevices = { diff --git a/src/WebAPIService.cpp b/src/WebAPIService.cpp index b98e98db9..035059d24 100644 --- a/src/WebAPIService.cpp +++ b/src/WebAPIService.cpp @@ -16,76 +16,308 @@ * along with this program. If not, see . */ +// SUrlParser from https://github.com/Mad-ness/simple-url-parser + #include "emsesp.h" using namespace std::placeholders; // for `_1` etc namespace emsesp { -WebAPIService::WebAPIService(AsyncWebServer * server) { - server->on(EMSESP_API_SERVICE_PATH, HTTP_GET, std::bind(&WebAPIService::webAPIService, this, _1)); +WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * securityManager) + : _securityManager(securityManager) + , _apiHandler("/api", std::bind(&WebAPIService::webAPIService_post, this, _1, _2), 256) { // for POSTS + server->on("/api", HTTP_GET, std::bind(&WebAPIService::webAPIService_get, this, _1)); // for GETS + server->addHandler(&_apiHandler); } -// e.g. http://ems-esp/api?device=boiler&cmd=wwtemp&data=20&id=1 -void WebAPIService::webAPIService(AsyncWebServerRequest * request) { - // must have device and cmd parameters - if ((!request->hasParam(F_(device))) || (!request->hasParam(F_(cmd)))) { - request->send(400, "text/plain", F("Invalid syntax")); +// GET /{device} +// GET /{device}/{name} +// GET /device={device}?cmd={name}?data={value}[?id={hc} +void WebAPIService::webAPIService_get(AsyncWebServerRequest * request) { + std::string device(""); + std::string cmd(""); + int id = -1; + std::string value(""); + + parse(request, device, cmd, id, value); // pass it defaults +} + +// For POSTS with an optional JSON body +// HTTP_POST | HTTP_PUT | HTTP_PATCH +// POST/PUT /{device}[/{hc}][/{name}] +void WebAPIService::webAPIService_post(AsyncWebServerRequest * request, JsonVariant & json) { + // extra the params from the json body + if (not json.is()) { + webAPIService_get(request); return; } - // get device - String device = request->getParam(F_(device))->value(); - uint8_t device_type = EMSdevice::device_name_2_device_type(device.c_str()); - if (device_type == emsesp::EMSdevice::DeviceType::UNKNOWN) { - request->send(400, "text/plain", F("Invalid device")); - return; - } + // extract values from the json + // these will be used as default values + auto && body = json.as(); - // get cmd, we know we have one - String cmd = request->getParam(F_(cmd))->value(); - - String data; - if (request->hasParam(F_(data))) { - data = request->getParam(F_(data))->value(); - } - - String id; - if (request->hasParam(F_(id))) { - id = request->getParam(F_(id))->value(); - } - - if (id.isEmpty()) { - id = "-1"; - } - - DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); - JsonObject json = doc.to(); - bool ok = false; - - // execute the command - if (data.isEmpty()) { - ok = Command::call(device_type, cmd.c_str(), nullptr, id.toInt(), json); // command only + std::string device = body["name"].as(); // note this was called device in the v2 + std::string cmd = body["cmd"].as(); + int id = -1; + if (body.containsKey("id")) { + id = body["id"]; + } else if (body.containsKey("hc")) { + id = body["hc"]; } else { - // we only allow commands with parameters if the API is enabled - bool api_enabled; - EMSESP::webSettingsService.read([&](WebSettings & settings) { api_enabled = settings.api_enabled; }); - if (api_enabled) { - ok = Command::call(device_type, cmd.c_str(), data.c_str(), id.toInt(), json); // has cmd, data and id - } else { - request->send(401, "text/plain", F("Unauthorized")); + id = -1; + } + + // make sure we have a value. There must always be a value + if (!body.containsKey("value")) { + send_message_response(request, 400, "Problems parsing JSON"); // Bad Request + return; + } + std::string value = body["value"].as(); // always convert value to string + + // now parse the URL. The URL is always leading and will overwrite anything provided in the json body + parse(request, device, cmd, id, value); // pass it defaults +} + +// parse the URL looking for query or path parameters +// reporting back any errors +void WebAPIService::parse(AsyncWebServerRequest * request, std::string & device_s, std::string & cmd_s, int id, std::string & value_s) { +#ifndef EMSESP_STANDALONE + // parse URL for the path names + SUrlParser p; + p.parse(request->url().c_str()); + + // remove the /api from the path + if (p.paths().front() == "api") { + p.paths().erase(p.paths().begin()); + } else { + return; // bad URL + } + + uint8_t device_type; + int8_t id_n = -1; // default hc + + // check for query parameters first + // /device={device}?cmd={name}?data={value}[?id={hc} + if (p.paths().size() == 0) { + // get the device + if (request->hasParam(F_(device))) { + device_s = request->getParam(F_(device))->value().c_str(); + } + + // get cmd + if (request->hasParam(F_(cmd))) { + cmd_s = request->getParam(F_(cmd))->value().c_str(); + } + + // get data, which is optional. This is now replaced with the name 'value' in JSON body + if (request->hasParam(F_(data))) { + value_s = request->getParam(F_(data))->value().c_str(); + } + if (request->hasParam("value")) { + value_s = request->getParam("value")->value().c_str(); + } + + // get id (or hc), which is optional + if (request->hasParam(F_(id))) { + id_n = Helpers::atoint(request->getParam(F_(id))->value().c_str()); + } + if (request->hasParam("hc")) { + id_n = Helpers::atoint(request->getParam("hc")->value().c_str()); + } + } else { + // parse paths and json data + // /{device}[/{hc}][/{name}] + // first param must be a valid device, which includes "system" + device_s = p.paths().front(); + device_type = EMSdevice::device_name_2_device_type(device_s.c_str()); + + // if there are no more paths parameters, default to 'info' + auto num_paths = p.paths().size(); + if (num_paths > 1) { + auto path2 = p.paths()[1]; // get next path + // if it's a system, the next path must be a command (info, settings,...) + if (device_type == EMSdevice::DeviceType::SYSTEM) { + cmd_s = path2; + } else { + // it's an EMS device + // path2 could be a hc which is optional or a name. first check if it's a hc + if (path2.substr(0, 2) == "hc") { + id_n = (byte)path2[2] - '0'; // bit of a hack + // there must be a name following + if (num_paths > 2) { + cmd_s = p.paths()[2]; + } + } else { + cmd_s = path2; + } + } + } + } + + // now go and validate everything + + // device check + if (device_s.empty()) { + send_message_response(request, 422, "Missing device"); // Unprocessable Entity + return; + } + device_type = EMSdevice::device_name_2_device_type(device_s.c_str()); + if (device_type == EMSdevice::DeviceType::UNKNOWN) { + send_message_response(request, 422, "Invalid device"); // Unprocessable Entity + return; + } + + // cmd check + // if the cmd is empty, default it 'info' + if (cmd_s.empty()) { + cmd_s = "info"; + } + if (Command::find_command(device_type, cmd_s.c_str()) == nullptr) { + send_message_response(request, 422, "Invalid cmd"); // Unprocessable Entity + return; + } + + // check that we have permissions first. We require authenticating on 1 or more of these conditions: + // 1. any HTTP POSTs or PUTs + // 2. a HTTP GET which has a 'data' parameter which is not empty (to keep v2 compatibility) + auto method = request->method(); + bool have_data = !value_s.empty(); + bool admin_allowed; + EMSESP::webSettingsService.read([&](WebSettings & settings) { + Authentication authentication = _securityManager->authenticateRequest(request); + admin_allowed = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication); + }); + + if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) { + if (!admin_allowed) { + send_message_response(request, 401, "Bad credentials"); // Unauthorized return; } } - if (ok && json.size()) { - // send json output back to web - doc.shrinkToFit(); - std::string buffer; - serializeJsonPretty(doc, buffer); - request->send(200, "text/plain;charset=utf-8", buffer.c_str()); + + // now we have all the parameters go and execute the command + PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN); + JsonObject json = response->getRoot(); + + // EMSESP::logger().notice("Calling device=%s, cmd=%s, data=%s, id/hc=%d", device_s.c_str(), cmd_s.c_str(), value_s.c_str(), id_n); + bool ok = Command::call(device_type, cmd_s.c_str(), (have_data ? value_s.c_str() : nullptr), id_n, json); + + // check for errors + if (!ok) { + send_message_response(request, 400, "Problems parsing elements"); // Bad Request return; } - request->send(200, "text/plain", ok ? F("OK") : F("Invalid")); + + if (!json.size()) { + send_message_response(request, 200, "OK"); // OK + return; + } + + // send the json that came back from the command call + response->setLength(); + request->send(response); // send json response + +#endif } -} // namespace emsesp \ No newline at end of file +// send a HTTP error back, with optional JSON body data +void WebAPIService::send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * error_message) { + if (error_message == nullptr) { + AsyncWebServerResponse * response = request->beginResponse(error_code); // just send the code + request->send(response); + } else { + // build a return message and send it + PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL); + JsonObject json = response->getRoot(); + json["message"] = error_message; + response->setCode(error_code); + response->setLength(); + response->setContentType("application/json"); + request->send(response); + } +} + +/** + * Extract only the path component from the passed URI + * and normalized it. + * Ex. //one/two////three/// + * becomes + * /one/two/three + */ +std::string SUrlParser::path() { + std::string s = "/"; // set up the beginning slash + for (std::string & f : m_folders) { + s += f; + s += "/"; + } + s.pop_back(); // deleting last letter, that is slash '/' + return std::string(s); +} + +SUrlParser::SUrlParser(const char * uri) { + parse(uri); +} + +bool SUrlParser::parse(const char * uri) { + m_folders.clear(); + m_keysvalues.clear(); + enum Type { begin, folder, param, value }; + std::string s; + + const char * c = uri; + enum Type t = Type::begin; + std::string last_param; + + if (c != NULL || *c != '\0') { + do { + if (*c == '/') { + if (s.length() > 0) { + m_folders.push_back(s); + s.clear(); + } + t = Type::folder; + } else if (*c == '?' && (t == Type::folder || t == Type::begin)) { + if (s.length() > 0) { + m_folders.push_back(s); + s.clear(); + } + t = Type::param; + } else if (*c == '=' && (t == Type::param || t == Type::begin)) { + m_keysvalues[s] = ""; + last_param = s; + s.clear(); + t = Type::value; + } else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) { + if (t == Type::value) { + m_keysvalues[last_param] = s; + } else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) { + m_keysvalues[s] = ""; + last_param = s; + } + t = Type::param; + s.clear(); + } else if (*c == '\0' && s.length() > 0) { + if (t == Type::value) { + m_keysvalues[last_param] = s; + } else if (t == Type::folder || t == Type::begin) { + m_folders.push_back(s); + } else if (t == Type::param) { + m_keysvalues[s] = ""; + last_param = s; + } + s.clear(); + } else if (*c == '\0' && s.length() == 0) { + if (t == Type::param && last_param.length() > 0) { + m_keysvalues[last_param] = ""; + } + s.clear(); + } else { + s += *c; + } + } while (*c++ != '\0'); + } + return true; +} + +} // namespace emsesp diff --git a/src/WebAPIService.h b/src/WebAPIService.h index dd932fa20..13fd5aa66 100644 --- a/src/WebAPIService.h +++ b/src/WebAPIService.h @@ -23,16 +23,52 @@ #include #include +#include +#include +#include + #define EMSESP_API_SERVICE_PATH "/api" namespace emsesp { +typedef std::unordered_map KeyValueMap_t; +typedef std::vector Folder_t; + +class SUrlParser { + private: + KeyValueMap_t m_keysvalues; + Folder_t m_folders; + + public: + SUrlParser(){}; + SUrlParser(const char * url); + + bool parse(const char * url); + + Folder_t & paths() { + return m_folders; + }; + + KeyValueMap_t & params() { + return m_keysvalues; + }; + + std::string path(); +}; + class WebAPIService { public: - WebAPIService(AsyncWebServer * server); + WebAPIService(AsyncWebServer * server, SecurityManager * securityManager); private: - void webAPIService(AsyncWebServerRequest * request); + SecurityManager * _securityManager; + AsyncCallbackJsonWebHandler _apiHandler; // for POSTs + + void webAPIService_post(AsyncWebServerRequest * request, JsonVariant & json); // for POSTs + void webAPIService_get(AsyncWebServerRequest * request); // for GETs + + void parse(AsyncWebServerRequest * request, std::string & device, std::string & cmd, int id, std::string & value); + void send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * error_message = nullptr); }; } // namespace emsesp diff --git a/src/WebDevicesService.cpp b/src/WebDevicesService.cpp index e16d50b8d..bf70cc8e3 100644 --- a/src/WebDevicesService.cpp +++ b/src/WebDevicesService.cpp @@ -110,7 +110,7 @@ void WebDevicesService::device_data(AsyncWebServerRequest * request, JsonVariant void WebDevicesService::write_value(AsyncWebServerRequest * request, JsonVariant & json) { // only issue commands if the API is enabled EMSESP::webSettingsService.read([&](WebSettings & settings) { - if (!settings.api_enabled) { + if (!settings.notoken_api) { request->send(403); // forbidden error return; } diff --git a/src/WebSettingsService.cpp b/src/WebSettingsService.cpp index f1a4d2396..9302e88ff 100644 --- a/src/WebSettingsService.cpp +++ b/src/WebSettingsService.cpp @@ -55,7 +55,7 @@ void WebSettings::read(WebSettings & settings, JsonObject & root) { root["dallas_parasite"] = settings.dallas_parasite; root["led_gpio"] = settings.led_gpio; root["hide_led"] = settings.hide_led; - root["api_enabled"] = settings.api_enabled; + root["notoken_api"] = settings.notoken_api; root["analog_enabled"] = settings.analog_enabled; root["pbutton_gpio"] = settings.pbutton_gpio; root["board_profile"] = settings.board_profile; @@ -169,7 +169,7 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings) settings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT; // doesn't need any follow-up actions - settings.api_enabled = root["api_enabled"] | EMSESP_DEFAULT_API_ENABLED; + settings.notoken_api = root["notoken_api"] | EMSESP_DEFAULT_NOTOKEN_API; return StateUpdateResult::CHANGED; } diff --git a/src/WebSettingsService.h b/src/WebSettingsService.h index 2af02dcd6..38e7ae785 100644 --- a/src/WebSettingsService.h +++ b/src/WebSettingsService.h @@ -50,7 +50,7 @@ class WebSettings { bool dallas_parasite; uint8_t led_gpio; bool hide_led; - bool api_enabled; + bool notoken_api; bool analog_enabled; uint8_t pbutton_gpio; String board_profile; diff --git a/src/default_settings.h b/src/default_settings.h index d5b442127..ab7520207 100644 --- a/src/default_settings.h +++ b/src/default_settings.h @@ -76,8 +76,8 @@ #define EMSESP_DEFAULT_DALLAS_PARASITE false #endif -#ifndef EMSESP_DEFAULT_API_ENABLED -#define EMSESP_DEFAULT_API_ENABLED false // turn off, because its insecure +#ifndef EMSESP_DEFAULT_NOTOKEN_API +#define EMSESP_DEFAULT_NOTOKEN_API false #endif #ifndef EMSESP_DEFAULT_BOOL_FORMAT diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 9972933e1..20f3bf936 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -39,7 +39,7 @@ WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, & WebStatusService EMSESP::webStatusService = WebStatusService(&webServer, EMSESP::esp8266React.getSecurityManager()); WebDevicesService EMSESP::webDevicesService = WebDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager()); -WebAPIService EMSESP::webAPIService = WebAPIService(&webServer); +WebAPIService EMSESP::webAPIService = WebAPIService(&webServer, EMSESP::esp8266React.getSecurityManager()); using DeviceFlags = EMSdevice; using DeviceType = EMSdevice::DeviceType; @@ -90,6 +90,15 @@ void EMSESP::fetch_device_values(const uint8_t device_id) { } } +// for a specific EMS device type go and request data values +void EMSESP::fetch_device_values_type(const uint8_t device_type) { + for (const auto & emsdevice : emsdevices) { + if ((emsdevice) && (emsdevice->device_type() == device_type)) { + emsdevice->fetch_values(); + } + } +} + // clears list of recognized devices void EMSESP::clear_all_devices() { // temporary removed: clearing the list causes a crash, the associated commands and mqtt should also be removed. @@ -744,7 +753,8 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { } read_next_ = false; } else if (watch() == WATCH_ON) { - if ((watch_id_ == WATCH_ID_NONE) || (telegram->type_id == watch_id_) || ((watch_id_ < 0x80) && ((telegram->src == watch_id_) || (telegram->dest == watch_id_)))) { + if ((watch_id_ == WATCH_ID_NONE) || (telegram->type_id == watch_id_) + || ((watch_id_ < 0x80) && ((telegram->src == watch_id_) || (telegram->dest == watch_id_)))) { LOG_NOTICE(pretty_telegram(telegram).c_str()); } else if (!trace_raw_) { LOG_TRACE(pretty_telegram(telegram).c_str()); @@ -785,7 +795,8 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { found = emsdevice->handle_telegram(telegram); // if we correctly processes the telegram follow up with sending it via MQTT if needed if (found && Mqtt::connected()) { - if ((mqtt_.get_publish_onchange(emsdevice->device_type()) && emsdevice->has_update()) || (telegram->type_id == publish_id_ && telegram->dest == txservice_.ems_bus_id())) { + if ((mqtt_.get_publish_onchange(emsdevice->device_type()) && emsdevice->has_update()) + || (telegram->type_id == publish_id_ && telegram->dest == txservice_.ems_bus_id())) { if (telegram->type_id == publish_id_) { publish_id_ = 0; } @@ -803,7 +814,8 @@ bool EMSESP::process_telegram(std::shared_ptr telegram) { if (watch() == WATCH_UNKNOWN) { LOG_NOTICE(pretty_telegram(telegram).c_str()); } - if (first_scan_done_ && !knowndevice && (telegram->src != EMSbus::ems_bus_id()) && (telegram->src != 0x0B) && (telegram->src != 0x0C) && (telegram->src != 0x0D)) { + if (first_scan_done_ && !knowndevice && (telegram->src != EMSbus::ems_bus_id()) && (telegram->src != 0x0B) && (telegram->src != 0x0C) + && (telegram->src != 0x0D)) { send_read_request(EMSdevice::EMS_TYPE_VERSION, telegram->src); } } @@ -899,7 +911,8 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std:: // sometimes boilers share the same product id as controllers // so only add boilers if the device_id is 0x08, which is fixed for EMS if (device.device_type == DeviceType::BOILER) { - if (device_id == EMSdevice::EMS_DEVICE_ID_BOILER || (device_id >= EMSdevice::EMS_DEVICE_ID_BOILER_1 && device_id <= EMSdevice::EMS_DEVICE_ID_BOILER_F)) { + if (device_id == EMSdevice::EMS_DEVICE_ID_BOILER + || (device_id >= EMSdevice::EMS_DEVICE_ID_BOILER_1 && device_id <= EMSdevice::EMS_DEVICE_ID_BOILER_F)) { device_p = &device; break; } @@ -915,7 +928,8 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std:: if (device_p == nullptr) { LOG_NOTICE(F("Unrecognized EMS device (device ID 0x%02X, product ID %d). Please report on GitHub."), device_id, product_id); std::string name("unknown"); - emsdevices.push_back(EMSFactory::add(DeviceType::GENERIC, device_id, product_id, version, name, DeviceFlags::EMS_DEVICE_FLAG_NONE, EMSdevice::Brand::NO_BRAND)); + emsdevices.push_back( + EMSFactory::add(DeviceType::GENERIC, device_id, product_id, version, name, DeviceFlags::EMS_DEVICE_FLAG_NONE, EMSdevice::Brand::NO_BRAND)); return false; // not found } @@ -933,7 +947,9 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std:: return true; } - Command::add_with_json(device_type, F_(info), [device_type](const char * value, const int8_t id, JsonObject & json) { return command_info(device_type, json, id); }); + Command::add_with_json(device_type, F_(info), [device_type](const char * value, const int8_t id, JsonObject & json) { + return command_info(device_type, json, id); + }); return true; } @@ -952,7 +968,8 @@ bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t i } for (const auto & emsdevice : emsdevices) { - if (emsdevice && (emsdevice->device_type() == device_type) && ((device_type != DeviceType::THERMOSTAT) || (emsdevice->device_id() == EMSESP::actual_master_thermostat()))) { + if (emsdevice && (emsdevice->device_type() == device_type) + && ((device_type != DeviceType::THERMOSTAT) || (emsdevice->device_id() == EMSESP::actual_master_thermostat()))) { has_value |= emsdevice->generate_values_json(json, tag, (id < 1), (id == -1)); // nested for id -1,0 & console for id -1 } } @@ -966,7 +983,12 @@ void EMSESP::send_read_request(const uint16_t type_id, const uint8_t dest, const } // sends write request -void EMSESP::send_write_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid) { +void EMSESP::send_write_request(const uint16_t type_id, + const uint8_t dest, + const uint8_t offset, + uint8_t * message_data, + const uint8_t message_length, + const uint16_t validate_typeid) { txservice_.add(Telegram::Operation::TX_WRITE, dest, type_id, offset, message_data, message_length, validate_typeid, true); } diff --git a/src/emsesp.h b/src/emsesp.h index 6d1e4ac05..40dc7aa13 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -178,6 +178,7 @@ class EMSESP { } static void fetch_device_values(const uint8_t device_id = 0); + static void fetch_device_values_type(const uint8_t device_type); static bool add_device(const uint8_t device_id, const uint8_t product_id, std::string & version, const uint8_t brand); static void scan_devices(); diff --git a/src/version.h b/src/version.h index 587ba7833..cb113bc50 100644 --- a/src/version.h +++ b/src/version.h @@ -1,2 +1,2 @@ -#define EMSESP_APP_VERSION "3.0.3b4" +#define EMSESP_APP_VERSION "3.0.3b5" #define EMSESP_PLATFORM "ESP32"