diff --git a/src/CHANGES.md b/src/CHANGES.md index 4b6eda8e..9835873c 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -2,6 +2,7 @@ ## 0.8.70 - 2024-02-01 * prevent sending commands to inverter which isn't active #1387 +* protect commands from popup in `/live` if password is set #1199 ## 0.8.69 - 2024-01-31 * merge PR: Dynamic retries, pendular first rx chan #1394 diff --git a/src/app.cpp b/src/app.cpp index a14a8811..8b770989 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -97,9 +97,8 @@ void app::setup() { esp_task_wdt_reset(); mWeb.setup(this, &mSys, mConfig); - mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); - mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); + mProtection = Protection::getInstance(mConfig->sys.adminPwd); #ifdef ENABLE_SYSLOG mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback) @@ -182,6 +181,8 @@ void app::onNetwork(bool gotIp) { void app::regularTickers(void) { DPRINTLN(DBG_DEBUG, F("regularTickers")); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); + everySec([this]() { mProtection->tickSecond(); }, "prot"); + // Plugins #if defined(PLUGIN_DISPLAY) if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type) diff --git a/src/app.h b/src/app.h index ccf46297..b44d3781 100644 --- a/src/app.h +++ b/src/app.h @@ -30,6 +30,7 @@ #include "utils/scheduler.h" #include "utils/syslog.h" #include "web/RestApi.h" +#include "web/Protection.h" #if defined(ENABLE_HISTORY) #include "plugins/history.h" #endif /*ENABLE_HISTORY*/ @@ -246,8 +247,24 @@ class app : public IApp, public ah::Scheduler { #endif } - bool getProtection(AsyncWebServerRequest *request) { - return mWeb.isProtected(request); + void lock(void) override { + mProtection->lock(); + } + + void unlock(const char *clientIp) override { + mProtection->unlock(clientIp); + } + + void resetLockTimeout(void) override { + mProtection->resetLockTimeout(); + } + + bool isProtected(void) const override { + return mProtection->isProtected(); + } + + bool isProtected(const char *clientIp) const override { + return mProtection->isProtected(clientIp); } bool getNrfEnabled(void) { @@ -387,6 +404,7 @@ class app : public IApp, public ah::Scheduler { #endif /* defined(ETHERNET) */ WebType mWeb; RestApiType mApi; + Protection *mProtection; #ifdef ENABLE_SYSLOG DbgSyslog mDbgSyslog; #endif diff --git a/src/appInterface.h b/src/appInterface.h index f98fb0cb..9930c51e 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -61,7 +61,11 @@ class IApp { virtual uint32_t getMqttRxCnt() = 0; virtual uint32_t getMqttTxCnt() = 0; - virtual bool getProtection(AsyncWebServerRequest *request) = 0; + virtual void lock(void) = 0; + virtual void unlock(const char *clientIp) = 0; + virtual void resetLockTimeout(void) = 0; + virtual bool isProtected(void) const = 0; + virtual bool isProtected(const char *clientIp) const = 0; virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; virtual uint16_t getHistoryMaxDay() = 0; diff --git a/src/defines.h b/src/defines.h index a4a1d6bd..6d350add 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 69 +#define VERSION_PATCH 70 //------------------------------------- typedef struct { diff --git a/src/utils/spiPatcher.cpp b/src/utils/spiPatcher.cpp index 0470b476..3b7b5681 100644 --- a/src/utils/spiPatcher.cpp +++ b/src/utils/spiPatcher.cpp @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #if defined(ESP32) diff --git a/src/utils/spiPatcher.h b/src/utils/spiPatcher.h index 14cb138c..2f8ce616 100644 --- a/src/utils/spiPatcher.h +++ b/src/utils/spiPatcher.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __SPI_PATCHER_H__ diff --git a/src/web/Protection.cpp b/src/web/Protection.cpp new file mode 100644 index 00000000..b822fb6a --- /dev/null +++ b/src/web/Protection.cpp @@ -0,0 +1,7 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#include "Protection.h" +Protection *Protection::mInstance = nullptr; diff --git a/src/web/Protection.h b/src/web/Protection.h new file mode 100644 index 00000000..44926752 --- /dev/null +++ b/src/web/Protection.h @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __PROTECTION_H__ +#define __PROTECTION_H__ +#pragma once + +#include +#include + +#include "../config/config.h" +#include "../utils/helper.h" + +class Protection { + protected: + Protection(const char *pwd) { + mPwd = pwd; + mLogoutTimeout = 0; + mLoginIp.fill(0); + + // no password set - unlock + if(pwd[0] == '\0') + mProtected = false; + } + + public: + Protection(Protection &other) = delete; + void operator=(const Protection &) = delete; + + static Protection* getInstance(const char *pwd) { + if(nullptr == mInstance) + mInstance = new Protection(pwd); + return mInstance; + } + + void tickSecond() { + // auto logout + if(0 != mLogoutTimeout) { + if (0 == --mLogoutTimeout) { + if(mPwd[0] == '\0') + mProtected = true; + } + } + } + + void lock(void) { + mProtected = true; + mLoginIp.fill(0); + } + + void unlock(const char *clientIp) { + mProtected = false; + ah::ip2Arr(static_cast(mLoginIp.data()), clientIp); + } + + void resetLockTimeout(void) { + mLogoutTimeout = LOGOUT_TIMEOUT; + } + + bool isProtected(void) const { + return mProtected; + } + + bool isProtected(const char *clientIp) const { + if(mProtected) + return true; + + if(mPwd[0] == '\0') + return false; + + uint8_t ip[4]; + ah::ip2Arr(ip, clientIp); + for(uint8_t i = 0; i < 4; i++) { + if(mLoginIp[i] != ip[i]) + return true; + } + + return true; + } + + protected: + static Protection *mInstance; + + private: + const char *mPwd; + bool mProtected = true; + uint16_t mLogoutTimeout = LOGOUT_TIMEOUT; + std::array mLoginIp; +}; + +#endif /*__PROTECTION_H__*/ diff --git a/src/web/RestApi.h b/src/web/RestApi.h index ccdc8747..5438566c 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -68,7 +68,7 @@ class RestApi { DynamicJsonDocument json(128); JsonObject dummy = json.as(); if(obj[F("path")] == "ctrl") - setCtrl(obj, dummy); + setCtrl(obj, dummy, "api"); else if(obj[F("path")] == "setup") setSetup(obj, dummy); } @@ -168,7 +168,7 @@ class RestApi { if(!err) { String path = request->url().substring(5); if(path == "ctrl") - root[F("success")] = setCtrl(obj, root); + root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str()); else if(path == "setup") root[F("success")] = setSetup(obj, root); else { @@ -263,7 +263,7 @@ class RestApi { obj[F("modules")] = String(mApp->getVersionModules()); obj[F("build")] = String(AUTO_GIT_HASH); obj[F("env")] = String(ENV_NAME); - obj[F("menu_prot")] = mApp->getProtection(request); + obj[F("menu_prot")] = mApp->isProtected(request->client()->remoteIP().toString().c_str()); obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); obj[F("cst_lnk")] = String(mConfig->plugin.customLink); @@ -847,7 +847,7 @@ class RestApi { } - bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { + bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); bool accepted = true; if(NULL == iv) { @@ -856,6 +856,13 @@ class RestApi { } jsonOut[F("id")] = jsonIn[F("id")]; + if(strncmp("api", clientIP, 3) != 0) { + if(mApp->isProtected(clientIP)) { + jsonOut[F("error")] = F(INV_IS_PROTECTED); + return false; + } + } + if(F("power") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); else if(F("restart") == jsonIn[F("cmd")]) diff --git a/src/web/lang.h b/src/web/lang.h index e55493eb..6cddbc12 100644 --- a/src/web/lang.h +++ b/src/web/lang.h @@ -36,6 +36,12 @@ #define UNKNOWN_CMD "unknown cmd: '" #endif +#ifdef LANG_DE + #define INV_IS_PROTECTED "nicht angemeldet, Kommando nicht möglich!" +#else /*LANG_EN*/ + #define INV_IS_PROTECTED "not logged in, command not possible!" +#endif + #ifdef LANG_DE #define INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT "Leistungsbegrenzung / Ansteuerung aktuell nicht möglich" #else /*LANG_EN*/ diff --git a/src/web/web.h b/src/web/web.h index 6de9c877..01e48f74 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -47,9 +47,6 @@ template class Web { public: Web(void) : mWeb(80), mEvts("/events") { - mProtected = true; - mLogoutTimeout = 0; - memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); mSerialBufFill = 0; mSerialAddTime = true; @@ -110,16 +107,6 @@ class Web { } void tickSecond() { - if (0 != mLogoutTimeout) { - mLogoutTimeout -= 1; - if (0 == mLogoutTimeout) { - if (strlen(mConfig->sys.adminPwd) > 0) - mProtected = true; - } - - DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout)); - } - if (mSerialClientConnnected) { if (mSerialBufFill > 0) { mEvts.send(mSerialBuf, "serial", millis()); @@ -133,27 +120,6 @@ class Web { return &mWeb; } - void setProtection(bool protect) { - mProtected = protect; - } - - bool isProtected(AsyncWebServerRequest *request) { - bool prot; - prot = mProtected; - if(!prot) { - if(strlen(mConfig->sys.adminPwd) > 0) { - uint8_t ip[4]; - ah::ip2Arr(ip, request->client()->remoteIP().toString().c_str()); - for(uint8_t i = 0; i < 4; i++) { - if(mLoginIp[i] != ip[i]) - prot = true; - } - } - } - - return prot; - } - void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if (!index) { Serial.printf("Update Start: %s\n", filename.c_str()); @@ -264,7 +230,7 @@ class Web { } void checkProtection(AsyncWebServerRequest *request) { - if(isProtected(request)) { + if(mApp->isProtected(request->client()->remoteIP().toString().c_str())) { checkRedirect(request); return; } @@ -351,8 +317,7 @@ class Web { if (request->args() > 0) { if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { - mProtected = false; - ah::ip2Arr(mLoginIp, request->client()->remoteIP().toString().c_str()); + mApp->unlock(request->client()->remoteIP().toString().c_str()); request->redirect("/"); } } @@ -366,8 +331,7 @@ class Web { DPRINTLN(DBG_VERBOSE, F("onLogout")); checkProtection(request); - - mProtected = true; + mApp->lock(); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); response->addHeader(F("Content-Encoding"), "gzip"); @@ -390,7 +354,6 @@ class Web { void onCss(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("onCss")); - mLogoutTimeout = LOGOUT_TIMEOUT; AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); response->addHeader(F("Content-Encoding"), "gzip"); if(request->hasParam("v")) { @@ -424,6 +387,7 @@ class Web { AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len); response->addHeader(F("Content-Encoding"), "gzip"); request->send(response); + mApp->resetLockTimeout(); } void showNotFound(AsyncWebServerRequest *request) { @@ -495,7 +459,7 @@ class Web { // protection if (request->arg("adminpwd") != "{PWD}") { request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); - mProtected = (strlen(mConfig->sys.adminPwd) > 0); + mApp->lock(); } mConfig->sys.protectionMask = 0x0000; for (uint8_t i = 0; i < 7; i++) { @@ -945,9 +909,6 @@ class Web { #endif AsyncWebServer mWeb; AsyncEventSource mEvts; - bool mProtected; - uint32_t mLogoutTimeout; - uint8_t mLoginIp[4]; IApp *mApp; HMSYSTEM *mSys;