From 39a86b04112422834457133eb153344442d84681 Mon Sep 17 00:00:00 2001 From: rjwats Date: Sat, 1 Feb 2020 08:44:26 +0000 Subject: [PATCH] External config Allow config to be accessed from outside the framework core code. --- README.md | 75 ++++++++++++++++---- lib/framework/APSettingsService.cpp | 31 ++++---- lib/framework/APSettingsService.h | 15 ++-- lib/framework/AdminSettingsService.h | 9 +-- lib/framework/AuthenticationService.cpp | 6 +- lib/framework/ESP8266React.h | 23 +++++- lib/framework/NTPSettingsService.cpp | 32 ++++----- lib/framework/NTPSettingsService.h | 15 ++-- lib/framework/OTASettingsService.cpp | 20 +++--- lib/framework/OTASettingsService.h | 12 ++-- lib/framework/SecurityManager.cpp | 68 ------------------ lib/framework/SecurityManager.h | 65 +++++------------ lib/framework/SecuritySettingsService.cpp | 84 +++++++++++++++++++--- lib/framework/SecuritySettingsService.h | 27 ++++++- lib/framework/SettingsService.h | 86 ++++++++++++++++++++++- lib/framework/WiFiSettingsService.cpp | 58 +++++++-------- lib/framework/WiFiSettingsService.h | 32 +++++---- src/DemoProject.cpp | 6 +- src/DemoProject.h | 9 ++- 19 files changed, 422 insertions(+), 251 deletions(-) delete mode 100644 lib/framework/SecurityManager.cpp diff --git a/README.md b/README.md index 890afcf..4c8e3a5 100644 --- a/README.md +++ b/README.md @@ -349,10 +349,22 @@ Alternatively you can extend [AdminSettingsService.h](lib/framework/AdminSetting ## Extending the framework +It is recommend that you explore the framework code to gain a better understanding of how to use it's features. The framework provides APIs so you can add your own services or features or, if required, directly configure or observe changes to core framework features. Some of these capabilities are detailed below. + +### Adding a service with persistant settings + +The following code demonstrates how you might extend the framework with a feature which requires a username and password to be configured to drive an unspecified feature. + ```cpp #include -class ExampleSettingsService : public SettingsService { +class ExampleSettings { + public: + String username; + String password; +}; + +class ExampleSettingsService : public SettingsService { public: @@ -364,20 +376,15 @@ class ExampleSettingsService : public SettingsService { protected: void readFromJsonObject(JsonObject& root) { - _username = root["username"] | ""; - _password = root["password"] | ""; + _settings.username = root["username"] | ""; + _settings.password = root["password"] | ""; } void writeToJsonObject(JsonObject& root) { - root["username"] = _username; - root["password"] = _password; + root["username"] = _settings.username; + root["password"] = _settings.password; } - private: - - String _username; - String _password; - }; ``` @@ -391,7 +398,7 @@ exampleSettingsService.begin(); There will now be a REST service exposed on "/exampleSettings" for reading and writing (GET/POST) the settings. Any modifications will be persisted in SPIFFS, in this case to "/config/exampleSettings.json" -Sometimes you need to perform an action when the settings are updated, you can achieve this by overriding the onConfigUpdated() function which gets called every time the settings are updated. You can also perform an action when the service starts by overriding the begin() function, being sure to call SettingsService::begin(): +Sometimes you need to perform an action when the settings are updated, you can achieve this by overriding the onConfigUpdated() function which gets called every time the settings are updated. You can also perform an action when the service starts by overriding the begin() function, being sure to call SettingsService::begin(). You can also provide a "loop" function in order to allow your service class continuously perform an action, calling this from the main loop. ```cpp @@ -409,6 +416,50 @@ void reconfigureTheService() { // do whatever is required to react to the new settings } +void loop() { + // execute somthing as part of the main loop +} + +``` + +### Accessing settings and services + +The framework supplies access to it's SettingsService instances and the SecurityManager via getter functions: + +SettingsService | Description +---------------------------- | ---------------------------------------------- +getSecurityManager() | The security manager - detailed above +getSecuritySettingsService() | Configures the users and other security settings +getWiFiSettingsService() | Configures and manages the WiFi network connection +getAPSettingsService() | Configures and manages the Access Point +getNTPSettingsService() | Configures and manages the network time +getOTASettingsService() | Configures and manages the Over-The-Air update feature + +These can be used to observe changes to settings. They can also be used to fetch or update settings directly via objects, JSON strings and JsonObjects. Here are some examples of how you may use this. + +Inspect the current WiFi settings: + +```cpp +WiFiSettings wifiSettings = esp8266React.getWiFiSettingsService()->fetch(); +Serial.print("The ssid is:"); +Serial.println(wifiSettings.ssid); +``` + +Configure the SSID and password: + +```cpp +WiFiSettings wifiSettings = esp8266React->getWiFiSettingsService()->fetch(); +wifiSettings.ssid = "MyNetworkSSID"; +wifiSettings.password = "MySuperSecretPassword"; +esp8266React.getWiFiSettingsService()->update(wifiSettings); +``` + +Observe changes to the WiFiSettings: + +```cpp +esp8266React.getWiFiSettingsService()->addUpdateHandler([]() { + Serial.println("The WiFi Settings were updated!"); +}); ``` ## Libraries Used @@ -416,7 +467,5 @@ void reconfigureTheService() { * [React](https://reactjs.org/) * [Material-UI](https://material-ui-next.com/) * [notistack](https://github.com/iamhosseindhv/notistack) -* [Time](https://github.com/PaulStoffregen/Time) -* [NtpClient](https://github.com/gmag11/NtpClient) * [ArduinoJson](https://github.com/bblanchon/ArduinoJson) * [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index 3c5c4bf..8fc0ebf 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -9,7 +9,11 @@ APSettingsService::~APSettingsService() { void APSettingsService::begin() { SettingsService::begin(); - onConfigUpdated(); + reconfigureAP(); +} + +void APSettingsService::reconfigureAP() { + _lastManaged = millis() - MANAGE_NETWORK_DELAY; } void APSettingsService::loop() { @@ -24,7 +28,8 @@ void APSettingsService::loop() { void APSettingsService::manageAP() { WiFiMode_t currentWiFiMode = WiFi.getMode(); - if (_provisionMode == AP_MODE_ALWAYS || (_provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) { + if (_settings.provisionMode == AP_MODE_ALWAYS || + (_settings.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED)) { if (currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA) { startAP(); } @@ -37,7 +42,7 @@ void APSettingsService::manageAP() { void APSettingsService::startAP() { Serial.println("Starting software access point"); - WiFi.softAP(_ssid.c_str(), _password.c_str()); + WiFi.softAP(_settings.ssid.c_str(), _settings.password.c_str()); if (!_dnsServer) { IPAddress apIp = WiFi.softAPIP(); Serial.print("Starting captive portal on "); @@ -65,25 +70,25 @@ void APSettingsService::handleDNS() { } void APSettingsService::readFromJsonObject(JsonObject& root) { - _provisionMode = root["provision_mode"] | AP_MODE_ALWAYS; - switch (_provisionMode) { + _settings.provisionMode = root["provision_mode"] | AP_MODE_ALWAYS; + switch (_settings.provisionMode) { case AP_MODE_ALWAYS: case AP_MODE_DISCONNECTED: case AP_MODE_NEVER: break; default: - _provisionMode = AP_MODE_ALWAYS; + _settings.provisionMode = AP_MODE_ALWAYS; } - _ssid = root["ssid"] | AP_DEFAULT_SSID; - _password = root["password"] | AP_DEFAULT_PASSWORD; + _settings.ssid = root["ssid"] | AP_DEFAULT_SSID; + _settings.password = root["password"] | AP_DEFAULT_PASSWORD; } void APSettingsService::writeToJsonObject(JsonObject& root) { - root["provision_mode"] = _provisionMode; - root["ssid"] = _ssid; - root["password"] = _password; + root["provision_mode"] = _settings.provisionMode; + root["ssid"] = _settings.ssid; + root["password"] = _settings.password; } void APSettingsService::onConfigUpdated() { - _lastManaged = millis() - MANAGE_NETWORK_DELAY; -} \ No newline at end of file + reconfigureAP(); +} diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index 1a95638..ea29e7f 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -19,7 +19,14 @@ #define AP_SETTINGS_FILE "/config/apSettings.json" #define AP_SETTINGS_SERVICE_PATH "/rest/apSettings" -class APSettingsService : public AdminSettingsService { +class APSettings { + public: + uint8_t provisionMode; + String ssid; + String password; +}; + +class APSettingsService : public AdminSettingsService { public: APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); ~APSettingsService(); @@ -33,17 +40,13 @@ class APSettingsService : public AdminSettingsService { void onConfigUpdated(); private: - // access point settings - uint8_t _provisionMode; - String _ssid; - String _password; - // for the mangement delay loop unsigned long _lastManaged; // for the captive portal DNSServer* _dnsServer; + void reconfigureAP(); void manageAP(); void startAP(); void stopAP(); diff --git a/lib/framework/AdminSettingsService.h b/lib/framework/AdminSettingsService.h index 435c94f..2d6c8ab 100644 --- a/lib/framework/AdminSettingsService.h +++ b/lib/framework/AdminSettingsService.h @@ -3,14 +3,15 @@ #include -class AdminSettingsService : public SettingsService { +template +class AdminSettingsService : public SettingsService { public: AdminSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager, char const* servicePath, char const* filePath) : - SettingsService(server, fs, servicePath, filePath), + SettingsService(server, fs, servicePath, filePath), _securityManager(securityManager) { } @@ -26,7 +27,7 @@ class AdminSettingsService : public SettingsService { return; } // delegate to underlying implemetation - SettingsService::fetchConfig(request); + SettingsService::fetchConfig(request); } void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) { @@ -37,7 +38,7 @@ class AdminSettingsService : public SettingsService { return; } // delegate to underlying implemetation - SettingsService::updateConfig(request, jsonDocument); + SettingsService::updateConfig(request, jsonDocument); } // override this to replace the default authentication predicate, IS_ADMIN diff --git a/lib/framework/AuthenticationService.cpp b/lib/framework/AuthenticationService.cpp index ddc2904..4acf943 100644 --- a/lib/framework/AuthenticationService.cpp +++ b/lib/framework/AuthenticationService.cpp @@ -21,7 +21,7 @@ AuthenticationService::~AuthenticationService() { */ void AuthenticationService::verifyAuthorization(AsyncWebServerRequest* request) { Authentication authentication = _securityManager->authenticateRequest(request); - request->send(authentication.isAuthenticated() ? 200 : 401); + request->send(authentication.authenticated ? 200 : 401); } /** @@ -33,8 +33,8 @@ void AuthenticationService::signIn(AsyncWebServerRequest* request, JsonDocument& String username = jsonDocument["username"]; String password = jsonDocument["password"]; Authentication authentication = _securityManager->authenticate(username, password); - if (authentication.isAuthenticated()) { - User* user = authentication.getUser(); + if (authentication.authenticated) { + User* user = authentication.user; AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_AUTHENTICATION_SIZE); JsonObject jsonObject = response->getRoot(); jsonObject["access_token"] = _securityManager->generateJWT(user); diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h index c6331bf..625adb1 100644 --- a/lib/framework/ESP8266React.h +++ b/lib/framework/ESP8266React.h @@ -41,14 +41,34 @@ class ESP8266React { return &_securitySettingsService; } + SettingsService* getSecuritySettingsService() { + return &_securitySettingsService; + } + + SettingsService* getWiFiSettingsService() { + return &_wifiSettingsService; + } + + SettingsService* getAPSettingsService() { + return &_apSettingsService; + } + + SettingsService* getNTPSettingsService() { + return &_ntpSettingsService; + } + + SettingsService* getOTASettingsService() { + return &_otaSettingsService; + } + private: SecuritySettingsService _securitySettingsService; - WiFiSettingsService _wifiSettingsService; APSettingsService _apSettingsService; NTPSettingsService _ntpSettingsService; OTASettingsService _otaSettingsService; RestartService _restartService; + AuthenticationService _authenticationService; WiFiScanner _wifiScanner; @@ -56,7 +76,6 @@ class ESP8266React { NTPStatus _ntpStatus; APStatus _apStatus; SystemStatus _systemStatus; - }; #endif diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index 5835e36..5056198 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -28,17 +28,17 @@ void NTPSettingsService::loop() { } void NTPSettingsService::readFromJsonObject(JsonObject& root) { - _enabled = root["enabled"] | NTP_SETTINGS_SERVICE_DEFAULT_ENABLED; - _server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER; - _tzLabel = root["tz_label"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_LABEL; - _tzFormat = root["tz_format"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT; + _settings.enabled = root["enabled"] | NTP_SETTINGS_SERVICE_DEFAULT_ENABLED; + _settings.server = root["server"] | NTP_SETTINGS_SERVICE_DEFAULT_SERVER; + _settings.tzLabel = root["tz_label"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_LABEL; + _settings.tzFormat = root["tz_format"] | NTP_SETTINGS_SERVICE_DEFAULT_TIME_ZONE_FORMAT; } void NTPSettingsService::writeToJsonObject(JsonObject& root) { - root["enabled"] = _enabled; - root["server"] = _server; - root["tz_label"] = _tzLabel; - root["tz_format"] = _tzFormat; + root["enabled"] = _settings.enabled; + root["server"] = _settings.server; + root["tz_label"] = _settings.tzLabel; + root["tz_format"] = _settings.tzFormat; } void NTPSettingsService::onConfigUpdated() { @@ -47,23 +47,23 @@ void NTPSettingsService::onConfigUpdated() { #ifdef ESP32 void NTPSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { - Serial.printf("Got IP address, starting NTP Synchronization\n"); + Serial.println("Got IP address, starting NTP Synchronization"); _reconfigureNTP = true; } void NTPSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { - Serial.printf("WiFi connection dropped, stopping NTP.\n"); + Serial.println("WiFi connection dropped, stopping NTP."); _reconfigureNTP = false; sntp_stop(); } #elif defined(ESP8266) void NTPSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) { - Serial.printf("Got IP address, starting NTP Synchronization\n"); + Serial.println("Got IP address, starting NTP Synchronization"); _reconfigureNTP = true; } void NTPSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { - Serial.printf("WiFi connection dropped, stopping NTP.\n"); + Serial.println("WiFi connection dropped, stopping NTP."); _reconfigureNTP = false; sntp_stop(); } @@ -71,13 +71,13 @@ void NTPSettingsService::onStationModeDisconnected(const WiFiEventStationModeDis void NTPSettingsService::configureNTP() { Serial.println("Configuring NTP..."); - if (_enabled) { + if (_settings.enabled) { #ifdef ESP32 - configTzTime(_tzFormat.c_str(), _server.c_str()); + configTzTime(_settings.tzFormat.c_str(), _settings.server.c_str()); #elif defined(ESP8266) - configTime(_tzFormat.c_str(), _server.c_str()); + configTime(_settings.tzFormat.c_str(), _settings.server.c_str()); #endif } else { - sntp_stop(); + sntp_stop(); } } diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index 940f57f..b5f9a5e 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -23,7 +23,15 @@ #define NTP_SETTINGS_FILE "/config/ntpSettings.json" #define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings" -class NTPSettingsService : public AdminSettingsService { +class NTPSettings { + public: + bool enabled; + String tzLabel; + String tzFormat; + String server; +}; + +class NTPSettingsService : public AdminSettingsService { public: NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); ~NTPSettingsService(); @@ -37,11 +45,6 @@ class NTPSettingsService : public AdminSettingsService { void receivedNTPtime(); private: - bool _enabled; - String _tzLabel; - String _tzFormat; - String _server; - bool _reconfigureNTP = false; #ifdef ESP32 diff --git a/lib/framework/OTASettingsService.cpp b/lib/framework/OTASettingsService.cpp index 7702d60..3203f3c 100644 --- a/lib/framework/OTASettingsService.cpp +++ b/lib/framework/OTASettingsService.cpp @@ -15,7 +15,7 @@ OTASettingsService::~OTASettingsService() { } void OTASettingsService::loop() { - if (_enabled && _arduinoOTA) { + if ( _settings.enabled && _arduinoOTA) { _arduinoOTA->handle(); } } @@ -25,15 +25,15 @@ void OTASettingsService::onConfigUpdated() { } void OTASettingsService::readFromJsonObject(JsonObject& root) { - _enabled = root["enabled"] | DEFAULT_OTA_ENABLED; - _port = root["port"] | DEFAULT_OTA_PORT; - _password = root["password"] | DEFAULT_OTA_PASSWORD; + _settings.enabled = root["enabled"] | DEFAULT_OTA_ENABLED; + _settings.port = root["port"] | DEFAULT_OTA_PORT; + _settings.password = root["password"] | DEFAULT_OTA_PASSWORD; } void OTASettingsService::writeToJsonObject(JsonObject& root) { - root["enabled"] = _enabled; - root["port"] = _port; - root["password"] = _password; + root["enabled"] = _settings.enabled; + root["port"] = _settings.port; + root["password"] = _settings.password; } void OTASettingsService::configureArduinoOTA() { @@ -44,11 +44,11 @@ void OTASettingsService::configureArduinoOTA() { delete _arduinoOTA; _arduinoOTA = nullptr; } - if (_enabled) { + if (_settings.enabled) { Serial.println("Starting OTA Update Service"); _arduinoOTA = new ArduinoOTAClass; - _arduinoOTA->setPort(_port); - _arduinoOTA->setPassword(_password.c_str()); + _arduinoOTA->setPort(_settings.port); + _arduinoOTA->setPassword(_settings.password.c_str()); _arduinoOTA->onStart([]() { Serial.println("Starting"); }); _arduinoOTA->onEnd([]() { Serial.println("\nEnd"); }); _arduinoOTA->onProgress([](unsigned int progress, unsigned int total) { diff --git a/lib/framework/OTASettingsService.h b/lib/framework/OTASettingsService.h index 7478924..17a873b 100644 --- a/lib/framework/OTASettingsService.h +++ b/lib/framework/OTASettingsService.h @@ -20,7 +20,14 @@ #define OTA_SETTINGS_FILE "/config/otaSettings.json" #define OTA_SETTINGS_SERVICE_PATH "/rest/otaSettings" -class OTASettingsService : public AdminSettingsService { +class OTASettings { + public: + bool enabled; + int port; + String password; +}; + +class OTASettingsService : public AdminSettingsService { public: OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); ~OTASettingsService(); @@ -34,9 +41,6 @@ class OTASettingsService : public AdminSettingsService { private: ArduinoOTAClass* _arduinoOTA; - bool _enabled; - int _port; - String _password; void configureArduinoOTA(); #ifdef ESP32 diff --git a/lib/framework/SecurityManager.cpp b/lib/framework/SecurityManager.cpp deleted file mode 100644 index 30dae08..0000000 --- a/lib/framework/SecurityManager.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include - -Authentication SecurityManager::authenticateRequest(AsyncWebServerRequest *request) { - AsyncWebHeader *authorizationHeader = request->getHeader(AUTHORIZATION_HEADER); - if (authorizationHeader) { - String value = authorizationHeader->value(); - if (value.startsWith(AUTHORIZATION_HEADER_PREFIX)) { - value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN); - return authenticateJWT(value); - } - } - return Authentication(); -} - -Authentication SecurityManager::authenticateJWT(String jwt) { - DynamicJsonDocument payloadDocument(MAX_JWT_SIZE); - _jwtHandler.parseJWT(jwt, payloadDocument); - if (payloadDocument.is()) { - JsonObject parsedPayload = payloadDocument.as(); - String username = parsedPayload["username"]; - for (User _user : _users) { - if (_user.getUsername() == username && validatePayload(parsedPayload, &_user)) { - return Authentication(_user); - } - } - } - return Authentication(); -} - -Authentication SecurityManager::authenticate(String username, String password) { - for (User _user : _users) { - if (_user.getUsername() == username && _user.getPassword() == password) { - return Authentication(_user); - } - } - return Authentication(); -} - -inline void populateJWTPayload(JsonObject &payload, User *user) { - payload["username"] = user->getUsername(); - payload["admin"] = user->isAdmin(); -} - -boolean SecurityManager::validatePayload(JsonObject &parsedPayload, User *user) { - DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); - JsonObject payload = _jsonDocument.to(); - populateJWTPayload(payload, user); - return payload == parsedPayload; -} - -String SecurityManager::generateJWT(User *user) { - DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); - JsonObject payload = _jsonDocument.to(); - populateJWTPayload(payload, user); - return _jwtHandler.buildJWT(payload); -} - -ArRequestHandlerFunction SecurityManager::wrapRequest(ArRequestHandlerFunction onRequest, - AuthenticationPredicate predicate) { - return [this, onRequest, predicate](AsyncWebServerRequest *request) { - Authentication authentication = authenticateRequest(request); - if (!predicate(authentication)) { - request->send(401); - return; - } - onRequest(request); - }; -} diff --git a/lib/framework/SecurityManager.h b/lib/framework/SecurityManager.h index 67846e1..b275b4b 100644 --- a/lib/framework/SecurityManager.h +++ b/lib/framework/SecurityManager.h @@ -14,43 +14,28 @@ #define MAX_JWT_SIZE 128 class User { - private: - String _username; - String _password; - bool _admin; + public: + String username; + String password; + bool admin; public: - User(String username, String password, bool admin) : _username(username), _password(password), _admin(admin) { - } - String getUsername() { - return _username; - } - String getPassword() { - return _password; - } - bool isAdmin() { - return _admin; + User(String username, String password, bool admin) : username(username), password(password), admin(admin) { } }; class Authentication { - private: - User* _user; - boolean _authenticated; + public: + User* user; + boolean authenticated; public: - Authentication(User& user) : _user(new User(user)), _authenticated(true) { + Authentication(User& user) : user(new User(user)), authenticated(true) { } - Authentication() : _user(nullptr), _authenticated(false) { + Authentication() : user(nullptr), authenticated(false) { } ~Authentication() { - delete (_user); - } - User* getUser() { - return _user; - } - bool isAuthenticated() { - return _authenticated; + delete (user); } }; @@ -62,10 +47,10 @@ class AuthenticationPredicates { return true; }; static bool IS_AUTHENTICATED(Authentication& authentication) { - return authentication.isAuthenticated(); + return authentication.authenticated; }; static bool IS_ADMIN(Authentication& authentication) { - return authentication.isAuthenticated() && authentication.getUser()->isAdmin(); + return authentication.authenticated && authentication.user->admin; }; }; @@ -74,37 +59,23 @@ class SecurityManager { /* * Authenticate, returning the user if found */ - Authentication authenticate(String username, String password); + virtual Authentication authenticate(String username, String password) = 0; /* * Check the request header for the Authorization token */ - Authentication authenticateRequest(AsyncWebServerRequest* request); + virtual Authentication authenticateRequest(AsyncWebServerRequest* request) = 0; /* * Generate a JWT for the user provided */ - String generateJWT(User* user); + virtual String generateJWT(User* user) = 0; /** * Wrap the provided request to provide validation against an AuthenticationPredicate. */ - ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); - - protected: - ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET); - std::list _users; - - private: - /* - * Lookup the user by JWT - */ - Authentication authenticateJWT(String jwt); - - /* - * Verify the payload is correct - */ - boolean validatePayload(JsonObject& parsedPayload, User* user); + virtual ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, + AuthenticationPredicate predicate) = 0; }; #endif // end SecurityManager_h \ No newline at end of file diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index 47bc549..51477d2 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -12,14 +12,14 @@ void SecuritySettingsService::readFromJsonObject(JsonObject& root) { _jwtHandler.setSecret(root["jwt_secret"] | DEFAULT_JWT_SECRET); // users - _users.clear(); + _settings.users.clear(); if (root["users"].is()) { for (JsonVariant user : root["users"].as()) { - _users.push_back(User(user["username"], user["password"], user["admin"])); + _settings.users.push_back(User(user["username"], user["password"], user["admin"])); } } else { - _users.push_back(User(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_USERNAME, true)); - _users.push_back(User(DEFAULT_GUEST_USERNAME, DEFAULT_GUEST_USERNAME, false)); + _settings.users.push_back(User(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_USERNAME, true)); + _settings.users.push_back(User(DEFAULT_GUEST_USERNAME, DEFAULT_GUEST_USERNAME, false)); } } @@ -29,10 +29,78 @@ void SecuritySettingsService::writeToJsonObject(JsonObject& root) { // users JsonArray users = root.createNestedArray("users"); - for (User _user : _users) { + for (User _user : _settings.users) { JsonObject user = users.createNestedObject(); - user["username"] = _user.getUsername(); - user["password"] = _user.getPassword(); - user["admin"] = _user.isAdmin(); + user["username"] = _user.username; + user["password"] = _user.password; + user["admin"] = _user.admin; } } + + +Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest *request) { + AsyncWebHeader *authorizationHeader = request->getHeader(AUTHORIZATION_HEADER); + if (authorizationHeader) { + String value = authorizationHeader->value(); + if (value.startsWith(AUTHORIZATION_HEADER_PREFIX)) { + value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN); + return authenticateJWT(value); + } + } + return Authentication(); +} + +Authentication SecuritySettingsService::authenticateJWT(String jwt) { + DynamicJsonDocument payloadDocument(MAX_JWT_SIZE); + _jwtHandler.parseJWT(jwt, payloadDocument); + if (payloadDocument.is()) { + JsonObject parsedPayload = payloadDocument.as(); + String username = parsedPayload["username"]; + for (User _user : _settings.users) { + if (_user.username == username && validatePayload(parsedPayload, &_user)) { + return Authentication(_user); + } + } + } + return Authentication(); +} + +Authentication SecuritySettingsService::authenticate(String username, String password) { + for (User _user : _settings.users) { + if (_user.username == username && _user.password == password) { + return Authentication(_user); + } + } + return Authentication(); +} + +inline void populateJWTPayload(JsonObject &payload, User *user) { + payload["username"] = user->username; + payload["admin"] = user->admin; +} + +boolean SecuritySettingsService::validatePayload(JsonObject &parsedPayload, User *user) { + DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); + JsonObject payload = _jsonDocument.to(); + populateJWTPayload(payload, user); + return payload == parsedPayload; +} + +String SecuritySettingsService::generateJWT(User *user) { + DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); + JsonObject payload = _jsonDocument.to(); + populateJWTPayload(payload, user); + return _jwtHandler.buildJWT(payload); +} + +ArRequestHandlerFunction SecuritySettingsService::wrapRequest(ArRequestHandlerFunction onRequest, + AuthenticationPredicate predicate) { + return [this, onRequest, predicate](AsyncWebServerRequest *request) { + Authentication authentication = authenticateRequest(request); + if (!predicate(authentication)) { + request->send(401); + return; + } + onRequest(request); + }; +} diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 9ff0257..3722519 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -10,14 +10,39 @@ #define SECURITY_SETTINGS_FILE "/config/securitySettings.json" #define SECURITY_SETTINGS_PATH "/rest/securitySettings" -class SecuritySettingsService : public AdminSettingsService, public SecurityManager { +class SecuritySettings { + public: + String jwtSecret; + std::list users; +}; + +class SecuritySettingsService : public AdminSettingsService, public SecurityManager { public: SecuritySettingsService(AsyncWebServer* server, FS* fs); ~SecuritySettingsService(); + // Functions to implement SecurityManager + Authentication authenticate(String username, String password); + Authentication authenticateRequest(AsyncWebServerRequest* request); + String generateJWT(User* user); + ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); + protected: void readFromJsonObject(JsonObject& root); void writeToJsonObject(JsonObject& root); + + private: + ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET); + + /* + * Lookup the user by JWT + */ + Authentication authenticateJWT(String jwt); + + /* + * Verify the payload is correct + */ + boolean validatePayload(JsonObject& parsedPayload, User* user); }; #endif // end SecuritySettingsService_h \ No newline at end of file diff --git a/lib/framework/SettingsService.h b/lib/framework/SettingsService.h index 1d964fe..7fab585 100644 --- a/lib/framework/SettingsService.h +++ b/lib/framework/SettingsService.h @@ -1,6 +1,8 @@ #ifndef SettingsService_h #define SettingsService_h +#include + #ifdef ESP32 #include #include @@ -17,9 +19,24 @@ #include #include +typedef size_t update_handler_id_t; +typedef std::function SettingsUpdateCallback; +static update_handler_id_t currentUpdateHandlerId; + +typedef struct SettingsUpdateHandlerInfo { + update_handler_id_t _id; + SettingsUpdateCallback _cb; + bool _allowRemove; + SettingsUpdateHandlerInfo(SettingsUpdateCallback cb, bool allowRemove) : + _id(++currentUpdateHandlerId), + _cb(cb), + _allowRemove(allowRemove){}; +} SettingsUpdateHandlerInfo_t; + /* * Abstraction of a service which stores it's settings as JSON in a file system. */ +template class SettingsService : public SettingsPersistence { public: SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath) : @@ -37,14 +54,71 @@ class SettingsService : public SettingsPersistence { virtual ~SettingsService() { } + update_handler_id_t addUpdateHandler(SettingsUpdateCallback cb, bool allowRemove = true) { + if (!cb) { + return 0; + } + SettingsUpdateHandlerInfo_t updateHandler(cb, allowRemove); + _settingsUpdateHandlers.push_back(updateHandler); + return updateHandler._id; + } + + void removeUpdateHandler(update_handler_id_t id) { + for (auto i = _settingsUpdateHandlers.begin(); i != _settingsUpdateHandlers.end();) { + if ((*i)._id == id) { + i = _settingsUpdateHandlers.erase(i); + } else { + ++i; + } + } + } + + T fetch() { + return _settings; + } + + void update(T& settings) { + _settings = settings; + writeToFS(); + callUpdateHandlers(); + } + + void fetchAsString(String& config) { + DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE); + fetchAsDocument(jsonDocument); + serializeJson(jsonDocument, config); + } + + void updateFromString(String& config) { + DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE); + deserializeJson(jsonDocument, config); + updateFromDocument(jsonDocument); + } + + void fetchAsDocument(JsonDocument& jsonDocument) { + JsonObject jsonObject = jsonDocument.to(); + writeToJsonObject(jsonObject); + } + + void updateFromDocument(JsonDocument& jsonDocument) { + if (jsonDocument.is()) { + JsonObject newConfig = jsonDocument.as(); + readFromJsonObject(newConfig); + writeToFS(); + callUpdateHandlers(); + } + } + void begin() { // read the initial data from the file system readFromFS(); } protected: + T _settings; char const* _servicePath; AsyncJsonWebHandler _updateHandler; + std::list _settingsUpdateHandlers; virtual void fetchConfig(AsyncWebServerRequest* request) { // handle the request @@ -64,7 +138,7 @@ class SettingsService : public SettingsPersistence { // write settings back with a callback to reconfigure the wifi AsyncJsonCallbackResponse* response = - new AsyncJsonCallbackResponse([this]() { onConfigUpdated(); }, false, MAX_SETTINGS_SIZE); + new AsyncJsonCallbackResponse([this]() { callUpdateHandlers(); }, false, MAX_SETTINGS_SIZE); JsonObject jsonObject = response->getRoot(); writeToJsonObject(jsonObject); response->setLength(); @@ -74,6 +148,16 @@ class SettingsService : public SettingsPersistence { } } + void callUpdateHandlers() { + // call the classes own config update function + onConfigUpdated(); + + // call all setting update handlers + for (const SettingsUpdateHandlerInfo_t& handler : _settingsUpdateHandlers) { + handler._cb(); + } + } + // implement to perform action when config has been updated virtual void onConfigUpdated() { } diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index 9b70146..3526332 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -33,45 +33,45 @@ void WiFiSettingsService::begin() { } void WiFiSettingsService::readFromJsonObject(JsonObject& root) { - _ssid = root["ssid"] | ""; - _password = root["password"] | ""; - _hostname = root["hostname"] | ""; - _staticIPConfig = root["static_ip_config"] | false; + _settings.ssid = root["ssid"] | ""; + _settings.password = root["password"] | ""; + _settings.hostname = root["hostname"] | ""; + _settings.staticIPConfig = root["static_ip_config"] | false; // extended settings - readIP(root, "local_ip", _localIP); - readIP(root, "gateway_ip", _gatewayIP); - readIP(root, "subnet_mask", _subnetMask); - readIP(root, "dns_ip_1", _dnsIP1); - readIP(root, "dns_ip_2", _dnsIP2); + readIP(root, "local_ip", _settings.localIP); + readIP(root, "gateway_ip", _settings.gatewayIP); + readIP(root, "subnet_mask", _settings.subnetMask); + readIP(root, "dns_ip_1", _settings.dnsIP1); + readIP(root, "dns_ip_2", _settings.dnsIP2); // Swap around the dns servers if 2 is populated but 1 is not - if (_dnsIP1 == INADDR_NONE && _dnsIP2 != INADDR_NONE) { - _dnsIP1 = _dnsIP2; - _dnsIP2 = INADDR_NONE; + if (_settings.dnsIP1 == INADDR_NONE && _settings.dnsIP2 != INADDR_NONE) { + _settings.dnsIP1 = _settings.dnsIP2; + _settings.dnsIP2 = INADDR_NONE; } // Turning off static ip config if we don't meet the minimum requirements // of ipAddress, gateway and subnet. This may change to static ip only // as sensible defaults can be assumed for gateway and subnet - if (_staticIPConfig && (_localIP == INADDR_NONE || _gatewayIP == INADDR_NONE || _subnetMask == INADDR_NONE)) { - _staticIPConfig = false; + if (_settings.staticIPConfig && (_settings.localIP == INADDR_NONE || _settings.gatewayIP == INADDR_NONE || _settings.subnetMask == INADDR_NONE)) { + _settings.staticIPConfig = false; } } void WiFiSettingsService::writeToJsonObject(JsonObject& root) { // connection settings - root["ssid"] = _ssid; - root["password"] = _password; - root["hostname"] = _hostname; - root["static_ip_config"] = _staticIPConfig; + root["ssid"] = _settings.ssid; + root["password"] = _settings.password; + root["hostname"] = _settings.hostname; + root["static_ip_config"] = _settings.staticIPConfig; // extended settings - writeIP(root, "local_ip", _localIP); - writeIP(root, "gateway_ip", _gatewayIP); - writeIP(root, "subnet_mask", _subnetMask); - writeIP(root, "dns_ip_1", _dnsIP1); - writeIP(root, "dns_ip_2", _dnsIP2); + writeIP(root, "local_ip", _settings.localIP); + writeIP(root, "gateway_ip", _settings.gatewayIP); + writeIP(root, "subnet_mask", _settings.subnetMask); + writeIP(root, "dns_ip_1", _settings.dnsIP1); + writeIP(root, "dns_ip_2", _settings.dnsIP2); } void WiFiSettingsService::onConfigUpdated() { @@ -108,27 +108,27 @@ void WiFiSettingsService::loop() { void WiFiSettingsService::manageSTA() { // Abort if already connected, or if we have no SSID - if (WiFi.isConnected() || _ssid.length() == 0) { + if (WiFi.isConnected() || _settings.ssid.length() == 0) { return; } // Connect or reconnect as required if ((WiFi.getMode() & WIFI_STA) == 0) { Serial.println("Connecting to WiFi."); - if (_staticIPConfig) { + if (_settings.staticIPConfig) { // configure for static IP - WiFi.config(_localIP, _gatewayIP, _subnetMask, _dnsIP1, _dnsIP2); + WiFi.config(_settings.localIP, _settings.gatewayIP, _settings.subnetMask, _settings.dnsIP1, _settings.dnsIP2); } else { // configure for DHCP #ifdef ESP32 WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); - WiFi.setHostname(_hostname.c_str()); + WiFi.setHostname(_settings.hostname.c_str()); #elif defined(ESP8266) WiFi.config(INADDR_ANY, INADDR_ANY, INADDR_ANY); - WiFi.hostname(_hostname); + WiFi.hostname(_settings.hostname); #endif } // attempt to connect to the network - WiFi.begin(_ssid.c_str(), _password.c_str()); + WiFi.begin(_settings.ssid.c_str(), _settings.password.c_str()); } } diff --git a/lib/framework/WiFiSettingsService.h b/lib/framework/WiFiSettingsService.h index 102f571..321bcf9 100644 --- a/lib/framework/WiFiSettingsService.h +++ b/lib/framework/WiFiSettingsService.h @@ -8,7 +8,23 @@ #define WIFI_SETTINGS_SERVICE_PATH "/rest/wifiSettings" #define WIFI_RECONNECTION_DELAY 1000 * 60 -class WiFiSettingsService : public AdminSettingsService { +class WiFiSettings { + public: + // core wifi configuration + String ssid; + String password; + String hostname; + bool staticIPConfig; + + // optional configuration for static IP address + IPAddress localIP; + IPAddress gatewayIP; + IPAddress subnetMask; + IPAddress dnsIP1; + IPAddress dnsIP2; +}; + +class WiFiSettingsService : public AdminSettingsService { public: WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); ~WiFiSettingsService(); @@ -22,22 +38,8 @@ class WiFiSettingsService : public AdminSettingsService { void onConfigUpdated(); private: - // connection settings - String _ssid; - String _password; - String _hostname; - bool _staticIPConfig; - - // for the mangement delay loop unsigned long _lastConnectionAttempt; - // optional configuration for static IP address - IPAddress _localIP; - IPAddress _gatewayIP; - IPAddress _subnetMask; - IPAddress _dnsIP1; - IPAddress _dnsIP2; - #ifdef ESP32 void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); #elif defined(ESP8266) diff --git a/src/DemoProject.cpp b/src/DemoProject.cpp index d3eed27..4ad924e 100644 --- a/src/DemoProject.cpp +++ b/src/DemoProject.cpp @@ -9,7 +9,7 @@ DemoProject::~DemoProject() { } void DemoProject::loop() { - unsigned delay = MAX_DELAY / 255 * (255 - _blinkSpeed); + unsigned delay = MAX_DELAY / 255 * (255 - _settings.blinkSpeed); unsigned long currentMillis = millis(); if (!_lastBlink || (unsigned long)(currentMillis - _lastBlink) >= delay) { _lastBlink = currentMillis; @@ -18,10 +18,10 @@ void DemoProject::loop() { } void DemoProject::readFromJsonObject(JsonObject& root) { - _blinkSpeed = root["blink_speed"] | DEFAULT_BLINK_SPEED; + _settings.blinkSpeed = root["blink_speed"] | DEFAULT_BLINK_SPEED; } void DemoProject::writeToJsonObject(JsonObject& root) { // connection settings - root["blink_speed"] = _blinkSpeed; + root["blink_speed"] = _settings.blinkSpeed; } diff --git a/src/DemoProject.h b/src/DemoProject.h index cabf13c..bd36e57 100644 --- a/src/DemoProject.h +++ b/src/DemoProject.h @@ -2,6 +2,7 @@ #define DemoProject_h #include +#include #define BLINK_LED 2 #define MAX_DELAY 1000 @@ -10,7 +11,12 @@ #define DEMO_SETTINGS_FILE "/config/demoSettings.json" #define DEMO_SETTINGS_PATH "/rest/demoSettings" -class DemoProject : public AdminSettingsService { +class DemoSettings { + public: + uint8_t blinkSpeed; +}; + +class DemoProject : public AdminSettingsService { public: DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); ~DemoProject(); @@ -19,7 +25,6 @@ class DemoProject : public AdminSettingsService { private: unsigned long _lastBlink = 0; - uint8_t _blinkSpeed = 255; protected: void readFromJsonObject(JsonObject& root);