This commit is contained in:
proddy
2025-01-04 13:41:39 +01:00
parent 4138598db2
commit eb87651c47
166 changed files with 2099 additions and 10446 deletions

View File

@@ -0,0 +1,166 @@
#include "APSettingsService.h"
#include <emsesp_stub.hpp>
APSettingsService::APSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(APSettings::read, APSettings::update, this, server, AP_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(APSettings::read, APSettings::update, this, fs, AP_SETTINGS_FILE)
, _dnsServer(nullptr)
, _lastManaged(0)
, _reconfigureAp(false)
, _connected(0) {
addUpdateHandler([this] { reconfigureAP(); }, false);
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
}
void APSettingsService::begin() {
_fsPersistence.readFromFS();
// disabled for delayed start, first try station mode
// reconfigureAP();
}
// wait 10 sec on STA disconnect before starting AP
void APSettingsService::WiFiEvent(WiFiEvent_t event) {
uint8_t was_connected = _connected;
switch (event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
_connected &= ~1;
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
_connected &= ~2;
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
_connected |= 1;
break;
case ARDUINO_EVENT_ETH_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP6:
_connected |= 2;
break;
default:
break;
}
// wait 10 sec before starting AP
if (was_connected && !_connected) {
_lastManaged = uuid::get_uptime();
}
}
void APSettingsService::reconfigureAP() {
_lastManaged = uuid::get_uptime() - MANAGE_NETWORK_DELAY;
_reconfigureAp = true;
}
void APSettingsService::loop() {
unsigned long currentMillis = uuid::get_uptime();
unsigned long manageElapsed = static_cast<uint32_t>(currentMillis - _lastManaged);
if (manageElapsed >= MANAGE_NETWORK_DELAY) {
_lastManaged = currentMillis;
manageAP();
}
handleDNS();
}
void APSettingsService::manageAP() {
WiFiMode_t currentWiFiMode = WiFi.getMode();
if (_state.provisionMode == AP_MODE_ALWAYS || (_state.provisionMode == AP_MODE_DISCONNECTED && !_connected)) {
if (_reconfigureAp || currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA) {
startAP();
}
} else if ((currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA) && _connected && (_reconfigureAp || !WiFi.softAPgetStationNum())) {
stopAP();
}
_reconfigureAp = false;
}
void APSettingsService::startAP() {
#if ESP_IDF_VERSION_MAJOR < 5
WiFi.softAPenableIpV6(); // force IPV6, same as for WiFi - fixes https://github.com/emsesp/EMS-ESP32/issues/1922
#else
WiFi.softAPenableIPv6(); // force IPV6, same as for WiFi - fixes https://github.com/emsesp/EMS-ESP32/issues/1922
#endif
WiFi.softAPConfig(_state.localIP, _state.gatewayIP, _state.subnetMask);
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_AP), WIFI_BW_HT20);
WiFi.softAP(_state.ssid.c_str(), _state.password.c_str(), _state.channel, _state.ssidHidden, _state.maxClients);
#if CONFIG_IDF_TARGET_ESP32C3
WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
#endif
if (!_dnsServer) {
IPAddress apIp = WiFi.softAPIP();
emsesp::EMSESP::logger().info("Starting Access Point with captive portal on %s", apIp.toString().c_str());
_dnsServer = new DNSServer;
_dnsServer->start(DNS_PORT, "*", apIp);
}
}
void APSettingsService::stopAP() {
if (_dnsServer) {
emsesp::EMSESP::logger().info("Stopping Access Point");
_dnsServer->stop();
delete _dnsServer;
_dnsServer = nullptr;
}
WiFi.softAPdisconnect(true);
}
void APSettingsService::handleDNS() {
if (_dnsServer) {
_dnsServer->processNextRequest();
}
}
APNetworkStatus APSettingsService::getAPNetworkStatus() {
WiFiMode_t currentWiFiMode = WiFi.getMode();
bool apActive = currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA;
if (apActive && _state.provisionMode != AP_MODE_ALWAYS && WiFi.status() == WL_CONNECTED) {
return APNetworkStatus::LINGERING;
}
return apActive ? APNetworkStatus::ACTIVE : APNetworkStatus::INACTIVE;
}
void APSettings::read(const APSettings & settings, JsonObject root) {
root["provision_mode"] = settings.provisionMode;
root["ssid"] = settings.ssid;
root["password"] = settings.password;
root["channel"] = settings.channel;
root["ssid_hidden"] = settings.ssidHidden;
root["max_clients"] = settings.maxClients;
root["local_ip"] = settings.localIP.toString();
root["gateway_ip"] = settings.gatewayIP.toString();
root["subnet_mask"] = settings.subnetMask.toString();
}
StateUpdateResult APSettings::update(JsonObject root, APSettings & settings) {
APSettings newSettings = {};
newSettings.provisionMode = static_cast<uint8_t>(root["provision_mode"] | FACTORY_AP_PROVISION_MODE);
switch (settings.provisionMode) {
case AP_MODE_ALWAYS:
case AP_MODE_DISCONNECTED:
case AP_MODE_NEVER:
break;
default:
newSettings.provisionMode = AP_MODE_ALWAYS;
}
newSettings.ssid = root["ssid"] | FACTORY_AP_SSID;
newSettings.password = root["password"] | FACTORY_AP_PASSWORD;
newSettings.channel = static_cast<uint8_t>(root["channel"] | FACTORY_AP_CHANNEL);
newSettings.ssidHidden = root["ssid_hidden"] | FACTORY_AP_SSID_HIDDEN;
newSettings.maxClients = static_cast<uint8_t>(root["max_clients"] | FACTORY_AP_MAX_CLIENTS);
JsonUtils::readIP(root, "local_ip", newSettings.localIP, String(FACTORY_AP_LOCAL_IP));
JsonUtils::readIP(root, "gateway_ip", newSettings.gatewayIP, String(FACTORY_AP_GATEWAY_IP));
JsonUtils::readIP(root, "subnet_mask", newSettings.subnetMask, String(FACTORY_AP_SUBNET_MASK));
if (newSettings == settings) {
return StateUpdateResult::UNCHANGED;
}
settings = newSettings;
return StateUpdateResult::CHANGED;
}

View File

@@ -0,0 +1,110 @@
#ifndef APSettingsConfig_h
#define APSettingsConfig_h
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#include "JsonUtils.h"
#include <DNSServer.h>
#include <IPAddress.h>
#ifndef FACTORY_AP_PROVISION_MODE
#define FACTORY_AP_PROVISION_MODE AP_MODE_DISCONNECTED
#endif
#ifndef FACTORY_AP_SSID
#define FACTORY_AP_SSID "ems-esp"
#endif
#ifndef FACTORY_AP_PASSWORD
#define FACTORY_AP_PASSWORD "ems-esp-neo"
#endif
#ifndef FACTORY_AP_LOCAL_IP
#define FACTORY_AP_LOCAL_IP "192.168.4.1"
#endif
#ifndef FACTORY_AP_GATEWAY_IP
#define FACTORY_AP_GATEWAY_IP "192.168.4.1"
#endif
#ifndef FACTORY_AP_SUBNET_MASK
#define FACTORY_AP_SUBNET_MASK "255.255.255.0"
#endif
#ifndef FACTORY_AP_CHANNEL
#define FACTORY_AP_CHANNEL 1
#endif
#ifndef FACTORY_AP_SSID_HIDDEN
#define FACTORY_AP_SSID_HIDDEN false
#endif
#ifndef FACTORY_AP_MAX_CLIENTS
#define FACTORY_AP_MAX_CLIENTS 4
#endif
#define AP_SETTINGS_FILE "/config/apSettings.json"
#define AP_SETTINGS_SERVICE_PATH "/rest/apSettings"
#define AP_MODE_ALWAYS 0
#define AP_MODE_DISCONNECTED 1
#define AP_MODE_NEVER 2
#define MANAGE_NETWORK_DELAY 10000
#define DNS_PORT 53
enum APNetworkStatus { ACTIVE = 0, INACTIVE, LINGERING };
class APSettings {
public:
uint8_t provisionMode;
String ssid;
String password;
uint8_t channel;
bool ssidHidden;
uint8_t maxClients;
IPAddress localIP;
IPAddress gatewayIP;
IPAddress subnetMask;
bool operator==(const APSettings & settings) const {
return provisionMode == settings.provisionMode && ssid == settings.ssid && password == settings.password && channel == settings.channel
&& ssidHidden == settings.ssidHidden && maxClients == settings.maxClients && localIP == settings.localIP && gatewayIP == settings.gatewayIP
&& subnetMask == settings.subnetMask;
}
static void read(const APSettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, APSettings & settings);
};
class APSettingsService : public StatefulService<APSettings> {
public:
APSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
void loop();
APNetworkStatus getAPNetworkStatus();
private:
HttpEndpoint<APSettings> _httpEndpoint;
FSPersistence<APSettings> _fsPersistence;
// for the captive portal
DNSServer * _dnsServer;
// for the management delay loop
volatile unsigned long _lastManaged;
volatile boolean _reconfigureAp;
uint8_t _connected;
void reconfigureAP();
void manageAP();
void startAP();
void stopAP();
void handleDNS();
void WiFiEvent(WiFiEvent_t event);
};
#endif

View File

@@ -0,0 +1,21 @@
#include "APStatus.h"
APStatus::APStatus(AsyncWebServer * server, SecurityManager * securityManager, APSettingsService * apSettingsService)
: _apSettingsService(apSettingsService) {
securityManager->addEndpoint(server, AP_STATUS_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
apStatus(request);
});
}
void APStatus::apStatus(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["status"] = _apSettingsService->getAPNetworkStatus();
root["ip_address"] = WiFi.softAPIP().toString();
root["mac_address"] = WiFi.softAPmacAddress();
root["station_num"] = WiFi.softAPgetStationNum();
response->setLength();
request->send(response);
}

25
src/ESP32React/APStatus.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef APStatus_h
#define APStatus_h
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <IPAddress.h>
#include "SecurityManager.h"
#include "APSettingsService.h"
#define AP_STATUS_SERVICE_PATH "/rest/apStatus"
class APStatus {
public:
APStatus(AsyncWebServer * server, SecurityManager * securityManager, APSettingsService * apSettingsService);
private:
APSettingsService * _apSettingsService;
void apStatus(AsyncWebServerRequest * request);
};
#endif

View File

@@ -0,0 +1,137 @@
#include "ArduinoJsonJWT.h"
#include <array>
ArduinoJsonJWT::ArduinoJsonJWT(String secret)
: _secret(std::move(secret)) {
}
void ArduinoJsonJWT::setSecret(String secret) {
_secret = std::move(secret);
}
String ArduinoJsonJWT::getSecret() {
return _secret;
}
String ArduinoJsonJWT::buildJWT(JsonObject payload) {
// serialize, then encode payload
String jwt;
serializeJson(payload, jwt);
jwt = encode(jwt.c_str(), static_cast<int>(jwt.length()));
// add the header to payload
jwt = getJWTHeader() + '.' + jwt;
// add signature
jwt += '.' + sign(jwt);
return jwt;
}
void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument & jsonDocument) {
// clear json document before we begin, jsonDocument wil be null on failure
jsonDocument.clear();
const String & jwt_header = getJWTHeader();
const unsigned int jwt_header_size = jwt_header.length();
// must have the correct header and delimiter
if (!jwt.startsWith(jwt_header) || jwt.indexOf('.') != static_cast<int>(jwt_header_size)) {
return;
}
// check there is a signature delimieter
const int signatureDelimiterIndex = jwt.lastIndexOf('.');
if (signatureDelimiterIndex == static_cast<int>(jwt_header_size)) {
return;
}
// check the signature is valid
const String signature = jwt.substring(static_cast<unsigned int>(signatureDelimiterIndex) + 1);
jwt = jwt.substring(0, static_cast<unsigned int>(signatureDelimiterIndex));
if (sign(jwt) != signature) {
return;
}
// decode payload
jwt = jwt.substring(jwt_header_size + 1);
jwt = decode(jwt);
// parse payload, clearing json document after failure
const DeserializationError error = deserializeJson(jsonDocument, jwt);
if (error != DeserializationError::Ok || !jsonDocument.is<JsonObject>()) {
jsonDocument.clear();
}
}
/*
* ESP32 uses mbedtls, with decent HMAC implementations supporting sha256, as well as others.
* No need to pull in additional crypto libraries - lets use what we already have.
*/
String ArduinoJsonJWT::sign(String & payload) {
std::array<unsigned char, 32> hmacResult{};
{
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
mbedtls_md_hmac_starts(&ctx, reinterpret_cast<const unsigned char *>(_secret.c_str()), _secret.length());
mbedtls_md_hmac_update(&ctx, reinterpret_cast<const unsigned char *>(payload.c_str()), payload.length());
mbedtls_md_hmac_finish(&ctx, hmacResult.data());
mbedtls_md_free(&ctx);
}
return encode(reinterpret_cast<const char *>(hmacResult.data()), hmacResult.size());
}
String ArduinoJsonJWT::encode(const char * cstr, int inputLen) {
// prepare encoder
base64_encodestate _state;
base64_init_encodestate(&_state);
// prepare buffer of correct length
const auto bufferLength = static_cast<std::size_t>(base64_encode_expected_len(inputLen)) + 1;
auto * buffer = new char[bufferLength];
// encode to buffer
int len = base64_encode_block(cstr, inputLen, &buffer[0], &_state);
len += base64_encode_blockend(&buffer[len], &_state);
buffer[len] = '\0';
// convert to arduino string, freeing buffer
auto result = String(buffer);
delete[] buffer;
buffer = nullptr;
// remove padding and convert to URL safe form
while (result.length() > 0 && result.charAt(result.length() - 1) == '=') {
result.remove(result.length() - 1);
}
result.replace('+', '-');
result.replace('/', '_');
// return as string
return result;
}
String ArduinoJsonJWT::decode(String value) {
// convert to standard base64
value.replace('-', '+');
value.replace('_', '/');
// prepare buffer of correct length
const auto bufferLength = static_cast<std::size_t>(base64_decode_expected_len(value.length()) + 1);
auto * buffer = new char[bufferLength];
// decode
const int len = base64_decode_chars(value.c_str(), static_cast<int>(value.length()), &buffer[0]);
buffer[len] = '\0';
// convert to arduino string, freeing buffer
auto result = String(buffer);
delete[] buffer;
buffer = nullptr;
// return as string
return result;
}

View File

@@ -0,0 +1,35 @@
#ifndef ArduinoJsonJWT_H
#define ArduinoJsonJWT_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <libb64/cdecode.h>
#include <libb64/cencode.h>
#include <mbedtls/md.h>
class ArduinoJsonJWT {
public:
explicit ArduinoJsonJWT(String secret);
void setSecret(String secret);
String getSecret();
String buildJWT(JsonObject payload);
void parseJWT(String jwt, JsonDocument & jsonDocument);
private:
String _secret;
String sign(String & value);
static String encode(const char * cstr, int len);
static String decode(String value);
static const String & getJWTHeader() {
static const String JWT_HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
return JWT_HEADER;
}
};
#endif

View File

@@ -0,0 +1,37 @@
#include "AuthenticationService.h"
AuthenticationService::AuthenticationService(AsyncWebServer * server, SecurityManager * securityManager)
: _securityManager(securityManager) {
// none of these need authentication
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, [this](AsyncWebServerRequest * request) { verifyAuthorization(request); });
auto * handler = new AsyncCallbackJsonWebHandler(SIGN_IN_PATH);
handler->onRequest([this](AsyncWebServerRequest * request, JsonVariant json) { signIn(request, json); });
server->addHandler(handler);
}
// Verifies that the request supplied a valid JWT.
void AuthenticationService::verifyAuthorization(AsyncWebServerRequest * request) {
Authentication authentication = _securityManager->authenticateRequest(request);
request->send(authentication.authenticated ? 200 : 401);
}
// Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in subsequent requests.
void AuthenticationService::signIn(AsyncWebServerRequest * request, JsonVariant json) {
if (json.is<JsonObject>()) {
String username = json["username"];
String password = json["password"];
Authentication authentication = _securityManager->authenticate(username, password);
if (authentication.authenticated) {
User * user = authentication.user;
auto * response = new AsyncJsonResponse(false);
JsonObject jsonObject = response->getRoot();
jsonObject["access_token"] = _securityManager->generateJWT(user);
response->setLength();
request->send(response);
return;
}
}
AsyncWebServerResponse * response = request->beginResponse(401);
request->send(response);
}

View File

@@ -0,0 +1,22 @@
#ifndef AuthenticationService_H_
#define AuthenticationService_H_
#include "SecurityManager.h"
#include <ESPAsyncWebServer.h>
#define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization"
#define SIGN_IN_PATH "/rest/signIn"
class AuthenticationService {
public:
AuthenticationService(AsyncWebServer * server, SecurityManager * securityManager);
private:
SecurityManager * _securityManager;
void signIn(AsyncWebServerRequest * request, JsonVariant json);
void verifyAuthorization(AsyncWebServerRequest * request);
};
#endif

View File

@@ -0,0 +1,86 @@
#include "ESP32React.h"
#include "WWWData.h" // include auto-generated static web resources
ESP32React::ESP32React(AsyncWebServer * server, FS * fs)
: _securitySettingsService(server, fs)
, _networkSettingsService(server, fs, &_securitySettingsService)
, _wifiScanner(server, &_securitySettingsService)
, _networkStatus(server, &_securitySettingsService)
, _apSettingsService(server, fs, &_securitySettingsService)
, _apStatus(server, &_securitySettingsService, &_apSettingsService)
, _ntpSettingsService(server, fs, &_securitySettingsService)
, _ntpStatus(server, &_securitySettingsService)
, _uploadFileService(server, &_securitySettingsService)
, _mqttSettingsService(server, fs, &_securitySettingsService)
, _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService)
, _authenticationService(server, &_securitySettingsService) {
//
// Serve static web resources
//
// Populate the last modification date based on build datetime
static char last_modified[50];
sprintf(last_modified, "%s %s CET", __DATE__, __TIME__);
WWWData::registerRoutes([server](const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash) {
ArRequestHandlerFunction requestHandler = [contentType, content, len, hash](AsyncWebServerRequest * request) {
// Check if the client already has the same version and respond with a 304 (Not modified)
if (request->header("If-Modified-Since").indexOf(last_modified) > 0) {
return request->send(304);
} else if (request->header("If-None-Match").equals(hash)) {
return request->send(304);
}
AsyncWebServerResponse * response = request->beginResponse(200, contentType, content, len);
response->addHeader("Content-Encoding", "gzip");
// response->addHeader("Content-Encoding", "br"); // only works over HTTPS
// response->addHeader("Cache-Control", "public, immutable, max-age=31536000");
response->addHeader("Cache-Control", "must-revalidate"); // ensure that a client will check the server for a change
response->addHeader("Last-Modified", last_modified);
response->addHeader("ETag", hash);
request->send(response);
};
server->on(uri, HTTP_GET, requestHandler);
// Serving non matching get requests with "/index.html"
// OPTIONS get a straight up 200 response
if (strncmp(uri, "/index.html", 11) == 0) {
server->onNotFound([requestHandler](AsyncWebServerRequest * request) {
if (request->method() == HTTP_GET) {
requestHandler(request);
} else if (request->method() == HTTP_OPTIONS) {
request->send(200);
} else {
request->send(404);
}
});
}
});
}
void ESP32React::begin() {
_networkSettingsService.begin();
_networkSettingsService.read([&](NetworkSettings & networkSettings) {
DefaultHeaders & defaultHeaders = DefaultHeaders::Instance();
if (networkSettings.enableCORS) {
defaultHeaders.addHeader("Access-Control-Allow-Origin", networkSettings.CORSOrigin);
defaultHeaders.addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
defaultHeaders.addHeader("Access-Control-Allow-Credentials", "true");
}
defaultHeaders.addHeader("Server", networkSettings.hostname);
});
_apSettingsService.begin();
_ntpSettingsService.begin();
_mqttSettingsService.begin();
_securitySettingsService.begin();
}
void ESP32React::loop() {
_networkSettingsService.loop();
_apSettingsService.loop();
_mqttSettingsService.loop();
}

View File

@@ -0,0 +1,86 @@
#ifndef ESP32React_h
#define ESP32React_h
#include "APSettingsService.h"
#include "APStatus.h"
#include "AuthenticationService.h"
#include "MqttSettingsService.h"
#include "MqttStatus.h"
#include "NTPSettingsService.h"
#include "NTPStatus.h"
#include "UploadFileService.h"
#include "SecuritySettingsService.h"
#include "WiFiScanner.h"
#include "NetworkSettingsService.h"
#include "NetworkStatus.h"
#include <Arduino.h>
#include <AsyncJson.h>
#include <AsyncMessagePack.h>
#include <AsyncTCP.h>
#include <WiFi.h>
class ESP32React {
public:
ESP32React(AsyncWebServer * server, FS * fs);
void begin();
void loop();
SecurityManager * getSecurityManager() {
return &_securitySettingsService;
}
StatefulService<SecuritySettings> * getSecuritySettingsService() {
return &_securitySettingsService;
}
StatefulService<NetworkSettings> * getNetworkSettingsService() {
return &_networkSettingsService;
}
StatefulService<APSettings> * getAPSettingsService() {
return &_apSettingsService;
}
StatefulService<NTPSettings> * getNTPSettingsService() {
return &_ntpSettingsService;
}
StatefulService<MqttSettings> * getMqttSettingsService() {
return &_mqttSettingsService;
}
MqttClient * getMqttClient() {
return _mqttSettingsService.getMqttClient();
}
//
// special functions needed outside scope
//
// true if AP is active
bool apStatus() {
return _apSettingsService.getAPNetworkStatus() == APNetworkStatus::ACTIVE;
}
uint16_t getWifiReconnects() {
return _networkSettingsService.getWifiReconnects();
}
private:
SecuritySettingsService _securitySettingsService;
NetworkSettingsService _networkSettingsService;
WiFiScanner _wifiScanner;
NetworkStatus _networkStatus;
APSettingsService _apSettingsService;
APStatus _apStatus;
NTPSettingsService _ntpSettingsService;
NTPStatus _ntpStatus;
UploadFileService _uploadFileService;
MqttSettingsService _mqttSettingsService;
MqttStatus _mqttStatus;
AuthenticationService _authenticationService;
};
#endif

View File

@@ -0,0 +1,119 @@
#ifndef FSPersistence_h
#define FSPersistence_h
#include "StatefulService.h"
#include "FS.h"
template <class T>
class FSPersistence {
public:
FSPersistence(JsonStateReader<T> stateReader, JsonStateUpdater<T> stateUpdater, StatefulService<T> * statefulService, FS * fs, const char * filePath)
: _stateReader(stateReader)
, _stateUpdater(stateUpdater)
, _statefulService(statefulService)
, _fs(fs)
, _filePath(filePath)
, _updateHandlerId(0) {
enableUpdateHandler();
}
void readFromFS() {
File settingsFile = _fs->open(_filePath, "r");
if (settingsFile) {
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
#if defined(EMSESP_DEBUG)
// Serial.println();
// Serial.printf("Reading settings from %s ", _filePath);
// serializeJson(jsonDocument, Serial);
// Serial.println();
#endif
settingsFile.close();
return;
}
settingsFile.close();
}
// If we reach here we have not been successful in loading the config,
// hard-coded emergency defaults are now applied.
#if defined(EMSESP_DEBUG)
// Serial.println();
// Serial.printf("Applying defaults to %s", _filePath);
// Serial.println();
#endif
applyDefaults();
writeToFS(); // added to make sure the initial file is created
}
bool writeToFS() {
// create and populate a new json object
JsonDocument jsonDocument;
JsonObject jsonObject = jsonDocument.to<JsonObject>();
_statefulService->read(jsonObject, _stateReader);
// make directories if required, for new IDF4.2 & LittleFS
String path(_filePath);
int index = 0;
while ((index = path.indexOf('/', static_cast<unsigned int>(index) + 1)) != -1) {
String segment = path.substring(0, static_cast<unsigned int>(index));
if (!_fs->exists(segment)) {
_fs->mkdir(segment);
}
}
// serialize it to filesystem
File settingsFile = _fs->open(_filePath, "w");
// failed to open file, return false
if (!settingsFile || !jsonObject.size()) {
return false;
}
// serialize the data to the file
#if defined(EMSESP_DEBUG)
// Serial.println();
// Serial.printf("Writing settings to %s ", _filePath);
// serializeJson(jsonDocument, Serial);
// Serial.println();
#endif
serializeJson(jsonDocument, settingsFile);
settingsFile.close();
return true;
}
void disableUpdateHandler() {
if (_updateHandlerId) {
_statefulService->removeUpdateHandler(_updateHandlerId);
_updateHandlerId = 0;
}
}
void enableUpdateHandler() {
if (!_updateHandlerId) {
_updateHandlerId = _statefulService->addUpdateHandler([&] { writeToFS(); });
}
}
private:
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T> * _statefulService;
FS * _fs;
const char * _filePath;
update_handler_id_t _updateHandlerId;
protected:
// We assume the updater supplies sensible defaults if an empty object
// is supplied, this virtual function allows that to be changed.
virtual void applyDefaults() {
JsonDocument jsonDocument;
JsonObject jsonObject = jsonDocument.as<JsonObject>();
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
}
};
#endif

View File

@@ -0,0 +1,68 @@
#ifndef HttpEndpoint_h
#define HttpEndpoint_h
#include <functional>
#include <ESPAsyncWebServer.h>
#include "SecurityManager.h"
#include "StatefulService.h"
#define HTTP_ENDPOINT_ORIGIN_ID "http"
template <class T>
class HttpEndpoint {
protected:
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T> * _statefulService;
public:
HttpEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T> * statefulService,
AsyncWebServer * server,
const String & servicePath,
SecurityManager * securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN)
: _stateReader(stateReader)
, _stateUpdater(stateUpdater)
, _statefulService(statefulService) {
// Create handler for both GET and POST endpoints
securityManager->addEndpoint(
server, servicePath, authenticationPredicate, [this](AsyncWebServerRequest * request, JsonVariant json) { handleRequest(request, json); }, HTTP_ANY); // ALL methods
}
protected:
// for POST
void handleRequest(AsyncWebServerRequest * request, JsonVariant json) {
if (request->method() == HTTP_POST) {
// Handle POST
if (!json.is<JsonObject>()) {
request->send(400); // error, bad request
return;
}
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(json.as<JsonObject>(), _stateUpdater);
if (outcome == StateUpdateResult::ERROR) {
request->send(400); // error, bad request
return;
} else if (outcome == StateUpdateResult::CHANGED || outcome == StateUpdateResult::CHANGED_RESTART) {
// persist changes
request->onDisconnect([this] { _statefulService->callUpdateHandlers(); });
if (outcome == StateUpdateResult::CHANGED_RESTART) {
request->send(205); // reboot required
return;
}
}
}
auto * response = new AsyncJsonResponse(false);
JsonObject jsonObject = response->getRoot().to<JsonObject>();
_statefulService->read(jsonObject, _stateReader);
response->setLength();
request->send(response);
}
};
#endif

22
src/ESP32React/IPUtils.h Normal file
View File

@@ -0,0 +1,22 @@
#ifndef IPUtils_h
#define IPUtils_h
#include <IPAddress.h>
class IPUtils {
public:
static bool isSet(const IPAddress & ip) {
return ip != getNotSetIP();
}
static bool isNotSet(const IPAddress & ip) {
return ip == getNotSetIP();
}
private:
static const IPAddress & getNotSetIP() {
static const IPAddress IP_NOT_SET = IPAddress(INADDR_NONE);
return IP_NOT_SET;
}
};
#endif

View File

@@ -0,0 +1,31 @@
#ifndef JsonUtils_h
#define JsonUtils_h
#include <Arduino.h>
#include <ArduinoJson.h>
#include <IPAddress.h>
#include "IPUtils.h"
class JsonUtils {
public:
static void readIP(JsonObject root, const String & key, IPAddress & ip, const String & def) {
IPAddress defaultIp = {};
if (!defaultIp.fromString(def)) {
defaultIp = INADDR_NONE;
}
readIP(root, key, ip, defaultIp);
}
static void readIP(JsonObject root, const String & key, IPAddress & ip, const IPAddress & defaultIp = INADDR_NONE) {
if (!root[key].is<String>() || !ip.fromString(root[key].as<String>())) {
ip = defaultIp;
}
}
static void writeIP(JsonObject root, const String & key, const IPAddress & ip) {
if (IPUtils::isSet(ip)) {
root[key] = ip.toString();
}
}
};
#endif

View File

@@ -0,0 +1,404 @@
#include "MqttSettingsService.h"
#include <emsesp_stub.hpp>
MqttSettingsService::MqttSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(MqttSettings::read, MqttSettings::update, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(MqttSettings::read, MqttSettings::update, this, fs, MQTT_SETTINGS_FILE)
, _reconfigureMqtt(false)
, _disconnectedAt(0)
, _disconnectReason(espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED)
, _mqttClient(nullptr) {
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
addUpdateHandler([this] { onConfigUpdated(); }, false);
}
static String generateClientId() {
#ifdef EMSESP_STANDALONE
return "ems-esp";
#else
return "esp32-" + String(static_cast<uint32_t>(ESP.getEfuseMac()), HEX);
#endif
}
MqttSettingsService::~MqttSettingsService() {
delete _mqttClient;
}
void MqttSettingsService::begin() {
_fsPersistence.readFromFS();
startClient();
}
void MqttSettingsService::startClient() {
static bool isSecure = false;
if (_mqttClient != nullptr) {
// do we need to change the client?
if ((isSecure && _state.enableTLS) || (!isSecure && _state.enableTLS)) {
return;
}
delete _mqttClient;
_mqttClient = nullptr;
}
#ifndef TASMOTA_SDK
if (_state.enableTLS) {
isSecure = true;
if (emsesp::EMSESP::system_.PSram() > 0) {
_mqttClient = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::YES);
} else {
_mqttClient = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO);
}
if (_state.rootCA == "insecure") {
static_cast<espMqttClientSecure *>(_mqttClient)->setInsecure();
} else {
String certificate = "-----BEGIN CERTIFICATE-----\n" + _state.rootCA + "\n-----END CERTIFICATE-----\n";
static_cast<espMqttClientSecure *>(_mqttClient)->setCACert(certificate.c_str());
}
static_cast<espMqttClientSecure *>(_mqttClient)->onConnect([this](bool sessionPresent) { onMqttConnect(sessionPresent); });
static_cast<espMqttClientSecure *>(_mqttClient)->onDisconnect([this](espMqttClientTypes::DisconnectReason reason) { onMqttDisconnect(reason); });
static_cast<espMqttClientSecure *>(_mqttClient)
->onMessage(
[this](const espMqttClientTypes::MessageProperties & properties, const char * topic, const uint8_t * payload, size_t len, size_t index, size_t total) {
onMqttMessage(properties, topic, payload, len, index, total);
});
return;
}
#endif
isSecure = false;
if (emsesp::EMSESP::system_.PSram() > 0) {
_mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::YES);
} else {
_mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO);
}
static_cast<espMqttClient *>(_mqttClient)->onConnect([this](bool sessionPresent) { onMqttConnect(sessionPresent); });
static_cast<espMqttClient *>(_mqttClient)->onDisconnect([this](espMqttClientTypes::DisconnectReason reason) { onMqttDisconnect(reason); });
static_cast<espMqttClient *>(_mqttClient)
->onMessage(
[this](const espMqttClientTypes::MessageProperties & properties, const char * topic, const uint8_t * payload, size_t len, size_t index, size_t total) {
onMqttMessage(properties, topic, payload, len, index, total);
});
}
void MqttSettingsService::loop() {
if (_reconfigureMqtt || (_disconnectedAt && static_cast<uint32_t>(uuid::get_uptime() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) {
// reconfigure MQTT client
_disconnectedAt = configureMqtt() ? 0 : uuid::get_uptime();
_reconfigureMqtt = false;
}
if (emsesp::EMSESP::system_.PSram() == 0) {
_mqttClient->loop();
}
}
bool MqttSettingsService::isEnabled() {
return _state.enabled;
}
bool MqttSettingsService::isConnected() {
return _mqttClient->connected();
}
const char * MqttSettingsService::getClientId() {
return _mqttClient->getClientId();
}
void MqttSettingsService::onMqttMessage(const espMqttClientTypes::MessageProperties & properties,
const char * topic,
const uint8_t * payload,
size_t len,
size_t index,
size_t total) {
(void)properties;
(void)index;
(void)total;
emsesp::EMSESP::mqtt_.on_message(topic, payload, len);
}
espMqttClientTypes::DisconnectReason MqttSettingsService::getDisconnectReason() {
return _disconnectReason;
}
MqttClient * MqttSettingsService::getMqttClient() {
return _mqttClient;
}
void MqttSettingsService::onMqttConnect(bool sessionPresent) {
(void)sessionPresent;
emsesp::EMSESP::mqtt_.on_connect();
}
void MqttSettingsService::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) {
_disconnectReason = reason;
if (!_disconnectedAt) {
_disconnectedAt = uuid::get_uptime();
}
emsesp::EMSESP::mqtt_.on_disconnect(reason);
}
void MqttSettingsService::onConfigUpdated() {
_reconfigureMqtt = true;
_disconnectedAt = 0;
startClient();
emsesp::EMSESP::mqtt_.start(); // reload EMS-ESP MQTT settings
}
void MqttSettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP6:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
if (_state.enabled && !_mqttClient->connected()) {
onConfigUpdated();
}
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
case ARDUINO_EVENT_ETH_DISCONNECTED:
if (_state.enabled) {
_mqttClient->disconnect(true);
}
break;
default:
break;
}
}
bool MqttSettingsService::configureMqtt() {
// disconnect if already connected
if (_mqttClient->connected()) {
emsesp::EMSESP::logger().info("Disconnecting to configure");
_mqttClient->disconnect(true);
}
// only connect if WiFi is connected and MQTT is enabled
if (_state.enabled && emsesp::EMSESP::system_.network_connected() && !_state.host.isEmpty()) {
// create last will topic with the base prefixed. It has to be static because the client destroys the reference
static char will_topic[FACTORY_MQTT_MAX_TOPIC_LENGTH];
if (_state.base.isEmpty()) {
snprintf(will_topic, sizeof(will_topic), "status");
} else {
snprintf(will_topic, sizeof(will_topic), "%s/status", _state.base.c_str());
}
_reconfigureMqtt = false;
#ifndef TASMOTA_SDK
if (_state.enableTLS) {
#if defined(EMSESP_DEBUG)
emsesp::EMSESP::logger().debug("Start secure MQTT with rootCA");
#endif
static_cast<espMqttClientSecure *>(_mqttClient)->setServer(_state.host.c_str(), _state.port);
if (_state.username.length() > 0) {
static_cast<espMqttClientSecure *>(_mqttClient)
->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr);
}
static_cast<espMqttClientSecure *>(_mqttClient)->setClientId(_state.clientId.c_str());
static_cast<espMqttClientSecure *>(_mqttClient)->setKeepAlive(_state.keepAlive);
static_cast<espMqttClientSecure *>(_mqttClient)->setCleanSession(_state.cleanSession);
static_cast<espMqttClientSecure *>(_mqttClient)->setWill(will_topic, 1, true, "offline"); // QOS 1, retain
return _mqttClient->connect();
}
#endif
static_cast<espMqttClient *>(_mqttClient)->setServer(_state.host.c_str(), _state.port);
if (_state.username.length() > 0) {
static_cast<espMqttClient *>(_mqttClient)->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr);
}
static_cast<espMqttClient *>(_mqttClient)->setClientId(_state.clientId.c_str());
static_cast<espMqttClient *>(_mqttClient)->setKeepAlive(_state.keepAlive);
static_cast<espMqttClient *>(_mqttClient)->setCleanSession(_state.cleanSession);
static_cast<espMqttClient *>(_mqttClient)->setWill(will_topic, 1, true, "offline"); // QOS 1, retain
return _mqttClient->connect();
}
return false;
}
void MqttSettings::read(MqttSettings & settings, JsonObject root) {
#ifndef TASMOTA_SDK
root["enableTLS"] = settings.enableTLS;
root["rootCA"] = settings.rootCA;
#endif
root["enabled"] = settings.enabled;
root["host"] = settings.host;
root["port"] = settings.port;
root["base"] = settings.base;
root["username"] = settings.username;
root["password"] = settings.password;
root["client_id"] = settings.clientId;
root["keep_alive"] = settings.keepAlive;
root["clean_session"] = settings.cleanSession;
root["entity_format"] = settings.entity_format;
root["publish_time_boiler"] = settings.publish_time_boiler;
root["publish_time_thermostat"] = settings.publish_time_thermostat;
root["publish_time_solar"] = settings.publish_time_solar;
root["publish_time_mixer"] = settings.publish_time_mixer;
root["publish_time_water"] = settings.publish_time_water;
root["publish_time_other"] = settings.publish_time_other;
root["publish_time_sensor"] = settings.publish_time_sensor;
root["publish_time_heartbeat"] = settings.publish_time_heartbeat;
root["mqtt_qos"] = settings.mqtt_qos;
root["mqtt_retain"] = settings.mqtt_retain;
root["ha_enabled"] = settings.ha_enabled;
root["nested_format"] = settings.nested_format;
root["discovery_prefix"] = settings.discovery_prefix;
root["discovery_type"] = settings.discovery_type;
root["publish_single"] = settings.publish_single;
root["publish_single2cmd"] = settings.publish_single2cmd;
root["send_response"] = settings.send_response;
}
StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings) {
MqttSettings newSettings = {};
bool changed = false;
#ifndef TASMOTA_SDK
newSettings.enableTLS = root["enableTLS"];
newSettings.rootCA = root["rootCA"] | "";
#else
newSettings.enableTLS = false;
#endif
newSettings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
newSettings.host = root["host"] | FACTORY_MQTT_HOST;
newSettings.port = static_cast<uint16_t>(root["port"] | FACTORY_MQTT_PORT);
newSettings.base = root["base"] | FACTORY_MQTT_BASE;
newSettings.username = root["username"] | FACTORY_MQTT_USERNAME;
newSettings.password = root["password"] | FACTORY_MQTT_PASSWORD;
newSettings.clientId = root["client_id"] | generateClientId();
newSettings.keepAlive = static_cast<uint16_t>(root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE);
newSettings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION;
newSettings.mqtt_qos = static_cast<uint8_t>(root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS);
newSettings.mqtt_retain = root["mqtt_retain"] | EMSESP_DEFAULT_MQTT_RETAIN;
newSettings.publish_time_boiler = static_cast<uint16_t>(root["publish_time_boiler"] | EMSESP_DEFAULT_PUBLISH_TIME);
newSettings.publish_time_thermostat = static_cast<uint16_t>(root["publish_time_thermostat"] | EMSESP_DEFAULT_PUBLISH_TIME);
newSettings.publish_time_solar = static_cast<uint16_t>(root["publish_time_solar"] | EMSESP_DEFAULT_PUBLISH_TIME);
newSettings.publish_time_mixer = static_cast<uint16_t>(root["publish_time_mixer"] | EMSESP_DEFAULT_PUBLISH_TIME);
newSettings.publish_time_water = static_cast<uint16_t>(root["publish_time_water"] | EMSESP_DEFAULT_PUBLISH_TIME);
newSettings.publish_time_other = static_cast<uint16_t>(root["publish_time_other"] | EMSESP_DEFAULT_PUBLISH_TIME_OTHER);
newSettings.publish_time_sensor = static_cast<uint16_t>(root["publish_time_sensor"] | EMSESP_DEFAULT_PUBLISH_TIME);
newSettings.publish_time_heartbeat = static_cast<uint16_t>(root["publish_time_heartbeat"] | EMSESP_DEFAULT_PUBLISH_HEARTBEAT);
newSettings.ha_enabled = root["ha_enabled"] | EMSESP_DEFAULT_HA_ENABLED;
newSettings.nested_format = static_cast<uint8_t>(root["nested_format"] | EMSESP_DEFAULT_NESTED_FORMAT);
newSettings.discovery_prefix = root["discovery_prefix"] | EMSESP_DEFAULT_DISCOVERY_PREFIX;
newSettings.discovery_type = static_cast<uint8_t>(root["discovery_type"] | EMSESP_DEFAULT_DISCOVERY_TYPE);
newSettings.publish_single = root["publish_single"] | EMSESP_DEFAULT_PUBLISH_SINGLE;
newSettings.publish_single2cmd = root["publish_single2cmd"] | EMSESP_DEFAULT_PUBLISH_SINGLE2CMD;
newSettings.send_response = root["send_response"] | EMSESP_DEFAULT_SEND_RESPONSE;
newSettings.entity_format = static_cast<uint8_t>(root["entity_format"] | EMSESP_DEFAULT_ENTITY_FORMAT);
if (newSettings.enabled != settings.enabled) {
changed = true;
}
if (newSettings.mqtt_qos != settings.mqtt_qos) {
emsesp::EMSESP::mqtt_.set_qos(newSettings.mqtt_qos);
changed = true;
}
if (newSettings.nested_format != settings.nested_format) {
changed = true;
}
if (newSettings.discovery_prefix != settings.discovery_prefix) {
changed = true;
}
if (newSettings.discovery_type != settings.discovery_type) {
changed = true;
}
if (newSettings.entity_format != settings.entity_format) {
changed = true;
}
// if both settings are stored from older version, HA has priority
if (newSettings.ha_enabled && newSettings.publish_single) {
newSettings.publish_single = false;
}
if (newSettings.publish_single != settings.publish_single) {
if (newSettings.publish_single) {
newSettings.ha_enabled = false;
}
changed = true;
}
if (newSettings.publish_single2cmd != settings.publish_single2cmd) {
changed = true;
}
if (newSettings.send_response != settings.send_response) {
changed = true;
}
if (newSettings.ha_enabled != settings.ha_enabled) {
emsesp::EMSESP::mqtt_.ha_enabled(newSettings.ha_enabled);
if (newSettings.ha_enabled) {
newSettings.publish_single = false;
}
changed = true;
}
if (newSettings.mqtt_retain != settings.mqtt_retain) {
emsesp::EMSESP::mqtt_.set_retain(newSettings.mqtt_retain);
changed = true;
}
if (newSettings.publish_time_boiler != settings.publish_time_boiler) {
emsesp::EMSESP::mqtt_.set_publish_time_boiler(newSettings.publish_time_boiler);
}
if (newSettings.publish_time_thermostat != settings.publish_time_thermostat) {
emsesp::EMSESP::mqtt_.set_publish_time_thermostat(newSettings.publish_time_thermostat);
}
if (newSettings.publish_time_solar != settings.publish_time_solar) {
emsesp::EMSESP::mqtt_.set_publish_time_solar(newSettings.publish_time_solar);
}
if (newSettings.publish_time_mixer != settings.publish_time_mixer) {
emsesp::EMSESP::mqtt_.set_publish_time_mixer(newSettings.publish_time_mixer);
}
if (newSettings.publish_time_water != settings.publish_time_water) {
emsesp::EMSESP::mqtt_.set_publish_time_water(newSettings.publish_time_water);
}
if (newSettings.publish_time_other != settings.publish_time_other) {
emsesp::EMSESP::mqtt_.set_publish_time_other(newSettings.publish_time_other);
}
if (newSettings.publish_time_sensor != settings.publish_time_sensor) {
emsesp::EMSESP::mqtt_.set_publish_time_sensor(newSettings.publish_time_sensor);
}
if (newSettings.publish_time_heartbeat != settings.publish_time_heartbeat) {
emsesp::EMSESP::mqtt_.set_publish_time_heartbeat(newSettings.publish_time_heartbeat);
}
#ifndef TASMOTA_SDK
// strip down to certificate only
newSettings.rootCA.replace("\r", "");
newSettings.rootCA.replace("\n", "");
newSettings.rootCA.replace("-----BEGIN CERTIFICATE-----", "");
newSettings.rootCA.replace("-----END CERTIFICATE-----", "");
newSettings.rootCA.replace(" ", "");
if (newSettings.rootCA.length() == 0 && newSettings.enableTLS) {
newSettings.rootCA = "insecure";
}
if (newSettings.enableTLS != settings.enableTLS || newSettings.rootCA != settings.rootCA) {
changed = true;
}
#endif
// save the new settings
settings = newSettings;
if (changed) {
emsesp::EMSESP::mqtt_.reset_mqtt();
}
return StateUpdateResult::CHANGED;
}

View File

@@ -0,0 +1,131 @@
#ifndef MqttSettingsService_h
#define MqttSettingsService_h
#include "StatefulService.h"
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#include <espMqttClient.h>
#include <uuid/common.h>
#define MQTT_RECONNECTION_DELAY 2000 // 2 seconds
#define MQTT_SETTINGS_FILE "/config/mqttSettings.json"
#define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings"
#ifndef FACTORY_MQTT_ENABLED
#define FACTORY_MQTT_ENABLED false
#endif
#ifndef FACTORY_MQTT_HOST
#define FACTORY_MQTT_HOST "" // is blank
#endif
#ifndef FACTORY_MQTT_PORT
#define FACTORY_MQTT_PORT 1883
#endif
#ifndef FACTORY_MQTT_BASE
#define FACTORY_MQTT_BASE "ems-esp"
#endif
#ifndef FACTORY_MQTT_USERNAME
#define FACTORY_MQTT_USERNAME ""
#endif
#ifndef FACTORY_MQTT_PASSWORD
#define FACTORY_MQTT_PASSWORD ""
#endif
#ifndef FACTORY_MQTT_KEEP_ALIVE
#define FACTORY_MQTT_KEEP_ALIVE 16
#endif
#ifndef FACTORY_MQTT_CLEAN_SESSION
#define FACTORY_MQTT_CLEAN_SESSION false
#endif
#ifndef FACTORY_MQTT_MAX_TOPIC_LENGTH
#define FACTORY_MQTT_MAX_TOPIC_LENGTH 128
#endif
class MqttSettings {
public:
bool enabled;
String host;
uint16_t port;
String rootCA;
bool enableTLS;
String username;
String password;
String clientId;
uint16_t keepAlive;
bool cleanSession;
// EMS-ESP specific
String base;
uint16_t publish_time_boiler;
uint16_t publish_time_thermostat;
uint16_t publish_time_solar;
uint16_t publish_time_mixer;
uint16_t publish_time_water;
uint16_t publish_time_other;
uint16_t publish_time_sensor;
uint16_t publish_time_heartbeat;
uint8_t mqtt_qos;
bool mqtt_retain;
bool ha_enabled;
uint8_t nested_format;
String discovery_prefix;
uint8_t discovery_type;
bool publish_single;
bool publish_single2cmd;
bool send_response;
uint8_t entity_format;
static void read(MqttSettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, MqttSettings & settings);
};
class MqttSettingsService : public StatefulService<MqttSettings> {
public:
MqttSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
~MqttSettingsService();
void begin();
void startClient();
void loop();
bool isEnabled();
bool isConnected();
const char * getClientId();
espMqttClientTypes::DisconnectReason getDisconnectReason();
MqttClient * getMqttClient();
protected:
void onConfigUpdated();
private:
HttpEndpoint<MqttSettings> _httpEndpoint;
FSPersistence<MqttSettings> _fsPersistence;
// variable to help manage connection
bool _reconfigureMqtt;
unsigned long _disconnectedAt;
// connection status
espMqttClientTypes::DisconnectReason _disconnectReason;
// the MQTT client instance
MqttClient * _mqttClient;
void WiFiEvent(WiFiEvent_t event);
void onMqttConnect(bool sessionPresent);
void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason);
void
onMqttMessage(const espMqttClientTypes::MessageProperties & properties, const char * topic, const uint8_t * payload, size_t len, size_t index, size_t total);
bool configureMqtt();
};
#endif

View File

@@ -0,0 +1,27 @@
#include "MqttStatus.h"
#include <emsesp_stub.hpp>
MqttStatus::MqttStatus(AsyncWebServer * server, MqttSettingsService * mqttSettingsService, SecurityManager * securityManager)
: _mqttSettingsService(mqttSettingsService) {
securityManager->addEndpoint(server, MQTT_STATUS_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
mqttStatus(request);
});
}
void MqttStatus::mqttStatus(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["enabled"] = _mqttSettingsService->isEnabled();
root["connected"] = _mqttSettingsService->isConnected();
root["client_id"] = _mqttSettingsService->getClientId();
root["disconnect_reason"] = (uint8_t)_mqttSettingsService->getDisconnectReason();
root["mqtt_queued"] = emsesp::Mqtt::publish_queued();
root["mqtt_fails"] = emsesp::Mqtt::publish_fails();
root["connect_count"] = emsesp::Mqtt::connect_count();
response->setLength();
request->send(response);
}

View File

@@ -0,0 +1,23 @@
#ifndef MqttStatus_h
#define MqttStatus_h
#include <WiFi.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include "MqttSettingsService.h"
#include "SecurityManager.h"
#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus"
class MqttStatus {
public:
MqttStatus(AsyncWebServer * server, MqttSettingsService * mqttSettingsService, SecurityManager * securityManager);
private:
MqttSettingsService * _mqttSettingsService;
void mqttStatus(AsyncWebServerRequest * request);
};
#endif

View File

@@ -0,0 +1,101 @@
#include "NTPSettingsService.h"
#include <emsesp_stub.hpp>
NTPSettingsService::NTPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE)
, _connected(false) {
// POST
securityManager->addEndpoint(server, TIME_PATH, AuthenticationPredicates::IS_ADMIN, [this](AsyncWebServerRequest * request, JsonVariant json) {
configureTime(request, json);
});
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
addUpdateHandler([this] { configureNTP(); }, false);
}
void NTPSettingsService::begin() {
_fsPersistence.readFromFS();
configureNTP();
}
// handles both WiFI and Ethernet
void NTPSettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
case ARDUINO_EVENT_ETH_DISCONNECTED:
if (_connected) {
emsesp::EMSESP::logger().info("WiFi connection dropped, stopping NTP");
_connected = false;
configureNTP();
}
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP:
// emsesp::EMSESP::logger().info("Got IP address, starting NTP synchronization");
_connected = true;
configureNTP();
break;
default:
break;
}
}
// https://werner.rothschopf.net/microcontroller/202103_arduino_esp32_ntp_en.htm
void NTPSettingsService::configureNTP() {
emsesp::EMSESP::system_.ntp_connected(false);
if (_connected && _state.enabled) {
emsesp::EMSESP::logger().info("Starting NTP service");
esp_sntp_set_sync_interval(3600000); // one hour
esp_sntp_set_time_sync_notification_cb(ntp_received);
configTzTime(_state.tzFormat.c_str(), _state.server.c_str());
} else {
setenv("TZ", _state.tzFormat.c_str(), 1);
tzset();
esp_sntp_stop();
}
}
void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVariant json) {
if (json.is<JsonObject>()) {
struct tm tm = {};
String timeLocal = json["local_time"];
char * s = strptime(timeLocal.c_str(), "%Y-%m-%dT%H:%M:%S", &tm);
if (s != nullptr) {
tm.tm_isdst = -1; // not set by strptime, tells mktime to determine daylightsaving
time_t time = mktime(&tm);
struct timeval now = {.tv_sec = time, .tv_usec = {}};
settimeofday(&now, nullptr);
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
return;
}
}
AsyncWebServerResponse * response = request->beginResponse(400);
request->send(response);
}
void NTPSettingsService::ntp_received(struct timeval * tv) {
(void)tv;
// emsesp::EMSESP::logger().info("NTP sync to %d sec", tv->tv_sec);
emsesp::EMSESP::system_.ntp_connected(true);
}
void NTPSettings::read(NTPSettings & settings, JsonObject root) {
root["enabled"] = settings.enabled;
root["server"] = settings.server;
root["tz_label"] = settings.tzLabel;
root["tz_format"] = settings.tzFormat;
}
StateUpdateResult NTPSettings::update(JsonObject root, NTPSettings & settings) {
settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED;
settings.server = root["server"] | FACTORY_NTP_SERVER;
settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL;
settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT;
return StateUpdateResult::CHANGED;
}

View File

@@ -0,0 +1,59 @@
#ifndef NTPSettingsService_h
#define NTPSettingsService_h
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#include <ctime>
#include <esp_sntp.h>
#ifndef FACTORY_NTP_ENABLED
#define FACTORY_NTP_ENABLED true
#endif
#ifndef FACTORY_NTP_TIME_ZONE_LABEL
#define FACTORY_NTP_TIME_ZONE_LABEL "Europe/London"
#endif
#ifndef FACTORY_NTP_TIME_ZONE_FORMAT
#define FACTORY_NTP_TIME_ZONE_FORMAT "GMT0BST,M3.5.0/1,M10.5.0"
#endif
#ifndef FACTORY_NTP_SERVER
#define FACTORY_NTP_SERVER "time.google.com"
#endif
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
#define TIME_PATH "/rest/time"
class NTPSettings {
public:
bool enabled;
String tzLabel;
String tzFormat;
String server;
static void read(NTPSettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, NTPSettings & settings);
};
class NTPSettingsService : public StatefulService<NTPSettings> {
public:
NTPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
static void ntp_received(struct timeval * tv);
private:
HttpEndpoint<NTPSettings> _httpEndpoint;
FSPersistence<NTPSettings> _fsPersistence;
bool _connected;
void WiFiEvent(WiFiEvent_t event);
void configureNTP();
void configureTime(AsyncWebServerRequest * request, JsonVariant json);
};
#endif

View File

@@ -0,0 +1,62 @@
#include "NTPStatus.h"
#include <emsesp_stub.hpp>
#include <array>
NTPStatus::NTPStatus(AsyncWebServer * server, SecurityManager * securityManager) {
securityManager->addEndpoint(server, NTP_STATUS_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
ntpStatus(request);
});
}
/*
* Formats the time using the format provided.
*
* Uses a 25 byte buffer, large enough to fit an ISO time string with offset.
*/
String formatTime(tm * time, const char * format) {
std::array<char, 25> time_string{};
strftime(time_string.data(), time_string.size(), format, time);
return {time_string.data()};
}
String toUTCTimeString(tm * time) {
return formatTime(time, "%FT%TZ");
}
String toLocalTimeString(tm * time) {
return formatTime(time, "%FT%T");
}
void NTPStatus::ntpStatus(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
// grab the current instant in unix seconds
time_t now = time(nullptr);
// only provide enabled/disabled status for now
root["status"] = [] {
if (esp_sntp_enabled()) {
if (emsesp::EMSESP::system_.ntp_connected()) {
return 2;
} else {
return 1;
}
}
return 0;
}();
// the current time in UTC
root["utc_time"] = toUTCTimeString(gmtime(&now));
// local time with offset
root["local_time"] = toLocalTimeString(localtime(&now));
// the sntp server name
root["server"] = esp_sntp_getservername(0);
response->setLength();
request->send(response);
}

View File

@@ -0,0 +1,24 @@
#ifndef NTPStatus_h
#define NTPStatus_h
#include <ctime>
#include <WiFi.h>
#include <esp_sntp.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include "SecurityManager.h"
#include <uuid/common.h>
#define NTP_STATUS_SERVICE_PATH "/rest/ntpStatus"
class NTPStatus {
public:
NTPStatus(AsyncWebServer * server, SecurityManager * securityManager);
private:
void ntpStatus(AsyncWebServerRequest * request);
};
#endif

View File

@@ -0,0 +1,466 @@
#include "NetworkSettingsService.h"
#include <emsesp_stub.hpp>
NetworkSettingsService::NetworkSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(NetworkSettings::read, NetworkSettings::update, this, server, NETWORK_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(NetworkSettings::read, NetworkSettings::update, this, fs, NETWORK_SETTINGS_FILE)
, _lastConnectionAttempt(0)
, _stopping(false) {
addUpdateHandler([this] { reconfigureWiFiConnection(); }, false);
// Eth is also bound to the WifiGeneric event handler
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event, info); });
}
static bool formatBssid(const String & bssid, uint8_t (&mac)[6]) {
uint tmp[6];
if (bssid.isEmpty() || sscanf(bssid.c_str(), "%X:%X:%X:%X:%X:%X", &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5]) != 6) {
return false;
}
for (uint8_t i = 0; i < 6; i++) {
mac[i] = static_cast<uint8_t>(tmp[i]);
}
return true;
}
void NetworkSettingsService::begin() {
// TODO: may need to change this for Arduino Core 3.1 / IDF 5.x
// We want the device to come up in opmode=0 (WIFI_OFF), when erasing the flash this is not the default.
// If needed, we save opmode=0 before disabling persistence so the device boots with WiFi disabled in the future.
if (WiFi.getMode() != WIFI_OFF) {
WiFi.mode(WIFI_OFF);
}
// Disable WiFi config persistance and auto reconnect
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_MODE_MAX);
WiFi.mode(WIFI_MODE_NULL);
// WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // default is FAST_SCAN, connect issues in 2.0.14
// WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); // is default, no need to set
_fsPersistence.readFromFS();
}
void NetworkSettingsService::reconfigureWiFiConnection() {
// do not disconnect for switching to eth, restart is needed
if (WiFi.isConnected() && _state.ssid.length() == 0) {
return;
}
// disconnect and de-configure wifi
if (WiFi.disconnect(true)) {
_stopping = true;
}
}
void NetworkSettingsService::loop() {
unsigned long currentMillis = millis();
if (!_lastConnectionAttempt || static_cast<uint32_t>(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY) {
_lastConnectionAttempt = currentMillis;
manageSTA();
}
}
void NetworkSettingsService::manageSTA() {
// Abort if already connected, or if we have no SSID
if (WiFi.isConnected() || _state.ssid.length() == 0) {
#if ESP_IDF_VERSION_MAJOR >= 5
if (_state.ssid.length() == 0) {
ETH.enableIPv6(true);
}
#endif
return;
}
// Connect or reconnect as required
if ((WiFi.getMode() & WIFI_STA) == 0) {
#if ESP_IDF_VERSION_MAJOR >= 5
WiFi.enableIPv6(true);
#endif
if (_state.staticIPConfig) {
WiFi.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2); // configure for static IP
}
WiFi.setHostname(_state.hostname.c_str()); // set hostname
// www.esp32.com/viewtopic.php?t=12055
if (_state.bandwidth20) {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT20);
} else {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT40);
}
if (_state.nosleep) {
WiFi.setSleep(false); // turn off sleep - WIFI_PS_NONE
}
// attempt to connect to the network
uint8_t bssid[6];
if (formatBssid(_state.bssid, bssid)) {
WiFi.begin(_state.ssid.c_str(), _state.password.c_str(), 0, bssid);
} else {
WiFi.begin(_state.ssid.c_str(), _state.password.c_str());
}
#ifdef BOARD_C3_MINI_V1
// always hardcode Tx power for Wemos CS Mini v1
// v1 needs this value, see https://github.com/emsesp/EMS-ESP32/pull/620#discussion_r993173979
// https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
WiFi.setTxPower(WIFI_POWER_8_5dBm);
#else
if (_state.tx_power != 0) {
// if not set to Auto (0) set the Tx power now
if (!WiFi.setTxPower(static_cast<wifi_power_t>(_state.tx_power))) {
emsesp::EMSESP::logger().warning("Failed to set WiFi Tx Power");
}
}
#endif
} else { // not connected but STA-mode active => disconnect
reconfigureWiFiConnection();
}
}
// set the TxPower based on the RSSI (signal strength), picking the lowest value
// code is based of RSSI (signal strength) and copied from Tasmota's WiFiSetTXpowerBasedOnRssi() which is copied ESPEasy's ESPEasyWifi.SetWiFiTXpower() function
void NetworkSettingsService::setWiFiPowerOnRSSI() {
// Range ESP32 : 2dBm - 20dBm
// 802.11b - wifi1
// 802.11a - wifi2
// 802.11g - wifi3
// 802.11n - wifi4
// 802.11ac - wifi5
// 802.11ax - wifi6
int max_tx_pwr = MAX_TX_PWR_DBM_n; // assume wifi4
int threshold = WIFI_SENSITIVITY_n + 120; // Margin in dBm * 10 on top of threshold
// Assume AP sends with max set by ETSI standard.
// 2.4 GHz: 100 mWatt (20 dBm)
// US and some other countries allow 1000 mW (30 dBm)
int rssi = WiFi.RSSI() * 10;
int newrssi = rssi - 200; // We cannot send with over 20 dBm, thus it makes no sense to force higher TX power all the time.
int min_tx_pwr = 0;
if (newrssi < threshold) {
min_tx_pwr = threshold - newrssi;
}
if (min_tx_pwr > max_tx_pwr) {
min_tx_pwr = max_tx_pwr;
}
// from WiFIGeneric.h use:
// WIFI_POWER_19_5dBm = 78,// 19.5dBm
// WIFI_POWER_19dBm = 76,// 19dBm
// WIFI_POWER_18_5dBm = 74,// 18.5dBm
// WIFI_POWER_17dBm = 68,// 17dBm
// WIFI_POWER_15dBm = 60,// 15dBm
// WIFI_POWER_13dBm = 52,// 13dBm
// WIFI_POWER_11dBm = 44,// 11dBm
// WIFI_POWER_8_5dBm = 34,// 8.5dBm
// WIFI_POWER_7dBm = 28,// 7dBm
// WIFI_POWER_5dBm = 20,// 5dBm
// WIFI_POWER_2dBm = 8,// 2dBm
// WIFI_POWER_MINUS_1dBm = -4// -1dBm
wifi_power_t p = WIFI_POWER_2dBm;
if (min_tx_pwr > 185)
p = WIFI_POWER_19_5dBm;
else if (min_tx_pwr > 170)
p = WIFI_POWER_18_5dBm;
else if (min_tx_pwr > 150)
p = WIFI_POWER_17dBm;
else if (min_tx_pwr > 130)
p = WIFI_POWER_15dBm;
else if (min_tx_pwr > 110)
p = WIFI_POWER_13dBm;
else if (min_tx_pwr > 85)
p = WIFI_POWER_11dBm;
else if (min_tx_pwr > 70)
p = WIFI_POWER_8_5dBm;
else if (min_tx_pwr > 50)
p = WIFI_POWER_7dBm;
else if (min_tx_pwr > 20)
p = WIFI_POWER_5dBm;
#if defined(EMSESP_DEBUG)
// emsesp::EMSESP::logger().debug("Recommended WiFi Tx Power (set_power %d, new power %d, rssi %d, threshold %d)", min_tx_pwr / 10, p, rssi, threshold);
#endif
if (!WiFi.setTxPower(p)) {
emsesp::EMSESP::logger().warning("Failed to set WiFi Tx Power");
}
}
// start the multicast UDP service so EMS-ESP is discoverable via .local
void NetworkSettingsService::mDNS_start() const {
#ifndef EMSESP_STANDALONE
MDNS.end();
if (_state.enableMDNS) {
if (!MDNS.begin(emsesp::EMSESP::system_.hostname().c_str())) {
emsesp::EMSESP::logger().warning("Failed to start mDNS Responder service");
return;
}
std::string address_s = emsesp::EMSESP::system_.hostname() + ".local";
MDNS.addService("http", "tcp", 80); // add our web server and rest API
MDNS.addService("telnet", "tcp", 23); // add our telnet console
// MDNS.addServiceTxt("http", "tcp", "version", EMSESP_APP_VERSION);
MDNS.addServiceTxt("http", "tcp", "address", address_s.c_str());
emsesp::EMSESP::logger().info("Starting mDNS Responder service");
}
#endif
}
const char * NetworkSettingsService::disconnectReason(uint8_t code) {
#ifndef EMSESP_STANDALONE
switch (code) {
case WIFI_REASON_UNSPECIFIED: // = 1,
return "unspecified";
case WIFI_REASON_AUTH_EXPIRE: // = 2,
return "auth expire";
case WIFI_REASON_AUTH_LEAVE: // = 3,
return "auth leave";
case WIFI_REASON_ASSOC_EXPIRE: // = 4,
return "assoc expired";
case WIFI_REASON_ASSOC_TOOMANY: // = 5,
return "assoc too many";
case WIFI_REASON_NOT_AUTHED: // = 6,
return "not authenticated";
case WIFI_REASON_NOT_ASSOCED: // = 7,
return "not assoc";
case WIFI_REASON_ASSOC_LEAVE: // = 8,
return "assoc leave";
case WIFI_REASON_ASSOC_NOT_AUTHED: // = 9,
return "assoc not authed";
case WIFI_REASON_DISASSOC_PWRCAP_BAD: // = 10,
return "disassoc powerCAP bad";
case WIFI_REASON_DISASSOC_SUPCHAN_BAD: // = 11,
return "disassoc supchan bad";
case WIFI_REASON_IE_INVALID: // = 13,
return "IE invalid";
case WIFI_REASON_MIC_FAILURE: // = 14,
return "MIC failure";
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: // = 15,
return "4way handshake timeout";
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: // = 16,
return "group key-update timeout";
case WIFI_REASON_IE_IN_4WAY_DIFFERS: // = 17,
return "IE in 4way differs";
case WIFI_REASON_GROUP_CIPHER_INVALID: // = 18,
return "group cipher invalid";
case WIFI_REASON_PAIRWISE_CIPHER_INVALID: // = 19,
return "pairwise cipher invalid";
case WIFI_REASON_AKMP_INVALID: // = 20,
return "AKMP invalid";
case WIFI_REASON_UNSUPP_RSN_IE_VERSION: // = 21,
return "unsupported RSN_IE version";
case WIFI_REASON_INVALID_RSN_IE_CAP: // = 22,
return "invalid RSN_IE_CAP";
case WIFI_REASON_802_1X_AUTH_FAILED: // = 23,
return "802 X1 auth failed";
case WIFI_REASON_CIPHER_SUITE_REJECTED: // = 24,
return "cipher suite rejected";
case WIFI_REASON_BEACON_TIMEOUT: // = 200,
return "beacon timeout";
case WIFI_REASON_NO_AP_FOUND: // = 201,
return "no AP found";
case WIFI_REASON_AUTH_FAIL: // = 202,
return "auth fail";
case WIFI_REASON_ASSOC_FAIL: // = 203,
return "assoc fail";
case WIFI_REASON_HANDSHAKE_TIMEOUT: // = 204,
return "handshake timeout";
case WIFI_REASON_CONNECTION_FAIL: // 205,
return "connection fail";
case WIFI_REASON_AP_TSF_RESET: // 206,
return "AP tsf reset";
case WIFI_REASON_ROAMING: // 207,
return "roaming";
case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG: // 208,
return "assoc comeback time too long";
case WIFI_REASON_SA_QUERY_TIMEOUT: // 209,
return "sa query timeout";
default:
return "unknown";
}
#endif
return "";
}
// handles both WiFI and Ethernet
void NetworkSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
#ifndef EMSESP_STANDALONE
switch (event) {
case ARDUINO_EVENT_WIFI_STA_STOP:
if (_stopping) {
_lastConnectionAttempt = 0;
_stopping = false;
}
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
connectcount_++; // count the number of WiFi reconnects
emsesp::EMSESP::logger().warning("WiFi disconnected (#%d). Reason: %s (%d)",
connectcount_,
disconnectReason(info.wifi_sta_disconnected.reason),
info.wifi_sta_disconnected.reason); // IDF 4.0
emsesp::EMSESP::system_.has_ipv6(false);
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
char result[10];
emsesp::EMSESP::logger().info("WiFi connected (Local IP=%s, hostname=%s, TxPower=%s dBm)",
WiFi.localIP().toString().c_str(),
WiFi.getHostname(),
emsesp::Helpers::render_value(result, ((double)(WiFi.getTxPower()) / 4), 1));
mDNS_start();
break;
case ARDUINO_EVENT_ETH_START:
// configure for static IP
if (_state.staticIPConfig) {
ETH.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2);
}
ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str());
break;
case ARDUINO_EVENT_ETH_GOT_IP:
// prevent double calls to mDNS
if (!emsesp::EMSESP::system_.ethernet_connected()) {
emsesp::EMSESP::logger().info("Ethernet connected (Local IP=%s, speed %d Mbps)", ETH.localIP().toString().c_str(), ETH.linkSpeed());
emsesp::EMSESP::system_.ethernet_connected(true);
mDNS_start();
}
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
emsesp::EMSESP::logger().warning("Ethernet disconnected. Reason: %s (%d)",
disconnectReason(info.wifi_sta_disconnected.reason),
info.wifi_sta_disconnected.reason);
emsesp::EMSESP::system_.ethernet_connected(false);
emsesp::EMSESP::system_.has_ipv6(false);
break;
case ARDUINO_EVENT_ETH_STOP:
emsesp::EMSESP::logger().info("Ethernet stopped");
emsesp::EMSESP::system_.ethernet_connected(false);
emsesp::EMSESP::system_.has_ipv6(false);
break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
// Set the TxPower after the connection is established, if we're using TxPower = 0 (Auto)
if (_state.tx_power == 0) {
setWiFiPowerOnRSSI();
}
#if ESP_IDF_VERSION_MAJOR < 5
WiFi.enableIpV6(); // force ipv6
#endif
break;
case ARDUINO_EVENT_ETH_CONNECTED:
#if ESP_IDF_VERSION_MAJOR < 5
ETH.enableIpV6(); // force ipv6
#endif
break;
// IPv6 specific - WiFi/Eth
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
case ARDUINO_EVENT_ETH_GOT_IP6: {
#if !TASMOTA_SDK && ESP_IDF_VERSION_MAJOR < 5
auto ip6 = IPv6Address((uint8_t *)info.got_ip6.ip6_info.ip.addr).toString();
#else
auto ip6 = IPAddress(IPv6, (uint8_t *)info.got_ip6.ip6_info.ip.addr, 0).toString();
#endif
const char * link = event == ARDUINO_EVENT_ETH_GOT_IP6 ? "Eth" : "WiFi";
if (ip6.startsWith("fe80")) {
emsesp::EMSESP::logger().info("IPv6 (%s) local: %s", link, ip6.c_str());
} else if (ip6.startsWith("fd") || ip6.startsWith("fc")) {
emsesp::EMSESP::logger().info("IPv6 (%s) ULA: %s", link, ip6.c_str());
} else {
emsesp::EMSESP::logger().info("IPv6 (%s) global: %s", link, ip6.c_str());
}
emsesp::EMSESP::system_.has_ipv6(true);
} break;
default:
break;
}
#endif
}
void NetworkSettings::read(NetworkSettings & settings, JsonObject root) {
// connection settings
root["ssid"] = settings.ssid;
root["bssid"] = settings.bssid;
root["password"] = settings.password;
root["hostname"] = settings.hostname;
root["static_ip_config"] = settings.staticIPConfig;
root["bandwidth20"] = settings.bandwidth20;
root["nosleep"] = settings.nosleep;
root["enableMDNS"] = settings.enableMDNS;
root["enableCORS"] = settings.enableCORS;
root["CORSOrigin"] = settings.CORSOrigin;
root["tx_power"] = settings.tx_power;
// extended settings
JsonUtils::writeIP(root, "local_ip", settings.localIP);
JsonUtils::writeIP(root, "gateway_ip", settings.gatewayIP);
JsonUtils::writeIP(root, "subnet_mask", settings.subnetMask);
JsonUtils::writeIP(root, "dns_ip_1", settings.dnsIP1);
JsonUtils::writeIP(root, "dns_ip_2", settings.dnsIP2);
}
StateUpdateResult NetworkSettings::update(JsonObject root, NetworkSettings & settings) {
// keep copy of original settings
auto enableCORS = settings.enableCORS;
auto CORSOrigin = settings.CORSOrigin;
auto ssid = settings.ssid;
auto tx_power = settings.tx_power;
settings.ssid = root["ssid"] | FACTORY_WIFI_SSID;
settings.bssid = root["bssid"] | "";
settings.password = root["password"] | FACTORY_WIFI_PASSWORD;
settings.hostname = root["hostname"] | FACTORY_WIFI_HOSTNAME;
settings.staticIPConfig = root["static_ip_config"];
settings.bandwidth20 = root["bandwidth20"];
settings.tx_power = static_cast<uint8_t>(root["tx_power"] | 0);
settings.nosleep = root["nosleep"] | true;
settings.enableMDNS = root["enableMDNS"] | true;
settings.enableCORS = root["enableCORS"];
settings.CORSOrigin = root["CORSOrigin"] | "*";
// extended settings
JsonUtils::readIP(root, "local_ip", settings.localIP);
JsonUtils::readIP(root, "gateway_ip", settings.gatewayIP);
JsonUtils::readIP(root, "subnet_mask", settings.subnetMask);
JsonUtils::readIP(root, "dns_ip_1", settings.dnsIP1);
JsonUtils::readIP(root, "dns_ip_2", settings.dnsIP2);
// Swap around the dns servers if 2 is populated but 1 is not
if (IPUtils::isNotSet(settings.dnsIP1) && IPUtils::isSet(settings.dnsIP2)) {
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 (settings.staticIPConfig && (IPUtils::isNotSet(settings.localIP) || IPUtils::isNotSet(settings.gatewayIP) || IPUtils::isNotSet(settings.subnetMask))) {
settings.staticIPConfig = false;
}
// see if we need to inform the user of a restart
if (tx_power != settings.tx_power || enableCORS != settings.enableCORS || CORSOrigin != settings.CORSOrigin
|| (ssid != settings.ssid && settings.ssid.isEmpty())) {
return StateUpdateResult::CHANGED_RESTART; // tell WebUI that a restart is needed
}
return StateUpdateResult::CHANGED;
}

View File

@@ -0,0 +1,118 @@
#ifndef NetworkSettingsService_h
#define NetworkSettingsService_h
#include "StatefulService.h"
#include "FSPersistence.h"
#include "HttpEndpoint.h"
#include "JsonUtils.h"
#ifndef EMSESP_STANDALONE
#include <esp_wifi.h>
#include <ETH.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPmDNS.h>
#endif
#define NETWORK_SETTINGS_FILE "/config/networkSettings.json"
#define NETWORK_SETTINGS_SERVICE_PATH "/rest/networkSettings"
#define WIFI_RECONNECTION_DELAY (1000 * 3)
#ifndef FACTORY_WIFI_SSID
#define FACTORY_WIFI_SSID ""
#endif
#ifndef FACTORY_WIFI_PASSWORD
#define FACTORY_WIFI_PASSWORD ""
#endif
#ifndef FACTORY_WIFI_HOSTNAME
#define FACTORY_WIFI_HOSTNAME ""
#endif
// copied from Tasmota
#if CONFIG_IDF_TARGET_ESP32S2
#define MAX_TX_PWR_DBM_11b 195
#define MAX_TX_PWR_DBM_54g 150
#define MAX_TX_PWR_DBM_n 130
#define WIFI_SENSITIVITY_11b -880
#define WIFI_SENSITIVITY_54g -750
#define WIFI_SENSITIVITY_n -720
#elif CONFIG_IDF_TARGET_ESP32S3
#define MAX_TX_PWR_DBM_11b 210
#define MAX_TX_PWR_DBM_54g 190
#define MAX_TX_PWR_DBM_n 185
#define WIFI_SENSITIVITY_11b -880
#define WIFI_SENSITIVITY_54g -760
#define WIFI_SENSITIVITY_n -720
#elif CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3
#define MAX_TX_PWR_DBM_11b 210
#define MAX_TX_PWR_DBM_54g 190
#define MAX_TX_PWR_DBM_n 185
#define WIFI_SENSITIVITY_11b -880
#define WIFI_SENSITIVITY_54g -760
#define WIFI_SENSITIVITY_n -730
#else
#define MAX_TX_PWR_DBM_11b 195
#define MAX_TX_PWR_DBM_54g 160
#define MAX_TX_PWR_DBM_n 140
#define WIFI_SENSITIVITY_11b -880
#define WIFI_SENSITIVITY_54g -750
#define WIFI_SENSITIVITY_n -700
#endif
class NetworkSettings {
public:
// core wifi configuration
String ssid;
String bssid;
String password;
String hostname;
bool staticIPConfig;
bool bandwidth20;
uint8_t tx_power;
bool nosleep;
bool enableMDNS;
bool enableCORS;
String CORSOrigin;
// optional configuration for static IP address
IPAddress localIP;
IPAddress gatewayIP;
IPAddress subnetMask;
IPAddress dnsIP1;
IPAddress dnsIP2;
static void read(NetworkSettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, NetworkSettings & settings);
};
class NetworkSettingsService : public StatefulService<NetworkSettings> {
public:
NetworkSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
void loop();
uint16_t getWifiReconnects() const {
return connectcount_;
}
private:
HttpEndpoint<NetworkSettings> _httpEndpoint;
FSPersistence<NetworkSettings> _fsPersistence;
unsigned long _lastConnectionAttempt;
bool _stopping;
uint16_t connectcount_ = 0; // number of wifi reconnects
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
void mDNS_start() const;
const char * disconnectReason(uint8_t code);
void reconfigureWiFiConnection();
void manageSTA();
void setWiFiPowerOnRSSI();
};
#endif

View File

@@ -0,0 +1,93 @@
#include "NetworkStatus.h"
#include <emsesp_stub.hpp>
#ifdef TASMOTA_SDK
#include "lwip/dns.h"
#endif
NetworkStatus::NetworkStatus(AsyncWebServer * server, SecurityManager * securityManager) {
securityManager->addEndpoint(server, NETWORK_STATUS_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
networkStatus(request);
});
}
void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
bool ethernet_connected = emsesp::EMSESP::system_.ethernet_connected();
wl_status_t wifi_status = WiFi.status();
// see if Ethernet is connected
if (ethernet_connected) {
root["status"] = 10; // custom code #10 - ETHERNET_STATUS_CONNECTED
root["hostname"] = ETH.getHostname();
} else {
root["status"] = static_cast<uint8_t>(wifi_status);
root["hostname"] = WiFi.getHostname();
}
// for both connections show ethernet
if (ethernet_connected) {
// Ethernet
root["local_ip"] = ETH.localIP().toString();
#if ESP_IDF_VERSION_MAJOR < 5
root["local_ipv6"] = ETH.localIPv6().toString();
#else
root["local_ipv6"] = ETH.linkLocalIPv6().toString();
#endif
root["mac_address"] = ETH.macAddress();
root["subnet_mask"] = ETH.subnetMask().toString();
root["gateway_ip"] = ETH.gatewayIP().toString();
#ifdef TASMOTA_SDK
IPAddress dnsIP1 = IPAddress(dns_getserver(0));
IPAddress dnsIP2 = IPAddress(dns_getserver(1));
#else
IPAddress dnsIP1 = ETH.dnsIP(0);
IPAddress dnsIP2 = ETH.dnsIP(1);
#endif
if (IPUtils::isSet(dnsIP1)) {
root["dns_ip_1"] = dnsIP1.toString();
}
if (IPUtils::isSet(dnsIP2)) {
root["dns_ip_2"] = dnsIP2.toString();
}
} else if (wifi_status == WL_CONNECTED) {
root["local_ip"] = WiFi.localIP().toString();
// #if ESP_ARDUINO_VERSION_MAJOR < 3
#if ESP_IDF_VERSION_MAJOR < 5
root["local_ipv6"] = WiFi.localIPv6().toString();
#else
root["local_ipv6"] = WiFi.linkLocalIPv6().toString();
#endif
root["mac_address"] = WiFi.macAddress();
root["rssi"] = WiFi.RSSI();
root["ssid"] = WiFi.SSID();
root["bssid"] = WiFi.BSSIDstr();
root["channel"] = WiFi.channel();
root["reconnect_count"] = emsesp::EMSESP::esp32React.getWifiReconnects();
root["subnet_mask"] = WiFi.subnetMask().toString();
if (WiFi.gatewayIP() != INADDR_NONE) {
root["gateway_ip"] = WiFi.gatewayIP().toString();
}
#ifdef TASMOTA_SDK
IPAddress dnsIP1 = IPAddress(dns_getserver(0));
IPAddress dnsIP2 = IPAddress(dns_getserver(1));
#else
IPAddress dnsIP1 = WiFi.dnsIP(0);
IPAddress dnsIP2 = WiFi.dnsIP(1);
#endif
if (dnsIP1 != INADDR_NONE) {
root["dns_ip_1"] = dnsIP1.toString();
}
if (dnsIP2 != INADDR_NONE) {
root["dns_ip_2"] = dnsIP2.toString();
}
}
response->setLength();
request->send(response);
}

View File

@@ -0,0 +1,21 @@
#ifndef NetworkStatus_h
#define NetworkStatus_h
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <IPAddress.h>
#include "IPUtils.h"
#include "SecurityManager.h"
#define NETWORK_STATUS_SERVICE_PATH "/rest/networkStatus"
class NetworkStatus {
public:
NetworkStatus(AsyncWebServer * server, SecurityManager * securityManager);
private:
void networkStatus(AsyncWebServerRequest * request);
};
#endif

View File

@@ -0,0 +1,112 @@
#ifndef SecurityManager_h
#define SecurityManager_h
#include "ArduinoJsonJWT.h"
#include <ESPAsyncWebServer.h>
#include <AsyncJson.h>
#include <list>
#define ACCESS_TOKEN_PARAMATER "access_token"
#define AUTHORIZATION_HEADER "Authorization"
#define AUTHORIZATION_HEADER_PREFIX "Bearer "
#define AUTHORIZATION_HEADER_PREFIX_LEN 7
class User {
public:
String username;
String password;
bool admin;
public:
User(String username, String password, bool admin)
: username(std::move(username))
, password(std::move(password))
, admin(admin) {
}
};
class Authentication {
public:
User * user = nullptr;
boolean authenticated = false;
public:
explicit Authentication(const User & user)
: user(new User(user))
, authenticated(true) {
}
Authentication() = default;
~Authentication() {
delete user;
}
};
typedef std::function<boolean(Authentication & authentication)> AuthenticationPredicate;
class AuthenticationPredicates {
public:
static bool NONE_REQUIRED(const Authentication & authentication) {
(void)authentication;
return true;
};
static bool IS_AUTHENTICATED(const Authentication & authentication) {
return authentication.authenticated;
};
static bool IS_ADMIN(const Authentication & authentication) {
return authentication.authenticated && authentication.user->admin;
};
};
class SecurityManager {
public:
// Authenticate, returning the user if found
virtual Authentication authenticate(const String & username, const String & password) = 0;
// Generate a JWT for the user provided
virtual String generateJWT(const User * user) = 0;
// Check the request header for the Authorization token
virtual Authentication authenticateRequest(AsyncWebServerRequest * request) = 0;
// Filter a request with the provided predicate, only returning true if the predicate matches.
virtual ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0;
// Wrap the provided request to provide validation against an AuthenticationPredicate.
virtual ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate) = 0;
// Wrap the provided json request callback to provide validation against an AuthenticationPredicate.
virtual ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction onRequest, AuthenticationPredicate predicate) = 0;
// Json endpoints - default POST
void addEndpoint(AsyncWebServer * server,
const String & path,
AuthenticationPredicate predicate,
ArJsonRequestHandlerFunction function,
WebRequestMethodComposite method = HTTP_POST) {
auto handler = new AsyncCallbackJsonWebHandler(path);
handler->onRequest(
wrapCallback([this, function](AsyncWebServerRequest * request, JsonVariant json) { function(request, json); }, AuthenticationPredicates::IS_ADMIN));
handler->setMethod(method);
server->addHandler(handler);
}
// non-Json endpoints - default GET
void addEndpoint(AsyncWebServer * server,
const String & path,
AuthenticationPredicate predicate,
ArRequestHandlerFunction function,
WebRequestMethodComposite method = HTTP_GET) {
auto * handler = new AsyncCallbackWebHandler();
handler->onRequest(wrapRequest([this, function](AsyncWebServerRequest * request) { function(request); }, predicate));
handler->setUri(path);
handler->setMethod(method);
server->addHandler(handler);
}
};
#endif

View File

@@ -0,0 +1,124 @@
#include "SecuritySettingsService.h"
SecuritySettingsService::SecuritySettingsService(AsyncWebServer * server, FS * fs)
: _httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this)
, _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE)
, _jwtHandler(FACTORY_JWT_SECRET) {
addUpdateHandler([this] { configureJWTHandler(); }, false);
server->on(GENERATE_TOKEN_PATH,
HTTP_GET,
SecuritySettingsService::wrapRequest([this](AsyncWebServerRequest * request) { generateToken(request); }, AuthenticationPredicates::IS_ADMIN));
}
void SecuritySettingsService::begin() {
_fsPersistence.readFromFS();
configureJWTHandler();
}
Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest * request) {
auto 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);
}
} else if (request->hasParam(ACCESS_TOKEN_PARAMATER)) {
auto tokenParamater = request->getParam(ACCESS_TOKEN_PARAMATER);
String value = tokenParamater->value();
return authenticateJWT(value);
}
return {};
}
void SecuritySettingsService::configureJWTHandler() {
_jwtHandler.setSecret(_state.jwtSecret);
}
Authentication SecuritySettingsService::authenticateJWT(String & jwt) {
JsonDocument payloadDocument;
_jwtHandler.parseJWT(jwt, payloadDocument);
if (payloadDocument.is<JsonObject>()) {
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
String username = parsedPayload["username"];
for (const User & _user : _state.users) {
if (_user.username == username && validatePayload(parsedPayload, &_user)) {
return Authentication(_user);
}
}
}
return {};
}
Authentication SecuritySettingsService::authenticate(const String & username, const String & password) {
for (const User & _user : _state.users) {
if (_user.username == username && _user.password == password) {
return Authentication(_user);
}
}
return {};
}
inline void populateJWTPayload(JsonObject payload, const User * user) {
payload["username"] = user->username;
payload["admin"] = user->admin;
}
boolean SecuritySettingsService::validatePayload(JsonObject parsedPayload, const User * user) {
JsonDocument jsonDocument;
JsonObject payload = jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return payload == parsedPayload;
}
String SecuritySettingsService::generateJWT(const User * user) {
JsonDocument jsonDocument;
JsonObject payload = jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return _jwtHandler.buildJWT(payload);
}
ArRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) {
return [this, predicate](AsyncWebServerRequest * request) {
Authentication authentication = authenticateRequest(request);
return predicate(authentication);
};
}
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);
};
}
ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequestHandlerFunction onRequest, AuthenticationPredicate predicate) {
return [this, onRequest, predicate](AsyncWebServerRequest * request, JsonVariant json) {
Authentication authentication = authenticateRequest(request);
if (!predicate(authentication)) {
request->send(401);
return;
}
onRequest(request, json);
};
}
void SecuritySettingsService::generateToken(AsyncWebServerRequest * request) {
auto usernameParam = request->getParam("username");
for (const User & _user : _state.users) {
if (_user.username == usernameParam->value()) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["token"] = generateJWT(&_user);
response->setLength();
request->send(response);
return;
}
}
request->send(401);
}

View File

@@ -0,0 +1,94 @@
#ifndef SecuritySettingsService_h
#define SecuritySettingsService_h
#include "SecurityManager.h"
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#ifndef FACTORY_ADMIN_USERNAME
#define FACTORY_ADMIN_USERNAME "admin"
#endif
#ifndef FACTORY_ADMIN_PASSWORD
#define FACTORY_ADMIN_PASSWORD "admin"
#endif
#ifndef FACTORY_GUEST_USERNAME
#define FACTORY_GUEST_USERNAME "guest"
#endif
#ifndef FACTORY_GUEST_PASSWORD
#define FACTORY_GUEST_PASSWORD "guest"
#endif
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
#define GENERATE_TOKEN_SIZE 512
#define GENERATE_TOKEN_PATH "/rest/generateToken"
class SecuritySettings {
public:
String jwtSecret;
std::vector<User> users;
static void read(SecuritySettings & settings, JsonObject root) {
// secret
root["jwt_secret"] = settings.jwtSecret;
// users
JsonArray users = root["users"].to<JsonArray>();
for (const User & user : settings.users) {
JsonObject userRoot = users.add<JsonObject>();
userRoot["username"] = user.username;
userRoot["password"] = user.password;
userRoot["admin"] = user.admin;
}
}
static StateUpdateResult update(JsonObject root, SecuritySettings & settings) {
// secret
settings.jwtSecret = root["jwt_secret"] | FACTORY_JWT_SECRET;
// users
settings.users.clear();
if (root["users"].is<JsonArray>()) {
for (JsonVariant user : root["users"].as<JsonArray>()) {
settings.users.emplace_back(user["username"], user["password"], user["admin"]);
}
} else {
settings.users.emplace_back(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);
settings.users.emplace_back(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false);
}
return StateUpdateResult::CHANGED;
}
};
class SecuritySettingsService final : public StatefulService<SecuritySettings>, public SecurityManager {
public:
SecuritySettingsService(AsyncWebServer * server, FS * fs);
void begin();
Authentication authenticate(const String & username, const String & password) override;
Authentication authenticateRequest(AsyncWebServerRequest * request) override;
String generateJWT(const User * user) override;
ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate) override;
ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate) override;
ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate) override;
private:
HttpEndpoint<SecuritySettings> _httpEndpoint;
FSPersistence<SecuritySettings> _fsPersistence;
ArduinoJsonJWT _jwtHandler;
void generateToken(AsyncWebServerRequest * request);
void configureJWTHandler();
Authentication authenticateJWT(String & jwt); // Lookup the user by JWT
boolean validatePayload(JsonObject parsedPayload, const User * user); // Verify the payload is correct
};
#endif

View File

@@ -0,0 +1,3 @@
#include "StatefulService.h"
update_handler_id_t StateUpdateHandlerInfo::currentUpdatedHandlerId = 0;

View File

@@ -0,0 +1,137 @@
#ifndef StatefulService_h
#define StatefulService_h
#include <Arduino.h>
#include <ArduinoJson.h>
#include <vector>
#include <list>
#include <functional>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
enum class StateUpdateResult {
CHANGED = 0, // The update changed the state and propagation should take place if required
CHANGED_RESTART, // a restart of the device is needed
UNCHANGED, // The state was unchanged, propagation should not take place
ERROR // There was a problem updating the state, propagation should not take place
};
template <typename T>
using JsonStateUpdater = std::function<StateUpdateResult(JsonObject root, T & settings)>;
template <typename T>
using JsonStateReader = std::function<void(T & settings, JsonObject root)>;
typedef size_t update_handler_id_t;
typedef std::function<void()> StateUpdateCallback;
typedef struct StateUpdateHandlerInfo {
static update_handler_id_t currentUpdatedHandlerId;
update_handler_id_t _id;
StateUpdateCallback _cb;
bool _allowRemove;
StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove)
: _id(++currentUpdatedHandlerId)
, _cb(std::move(cb))
, _allowRemove(allowRemove) {};
} StateUpdateHandlerInfo_t;
template <class T>
class StatefulService {
public:
template <typename... Args>
explicit StatefulService(Args &&... args)
: _state(std::forward<Args>(args)...)
, _accessMutex(xSemaphoreCreateRecursiveMutex()) {
}
update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true) {
if (!cb) {
return 0;
}
StateUpdateHandlerInfo_t updateHandler(std::move(cb), allowRemove);
_updateHandlers.push_back(std::move(updateHandler));
return updateHandler._id;
}
void removeUpdateHandler(update_handler_id_t id) {
for (auto it = _updateHandlers.begin(); it != _updateHandlers.end();) {
auto & elem = *it;
if (elem._allowRemove && elem._id == id) {
it = _updateHandlers.erase(it);
} else {
++it;
}
}
}
StateUpdateResult update(std::function<StateUpdateResult(T &)> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(_state);
endTransaction();
if (result == StateUpdateResult::CHANGED) {
callUpdateHandlers();
}
return result;
}
StateUpdateResult updateWithoutPropagation(std::function<StateUpdateResult(T &)> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(_state);
endTransaction();
return result;
}
StateUpdateResult update(JsonObject jsonObject, JsonStateUpdater<T> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(jsonObject, _state);
endTransaction();
if (result == StateUpdateResult::CHANGED) {
callUpdateHandlers();
}
return result;
}
StateUpdateResult updateWithoutPropagation(JsonObject jsonObject, JsonStateUpdater<T> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(jsonObject, _state);
endTransaction();
return result;
}
void read(std::function<void(T &)> stateReader) {
beginTransaction();
stateReader(_state);
endTransaction();
}
void read(JsonObject jsonObject, JsonStateReader<T> stateReader) {
beginTransaction();
stateReader(_state, jsonObject);
endTransaction();
}
void callUpdateHandlers() {
for (const StateUpdateHandlerInfo_t & updateHandler : _updateHandlers) {
updateHandler._cb();
}
}
protected:
T _state;
inline void beginTransaction() {
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
}
inline void endTransaction() {
xSemaphoreGiveRecursive(_accessMutex);
}
private:
SemaphoreHandle_t _accessMutex;
std::vector<StateUpdateHandlerInfo_t> _updateHandlers;
};
#endif

View File

@@ -0,0 +1,167 @@
#include "UploadFileService.h"
#include <emsesp_stub.hpp>
#include <esp_app_format.h>
static String getFilenameExtension(const String & filename) {
const auto pos = filename.lastIndexOf('.');
if (pos != -1) {
return filename.substring(static_cast<unsigned int>(pos) + 1);
}
return {};
}
UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager * securityManager)
: _securityManager(securityManager)
, _is_firmware(false)
, _md5() {
// upload a file via a form
server->on(
UPLOAD_FILE_PATH,
HTTP_POST,
[this](AsyncWebServerRequest * request) { uploadComplete(request); },
[this](AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final) {
handleUpload(request, filename, index, data, len, final);
});
}
void UploadFileService::handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final) {
// quit if not authorized
Authentication authentication = _securityManager->authenticateRequest(request);
if (!AuthenticationPredicates::IS_ADMIN(authentication)) {
handleError(request, 403); // send the forbidden response
return;
}
// at init
if (!index) {
// check details of the file, to see if its a valid bin or json file
const String extension = getFilenameExtension(filename);
const std::size_t filesize = request->contentLength();
_is_firmware = false;
if ((extension == "bin") && (filesize > 1000000)) {
_is_firmware = true;
} else if (extension == "json") {
_md5[0] = '\0'; // clear md5
} else if (extension == "md5") {
if (len == _md5.size() - 1) {
std::memcpy(_md5.data(), data, _md5.size() - 1);
_md5.back() = '\0';
}
return;
} else {
_md5.front() = '\0';
handleError(request, 406); // Not Acceptable - unsupported file type
return;
}
if (_is_firmware) {
// Check firmware header, 0xE9 magic offset 0 indicates esp bin, chip offset 12: esp32:0, S2:2, C3:5
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
if (len > 12 && (data[0] != 0xE9 || data[12] != 0)) {
handleError(request, 503); // service unavailable
return;
}
#elif CONFIG_IDF_TARGET_ESP32S2
if (len > 12 && (data[0] != 0xE9 || data[12] != 2)) {
handleError(request, 503); // service unavailable
return;
}
#elif CONFIG_IDF_TARGET_ESP32C3
if (len > 12 && (data[0] != 0xE9 || data[12] != 5)) {
handleError(request, 503); // service unavailable
return;
}
#elif CONFIG_IDF_TARGET_ESP32S3
if (len > 12 && (data[0] != 0xE9 || data[12] != 9)) {
handleError(request, 503); // service unavailable
return;
}
#endif
// it's firmware - initialize the ArduinoOTA updater
if (Update.begin(filesize - sizeof(esp_image_header_t))) {
if (strlen(_md5.data()) == _md5.size() - 1) {
Update.setMD5(_md5.data());
_md5.front() = '\0';
}
request->onDisconnect([this] { handleEarlyDisconnect(); }); // success, let's make sure we end the update if the client hangs up
} else {
handleError(request, 507); // failed to begin, send an error response Insufficient Storage
return;
}
} else {
// its a normal file, open a new temp file to write the contents too
request->_tempFile = LittleFS.open(TEMP_FILENAME_PATH, "w");
}
}
if (!_is_firmware) {
if (len && len != request->_tempFile.write(data, len)) { // stream the incoming chunk to the opened file
handleError(request, 507); // 507-Insufficient Storage
}
} else if (!request->_tempObject) { // if we haven't delt with an error, continue with the firmware update
if (Update.write(data, len) != len) {
handleError(request, 500); // internal error, failed
return;
}
if (final && !Update.end(true)) {
handleError(request, 500); // internal error, failed
}
}
}
void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
// did we just complete uploading a json file?
if (request->_tempFile) {
request->_tempFile.close(); // close the file handle as the upload is now done
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
emsesp::EMSESP::system_.restart_pending(true); // will be handled by the main loop. We use pending for the Web's RestartMonitor
return;
}
// check if it was a firmware upgrade
// if no error, send the success response as a JSON
if (_is_firmware && !request->_tempObject) {
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
emsesp::EMSESP::system_.restart_pending(true); // will be handled by the main loop. We use pending for the Web's RestartMonitor
return;
}
if (strlen(_md5.data()) == _md5.size() - 1) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["md5"] = _md5.data();
response->setLength();
request->send(response);
return;
}
handleError(request, 500);
}
void UploadFileService::handleError(AsyncWebServerRequest * request, int code) {
// if we have had an error already, do nothing
if (request->_tempObject) {
return;
}
// send the error code to the client and record the error code in the temp object
AsyncWebServerResponse * response = request->beginResponse(code);
request->send(response);
// check for invalid extension and immediately kill the connection, which will through an error
// that is caught by the web code. Unfortunately the http error code is not sent to the client on fast network connections
if (code == 406) {
request->client()->close(true);
handleEarlyDisconnect();
}
}
void UploadFileService::handleEarlyDisconnect() {
_is_firmware = false;
Update.abort();
}

View File

@@ -0,0 +1,34 @@
#ifndef UploadFileService_h
#define UploadFileService_h
#include "SecurityManager.h"
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
#include <Update.h>
#include <WiFi.h>
#include <array>
#define UPLOAD_FILE_PATH "/rest/uploadFile"
#define TEMP_FILENAME_PATH "/pre_load.json" // for uploaded json files, handled by System::check_restore()
class UploadFileService {
public:
UploadFileService(AsyncWebServer * server, SecurityManager * securityManager);
private:
SecurityManager * _securityManager;
bool _is_firmware;
std::array<char, 33> _md5;
void handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final);
void uploadComplete(AsyncWebServerRequest * request);
void handleError(AsyncWebServerRequest * request, int code);
void handleEarlyDisconnect();
};
#endif

View File

@@ -0,0 +1,43 @@
#include "WiFiScanner.h"
WiFiScanner::WiFiScanner(AsyncWebServer * server, SecurityManager * securityManager) {
securityManager->addEndpoint(server, SCAN_NETWORKS_SERVICE_PATH, AuthenticationPredicates::IS_ADMIN, [this](AsyncWebServerRequest * request) {
scanNetworks(request);
});
securityManager->addEndpoint(server, LIST_NETWORKS_SERVICE_PATH, AuthenticationPredicates::IS_ADMIN, [this](AsyncWebServerRequest * request) {
listNetworks(request);
});
};
void WiFiScanner::scanNetworks(AsyncWebServerRequest * request) {
request->send(202); // special code to indicate scan in progress
if (WiFi.scanComplete() != -1) {
WiFi.scanDelete();
WiFi.scanNetworks(true);
}
}
void WiFiScanner::listNetworks(AsyncWebServerRequest * request) {
const int numNetworks = WiFi.scanComplete();
if (numNetworks > -1) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
JsonArray networks = root["networks"].to<JsonArray>();
for (uint8_t i = 0; i < numNetworks; i++) {
JsonObject network = networks.add<JsonObject>();
network["rssi"] = WiFi.RSSI(i);
network["ssid"] = WiFi.SSID(i);
network["bssid"] = WiFi.BSSIDstr(i);
network["channel"] = WiFi.channel(i);
network["encryption_type"] = static_cast<uint8_t>(WiFi.encryptionType(i));
}
response->setLength();
request->send(response);
} else if (numNetworks == -1) {
request->send(202); // special code to indicate scan in progress
} else {
scanNetworks(request);
}
}

View File

@@ -0,0 +1,23 @@
#ifndef WiFiScanner_h
#define WiFiScanner_h
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include "SecurityManager.h"
#define SCAN_NETWORKS_SERVICE_PATH "/rest/scanNetworks"
#define LIST_NETWORKS_SERVICE_PATH "/rest/listNetworks"
class WiFiScanner {
public:
WiFiScanner(AsyncWebServer * server, SecurityManager * securityManager);
private:
void scanNetworks(AsyncWebServerRequest * request);
void listNetworks(AsyncWebServerRequest * request);
};
#endif