External config
Allow config to be accessed from outside the framework core code.
This commit is contained in:
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
@ -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
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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
|
@ -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() {
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user