Browse Source

External config

Allow config to be accessed from outside the framework core code.
master
rjwats 4 years ago
committed by GitHub
parent
commit
39a86b0411
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 75
      README.md
  2. 31
      lib/framework/APSettingsService.cpp
  3. 15
      lib/framework/APSettingsService.h
  4. 9
      lib/framework/AdminSettingsService.h
  5. 6
      lib/framework/AuthenticationService.cpp
  6. 23
      lib/framework/ESP8266React.h
  7. 32
      lib/framework/NTPSettingsService.cpp
  8. 15
      lib/framework/NTPSettingsService.h
  9. 20
      lib/framework/OTASettingsService.cpp
  10. 12
      lib/framework/OTASettingsService.h
  11. 68
      lib/framework/SecurityManager.cpp
  12. 65
      lib/framework/SecurityManager.h
  13. 84
      lib/framework/SecuritySettingsService.cpp
  14. 27
      lib/framework/SecuritySettingsService.h
  15. 86
      lib/framework/SettingsService.h
  16. 58
      lib/framework/WiFiSettingsService.cpp
  17. 32
      lib/framework/WiFiSettingsService.h
  18. 6
      src/DemoProject.cpp
  19. 9
      src/DemoProject.h

75
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 <SettingsService.h>
class ExampleSettingsService : public SettingsService {
class ExampleSettings {
public:
String username;
String password;
};
class ExampleSettingsService : public SettingsService<ExampleSettings> {
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)

31
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;
}
reconfigureAP();
}

15
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<APSettings> {
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();

9
lib/framework/AdminSettingsService.h

@ -3,14 +3,15 @@
#include <SettingsService.h>
class AdminSettingsService : public SettingsService {
template <class T>
class AdminSettingsService : public SettingsService<T> {
public:
AdminSettingsService(AsyncWebServer* server,
FS* fs,
SecurityManager* securityManager,
char const* servicePath,
char const* filePath) :
SettingsService(server, fs, servicePath, filePath),
SettingsService<T>(server, fs, servicePath, filePath),
_securityManager(securityManager) {
}
@ -26,7 +27,7 @@ class AdminSettingsService : public SettingsService {
return;
}
// delegate to underlying implemetation
SettingsService::fetchConfig(request);
SettingsService<T>::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<T>::updateConfig(request, jsonDocument);
}
// override this to replace the default authentication predicate, IS_ADMIN

6
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);

23
lib/framework/ESP8266React.h

@ -41,14 +41,34 @@ class ESP8266React {
return &_securitySettingsService;
}
SettingsService<SecuritySettings>* getSecuritySettingsService() {
return &_securitySettingsService;
}
SettingsService<WiFiSettings>* getWiFiSettingsService() {
return &_wifiSettingsService;
}
SettingsService<APSettings>* getAPSettingsService() {
return &_apSettingsService;
}
SettingsService<NTPSettings>* getNTPSettingsService() {
return &_ntpSettingsService;
}
SettingsService<OTASettings>* 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

32
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();
}
}

15
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<NTPSettings> {
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

20
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) {

12
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<OTASettings> {
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

68
lib/framework/SecurityManager.cpp

@ -1,68 +0,0 @@
#include <SecurityManager.h>
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>()) {
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
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<JsonObject>();
populateJWTPayload(payload, user);
return payload == parsedPayload;
}
String SecurityManager::generateJWT(User *user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject payload = _jsonDocument.to<JsonObject>();
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);
};
}

65
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<User> _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

84
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<JsonArray>()) {
for (JsonVariant user : root["users"].as<JsonArray>()) {
_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>()) {
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
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<JsonObject>();
populateJWTPayload(payload, user);
return payload == parsedPayload;
}
String SecuritySettingsService::generateJWT(User *user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject payload = _jsonDocument.to<JsonObject>();
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);
};
}

27
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<User> users;
};
class SecuritySettingsService : public AdminSettingsService<SecuritySettings>, 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

86
lib/framework/SettingsService.h

@ -1,6 +1,8 @@
#ifndef SettingsService_h
#define SettingsService_h
#include <functional>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
@ -17,9 +19,24 @@
#include <SecurityManager.h>
#include <SettingsPersistence.h>
typedef size_t update_handler_id_t;
typedef std::function<void(void)> 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 T>
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<JsonObject>();
writeToJsonObject(jsonObject);
}
void updateFromDocument(JsonDocument& jsonDocument) {
if (jsonDocument.is<JsonObject>()) {
JsonObject newConfig = jsonDocument.as<JsonObject>();
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<SettingsUpdateHandlerInfo_t> _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() {
}

58
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());
}
}

32
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<WiFiSettings> {
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)

6
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;
}

9
src/DemoProject.h

@ -2,6 +2,7 @@
#define DemoProject_h
#include <AdminSettingsService.h>
#include <ESP8266React.h>
#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<DemoSettings> {
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);

Loading…
Cancel
Save