This commit is contained in:
MichaelDvP
2024-02-15 09:08:01 +01:00
110 changed files with 1942 additions and 1923 deletions

View File

@@ -1,4 +1,4 @@
#include <APSettingsService.h>
#include "APSettingsService.h"
#include "../../src/emsesp_stub.hpp"
@@ -9,8 +9,8 @@ APSettingsService::APSettingsService(AsyncWebServer * server, FS * fs, SecurityM
, _lastManaged(0)
, _reconfigureAp(false)
, _connected(0) {
addUpdateHandler([&](const String & originId) { reconfigureAP(); }, false);
WiFi.onEvent(std::bind(&APSettingsService::WiFiEvent, this, _1));
addUpdateHandler([this] { reconfigureAP(); }, false);
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
}
void APSettingsService::begin() {
@@ -53,7 +53,7 @@ void APSettingsService::reconfigureAP() {
void APSettingsService::loop() {
unsigned long currentMillis = uuid::get_uptime();
unsigned long manageElapsed = (uint32_t)(currentMillis - _lastManaged);
unsigned long manageElapsed = static_cast<uint32_t>(currentMillis - _lastManaged);
if (manageElapsed >= MANAGE_NETWORK_DELAY) {
_lastManaged = currentMillis;
manageAP();
@@ -76,7 +76,7 @@ void APSettingsService::manageAP() {
void APSettingsService::startAP() {
WiFi.softAPConfig(_state.localIP, _state.gatewayIP, _state.subnetMask);
esp_wifi_set_bandwidth((wifi_interface_t)ESP_IF_WIFI_AP, WIFI_BW_HT20);
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
@@ -108,8 +108,54 @@ void APSettingsService::handleDNS() {
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, FACTORY_AP_LOCAL_IP);
JsonUtils::readIP(root, "gateway_ip", newSettings.gatewayIP, FACTORY_AP_GATEWAY_IP);
JsonUtils::readIP(root, "subnet_mask", newSettings.subnetMask, FACTORY_AP_SUBNET_MASK);
if (newSettings == settings) {
return StateUpdateResult::UNCHANGED;
}
settings = newSettings;
return StateUpdateResult::CHANGED;
}

View File

@@ -1,9 +1,9 @@
#ifndef APSettingsConfig_h
#define APSettingsConfig_h
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include <JsonUtils.h>
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#include "JsonUtils.h"
#include <DNSServer.h>
#include <IPAddress.h>
@@ -75,45 +75,8 @@ class APSettings {
&& subnetMask == settings.subnetMask;
}
static void read(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();
}
static StateUpdateResult update(JsonObject root, APSettings & settings) {
APSettings newSettings = {};
newSettings.provisionMode = 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 = root["channel"] | FACTORY_AP_CHANNEL;
newSettings.ssidHidden = root["ssid_hidden"] | FACTORY_AP_SSID_HIDDEN;
newSettings.maxClients = root["max_clients"] | FACTORY_AP_MAX_CLIENTS;
JsonUtils::readIP(root, "local_ip", newSettings.localIP, FACTORY_AP_LOCAL_IP);
JsonUtils::readIP(root, "gateway_ip", newSettings.gatewayIP, FACTORY_AP_GATEWAY_IP);
JsonUtils::readIP(root, "subnet_mask", newSettings.subnetMask, FACTORY_AP_SUBNET_MASK);
if (newSettings == settings) {
return StateUpdateResult::UNCHANGED;
}
settings = newSettings;
return StateUpdateResult::CHANGED;
}
static void read(const APSettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, APSettings & settings);
};
class APSettingsService : public StatefulService<APSettings> {

View File

@@ -1,17 +1,15 @@
#include <APStatus.h>
using namespace std::placeholders; // for `_1` etc
#include "APStatus.h"
APStatus::APStatus(AsyncWebServer * server, SecurityManager * securityManager, APSettingsService * apSettingsService)
: _apSettingsService(apSettingsService) {
server->on(AP_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { apStatus(request); }, AuthenticationPredicates::IS_AUTHENTICATED));
}
void APStatus::apStatus(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["status"] = _apSettingsService->getAPNetworkStatus();
root["ip_address"] = WiFi.softAPIP().toString();

View File

@@ -7,8 +7,9 @@
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <IPAddress.h>
#include <SecurityManager.h>
#include <APSettingsService.h>
#include "SecurityManager.h"
#include "APSettingsService.h"
#define AP_STATUS_SERVICE_PATH "/rest/apStatus"

View File

@@ -1,47 +1,27 @@
#include "ArduinoJsonJWT.h"
#include <array>
ArduinoJsonJWT::ArduinoJsonJWT(String secret)
: _secret(secret) {
: _secret(std::move(secret)) {
}
void ArduinoJsonJWT::setSecret(String secret) {
_secret = secret;
_secret = std::move(secret);
}
String ArduinoJsonJWT::getSecret() {
return _secret;
}
/*
* ESP32 uses mbedtls, ESP2866 uses bearssl.
*
* Both come 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) {
unsigned char hmacResult[32];
{
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, (unsigned char *)_secret.c_str(), _secret.length());
mbedtls_md_hmac_update(&ctx, (unsigned char *)payload.c_str(), payload.length());
mbedtls_md_hmac_finish(&ctx, hmacResult);
mbedtls_md_free(&ctx);
}
return encode((char *)hmacResult, 32);
}
String ArduinoJsonJWT::buildJWT(JsonObject payload) {
// serialize, then encode payload
String jwt;
serializeJson(payload, jwt);
jwt = encode(jwt.c_str(), jwt.length());
jwt = encode(jwt.c_str(), static_cast<int>(jwt.length()));
// add the header to payload
jwt = JWT_HEADER + '.' + jwt;
jwt = getJWTHeader() + '.' + jwt;
// add signature
jwt += '.' + sign(jwt);
@@ -53,65 +33,88 @@ 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('.') != JWT_HEADER_SIZE) {
if (!jwt.startsWith(jwt_header) || jwt.indexOf('.') != static_cast<int>(jwt_header_size)) {
return;
}
// check there is a signature delimieter
int signatureDelimiterIndex = jwt.lastIndexOf('.');
if (signatureDelimiterIndex == JWT_HEADER_SIZE) {
const int signatureDelimiterIndex = jwt.lastIndexOf('.');
if (signatureDelimiterIndex == static_cast<int>(jwt_header_size)) {
return;
}
// check the signature is valid
String signature = jwt.substring(signatureDelimiterIndex + 1);
jwt = jwt.substring(0, signatureDelimiterIndex);
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 = jwt.substring(jwt_header_size + 1);
jwt = decode(jwt);
// parse payload, clearing json document after failure
DeserializationError error = deserializeJson(jsonDocument, jwt);
const DeserializationError error = deserializeJson(jsonDocument, jwt);
if (error != DeserializationError::Ok || !jsonDocument.is<JsonObject>()) {
jsonDocument.clear();
}
}
/*
* ESP32 uses mbedtls, ESP2866 uses bearssl.
*
* Both come 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);
size_t encodedLength = base64_encode_expected_len(inputLen) + 1;
// prepare buffer of correct length, returning an empty string on failure
char * buffer = (char *)malloc(encodedLength * sizeof(char));
if (buffer == nullptr) {
return "";
}
// 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;
buffer[len] = '\0';
// convert to arduino string, freeing buffer
String value = String(buffer);
free(buffer);
auto result = String(buffer);
delete[] buffer;
buffer = nullptr;
// remove padding and convert to URL safe form
while (value.length() > 0 && value.charAt(value.length() - 1) == '=') {
value.remove(value.length() - 1);
while (result.length() > 0 && result.charAt(result.length() - 1) == '=') {
result.remove(result.length() - 1);
}
value.replace('+', '-');
value.replace('/', '_');
result.replace('+', '-');
result.replace('/', '_');
// return as string
return value;
return result;
}
String ArduinoJsonJWT::decode(String value) {
@@ -120,12 +123,18 @@ String ArduinoJsonJWT::decode(String value) {
value.replace('_', '/');
// prepare buffer of correct length
char buffer[base64_decode_expected_len(value.length()) + 1];
const auto bufferLength = static_cast<std::size_t>(base64_decode_expected_len(value.length()) + 1);
auto * buffer = new char[bufferLength];
// decode
int len = base64_decode_chars(value.c_str(), value.length(), &buffer[0]);
buffer[len] = 0;
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 String(buffer);
}
return result;
}

View File

@@ -3,30 +3,33 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <libb64/cdecode.h>
#include <libb64/cencode.h>
#include <mbedtls/md.h>
class ArduinoJsonJWT {
private:
String _secret;
const String JWT_HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
const int JWT_HEADER_SIZE = JWT_HEADER.length();
String sign(String & value);
static String encode(const char * cstr, int len);
static String decode(String value);
public:
ArduinoJsonJWT(String secret);
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
#endif

View File

@@ -1,11 +1,9 @@
#include <AuthenticationService.h>
using namespace std::placeholders; // for `_1` etc
#include "AuthenticationService.h"
AuthenticationService::AuthenticationService(AsyncWebServer * server, SecurityManager * securityManager)
: _securityManager(securityManager)
, _signInHandler(SIGN_IN_PATH, std::bind(&AuthenticationService::signIn, this, _1, _2)) {
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, _1));
, _signInHandler(SIGN_IN_PATH, [this](AsyncWebServerRequest * request, JsonVariant json) { signIn(request, json); }) {
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, [this](AsyncWebServerRequest * request) { verifyAuthorization(request); });
_signInHandler.setMethod(HTTP_POST);
_signInHandler.setMaxContentLength(MAX_AUTHENTICATION_SIZE);
server->addHandler(&_signInHandler);
@@ -29,10 +27,10 @@ void AuthenticationService::signIn(AsyncWebServerRequest * request, JsonVariant
String password = json["password"];
Authentication authentication = _securityManager->authenticate(username, password);
if (authentication.authenticated) {
User * user = authentication.user;
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject jsonObject = response->getRoot();
jsonObject["access_token"] = _securityManager->generateJWT(user);
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;

View File

@@ -1,9 +1,10 @@
#ifndef AuthenticationService_H_
#define AuthenticationService_H_
#include <Features.h>
#include "Features.h"
#include "SecurityManager.h"
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization"
#define SIGN_IN_PATH "/rest/signIn"

View File

@@ -1,6 +1,6 @@
#include <ESP8266React.h>
#include "ESP8266React.h"
#include <WWWData.h>
#include "WWWData.h"
ESP8266React::ESP8266React(AsyncWebServer * server, FS * fs)
: _securitySettingsService(server, fs)
@@ -19,15 +19,33 @@ ESP8266React::ESP8266React(AsyncWebServer * server, FS * fs)
, _restartService(server, &_securitySettingsService)
, _factoryResetService(server, fs, &_securitySettingsService)
, _systemStatus(server, &_securitySettingsService) {
// Serve static resources
WWWData::registerRoutes([server, this](const String & uri, const String & contentType, const uint8_t * content, size_t len) {
ArRequestHandlerFunction requestHandler = [contentType, content, len](AsyncWebServerRequest * request) {
//
// 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 String & 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_P(200, contentType, content, len);
response->addHeader("Content-Encoding", "gzip");
response->addHeader("Cache-Control", "public, immutable, max-age=31536000");
// response->addHeader("Content-Encoding", "br"); // only works over HTTPS
// response->addHeader("Cache-Control", "public, immutable, max-age=31536000");
response->addHeader("Last-Modified", last_modified);
response->addHeader("ETag", hash);
request->send(response);
};
server->on(uri.c_str(), HTTP_GET, requestHandler);
// Serving non matching get requests with "/index.html"
// OPTIONS get a straight up 200 response
@@ -48,12 +66,13 @@ ESP8266React::ESP8266React(AsyncWebServer * server, FS * fs)
void ESP8266React::begin() {
_networkSettingsService.begin();
_networkSettingsService.read([&](NetworkSettings & networkSettings) {
DefaultHeaders & defaultHeaders = DefaultHeaders::Instance();
if (networkSettings.enableCORS) {
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", networkSettings.CORSOrigin);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
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::Instance().addHeader("Server", networkSettings.hostname);
defaultHeaders.addHeader("Server", networkSettings.hostname);
});
_apSettingsService.begin();
_ntpSettingsService.begin();

View File

@@ -1,28 +1,27 @@
#ifndef ESP8266React_h
#define ESP8266React_h
#include <Arduino.h>
#include "APSettingsService.h"
#include "APStatus.h"
#include "AuthenticationService.h"
#include "FactoryResetService.h"
#include "MqttSettingsService.h"
#include "MqttStatus.h"
#include "NTPSettingsService.h"
#include "NTPStatus.h"
#include "OTASettingsService.h"
#include "UploadFileService.h"
#include "RestartService.h"
#include "SecuritySettingsService.h"
#include "SystemStatus.h"
#include "WiFiScanner.h"
#include "NetworkSettingsService.h"
#include "NetworkStatus.h"
#include <Arduino.h>
#include <AsyncTCP.h>
#include <WiFi.h>
#include <APSettingsService.h>
#include <APStatus.h>
#include <AuthenticationService.h>
#include <FactoryResetService.h>
#include <MqttSettingsService.h>
#include <MqttStatus.h>
#include <NTPSettingsService.h>
#include <NTPStatus.h>
#include <OTASettingsService.h>
#include <UploadFileService.h>
#include <RestartService.h>
#include <SecuritySettingsService.h>
#include <SystemStatus.h>
#include <WiFiScanner.h>
#include <NetworkSettingsService.h>
#include <NetworkStatus.h>
class ESP8266React {
public:
ESP8266React(AsyncWebServer * server, FS * fs);
@@ -66,9 +65,11 @@ class ESP8266React {
_mqttSettingsService.setWill(will_topic);
}
#ifndef EMSESP_STANDALONE
void factoryReset() {
_factoryResetService.factoryReset();
}
#endif
private:
SecuritySettingsService _securitySettingsService;

View File

@@ -1,13 +0,0 @@
#ifndef ESPUtils_h
#define ESPUtils_h
#include <Arduino.h>
class ESPUtils {
public:
static String defaultDeviceValue(String prefix = "") {
return prefix + String((uint32_t)ESP.getEfuseMac(), HEX);
}
};
#endif

View File

@@ -1,8 +1,8 @@
#ifndef FSPersistence_h
#define FSPersistence_h
#include <StatefulService.h>
#include <FS.h>
#include "StatefulService.h"
#include "FS.h"
template <class T>
class FSPersistence {
@@ -47,8 +47,8 @@ class FSPersistence {
// make directories if required, for new IDF4.2 & LittleFS
String path(_filePath);
int index = 0;
while ((index = path.indexOf('/', index + 1)) != -1) {
String segment = path.substring(0, index);
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);
}
@@ -80,7 +80,7 @@ class FSPersistence {
void enableUpdateHandler() {
if (!_updateHandlerId) {
_updateHandlerId = _statefulService->addUpdateHandler([&](const String & originId) { writeToFS(); });
_updateHandlerId = _statefulService->addUpdateHandler([&] { writeToFS(); });
}
}

View File

@@ -1,16 +1,14 @@
#include <FactoryResetService.h>
using namespace std::placeholders;
#include "FactoryResetService.h"
FactoryResetService::FactoryResetService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: fs(fs) {
server->on(FACTORY_RESET_SERVICE_PATH,
HTTP_POST,
securityManager->wrapRequest(std::bind(&FactoryResetService::handleRequest, this, _1), AuthenticationPredicates::IS_ADMIN));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { handleRequest(request); }, AuthenticationPredicates::IS_ADMIN));
}
void FactoryResetService::handleRequest(AsyncWebServerRequest * request) {
request->onDisconnect(std::bind(&FactoryResetService::factoryReset, this));
request->onDisconnect([this] { factoryReset(); });
request->send(200);
}
@@ -21,7 +19,7 @@ void FactoryResetService::factoryReset() {
// TODO To replaced with fs.rmdir(FS_CONFIG_DIRECTORY) now we're using IDF 4.2
File root = fs->open(FS_CONFIG_DIRECTORY);
File file;
while (file = root.openNextFile()) {
while ((file = root.openNextFile())) {
String path = file.path();
file.close();
fs->remove(path);

View File

@@ -3,22 +3,22 @@
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <RestartService.h>
#include <FS.h>
#include "SecurityManager.h"
#include "RestartService.h"
#define FS_CONFIG_DIRECTORY "/config"
#define FACTORY_RESET_SERVICE_PATH "/rest/factoryReset"
class FactoryResetService {
FS * fs;
public:
FactoryResetService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void factoryReset();
private:
FS * fs;
void handleRequest(AsyncWebServerRequest * request);
};

View File

@@ -2,15 +2,13 @@
#define HttpEndpoint_h
#include <functional>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <StatefulService.h>
#include "SecurityManager.h"
#include "StatefulService.h"
#define HTTP_ENDPOINT_ORIGIN_ID "http"
using namespace std::placeholders; // for `_1` etc
#define HTTPS_ENDPOINT_ORIGIN_ID "https"
template <class T>
class HttpEndpoint {
@@ -19,8 +17,7 @@ class HttpEndpoint {
JsonStateUpdater<T> _stateUpdater;
StatefulService<T> * _statefulService;
AsyncCallbackWebHandler * GEThandler;
AsyncCallbackJsonWebHandler * POSThandler;
AsyncCallbackJsonWebHandler * handler;
public:
HttpEndpoint(JsonStateReader<T> stateReader,
@@ -33,12 +30,12 @@ class HttpEndpoint {
: _stateReader(stateReader)
, _stateUpdater(stateUpdater)
, _statefulService(statefulService) {
// Create the GET and POST endpoints
POSThandler = new AsyncCallbackJsonWebHandler(servicePath,
securityManager->wrapCallback([this](AsyncWebServerRequest * request,
JsonVariant json) { handleRequest(request, json); },
authenticationPredicate));
server->addHandler(POSThandler);
// Create hander for both GET and POST endpoints
handler = new AsyncCallbackJsonWebHandler(servicePath,
securityManager->wrapCallback([this](AsyncWebServerRequest * request,
JsonVariant json) { handleRequest(request, json); },
authenticationPredicate));
server->addHandler(handler);
}
protected:
@@ -56,16 +53,18 @@ class HttpEndpoint {
if (outcome == StateUpdateResult::ERROR) {
request->send(400); // error
return;
} else if (outcome == StateUpdateResult::CHANGED_RESTART) {
request->send(205); // reboot required
return;
} else if (outcome == StateUpdateResult::CHANGED) {
request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); });
} 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;
}
}
}
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject jsonObject = response->getRoot().to<JsonObject>();
auto * response = new AsyncJsonResponse(false);
JsonObject jsonObject = response->getRoot().to<JsonObject>();
_statefulService->read(jsonObject, _stateReader);
response->setLength();
request->send(response);

View File

@@ -3,16 +3,20 @@
#include <IPAddress.h>
const IPAddress IP_NOT_SET = IPAddress(INADDR_NONE);
class IPUtils {
public:
static bool isSet(const IPAddress & ip) {
return ip != IP_NOT_SET;
return ip != getNotSetIP();
}
static bool isNotSet(const IPAddress & ip) {
return ip == IP_NOT_SET;
return ip == getNotSetIP();
}
private:
static const IPAddress & getNotSetIP() {
static const IPAddress IP_NOT_SET = IPAddress(INADDR_NONE);
return IP_NOT_SET;
}
};
#endif
#endif

View File

@@ -2,9 +2,10 @@
#define JsonUtils_h
#include <Arduino.h>
#include <IPAddress.h>
#include <IPUtils.h>
#include <ArduinoJson.h>
#include <IPAddress.h>
#include "IPUtils.h"
class JsonUtils {
public:

View File

@@ -1,45 +1,27 @@
#include <MqttSettingsService.h>
#include "MqttSettingsService.h"
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
/**
* Retains a copy of the cstr provided in the pointer provided using dynamic allocation.
*
* Frees the pointer before allocation and leaves it as nullptr if cstr == nullptr.
*/
static char * retainCstr(const char * cstr, char ** ptr) {
// free up previously retained value if exists
free(*ptr);
*ptr = nullptr;
// dynamically allocate and copy cstr (if non null)
if (cstr != nullptr) {
*ptr = (char *)malloc(strlen(cstr) + 1);
strcpy(*ptr, cstr);
}
// return reference to pointer for convenience
return *ptr;
}
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)
, _retainedHost(nullptr)
, _retainedClientId(nullptr)
, _retainedUsername(nullptr)
, _retainedPassword(nullptr)
, _reconfigureMqtt(false)
, _disconnectedAt(0)
, _disconnectReason(espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED)
, _mqttClient(nullptr) {
WiFi.onEvent(std::bind(&MqttSettingsService::WiFiEvent, this, _1, _2));
addUpdateHandler([&](const String & originId) { onConfigUpdated(); }, false);
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() {
@@ -55,32 +37,41 @@ void MqttSettingsService::startClient() {
return;
}
delete _mqttClient;
_mqttClient = nullptr;
}
#if CONFIG_IDF_TARGET_ESP32S3
if (_state.enableTLS) {
isSecure = true;
_mqttClient = static_cast<MqttClient *>(new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO));
_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(retainCstr(certificate.c_str(), &_retainedRootCA));
static_cast<espMqttClientSecure *>(_mqttClient)->setCACert(certificate.c_str());
}
static_cast<espMqttClientSecure *>(_mqttClient)->onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, _1));
static_cast<espMqttClientSecure *>(_mqttClient)->onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, _1));
static_cast<espMqttClientSecure *>(_mqttClient)->onMessage(std::bind(&MqttSettingsService::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
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;
_mqttClient = static_cast<MqttClient *>(new espMqttClient(espMqttClientTypes::UseInternalTask::NO));
static_cast<espMqttClient *>(_mqttClient)->onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, _1));
static_cast<espMqttClient *>(_mqttClient)->onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, _1));
static_cast<espMqttClient *>(_mqttClient)->onMessage(std::bind(&MqttSettingsService::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
_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 && (uint32_t)(uuid::get_uptime() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) {
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;
@@ -116,6 +107,9 @@ void MqttSettingsService::onMqttMessage(const espMqttClientTypes::MessagePropert
size_t len,
size_t index,
size_t total) {
(void)properties;
(void)index;
(void)total;
emsesp::EMSESP::mqtt_.on_message(topic, payload, len);
}
@@ -128,9 +122,12 @@ MqttClient * MqttSettingsService::getMqttClient() {
}
void MqttSettingsService::onMqttConnect(bool sessionPresent) {
(void)sessionPresent;
// _disconnectedAt = 0;
emsesp::EMSESP::mqtt_.on_connect();
// emsesp::EMSESP::logger().info("Connected to MQTT, %s", (sessionPresent) ? ("with persistent session") : ("without persistent session"));
#ifdef EMSESP_DEBUG
emsesp::EMSESP::logger().debug("Connected to MQTT, %s", (sessionPresent) ? ("with persistent session") : ("without persistent session"));
#endif
}
void MqttSettingsService::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) {
@@ -149,7 +146,7 @@ void MqttSettingsService::onConfigUpdated() {
emsesp::EMSESP::mqtt_.start(); // reload EMS-ESP MQTT settings
}
void MqttSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
void MqttSettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP:
@@ -183,31 +180,25 @@ bool MqttSettingsService::configureMqtt() {
_reconfigureMqtt = false;
#if CONFIG_IDF_TARGET_ESP32S3
if (_state.enableTLS) {
// emsesp::EMSESP::logger().info("Start secure MQTT with rootCA");
static_cast<espMqttClientSecure *>(_mqttClient)->setServer(retainCstr(_state.host.c_str(), &_retainedHost), _state.port);
#if 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(retainCstr(_state.username.c_str(), &_retainedUsername),
retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword));
} else {
static_cast<espMqttClientSecure *>(_mqttClient)->setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword));
->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr);
}
static_cast<espMqttClientSecure *>(_mqttClient)->setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId));
static_cast<espMqttClientSecure *>(_mqttClient)->setClientId(_state.clientId.c_str());
static_cast<espMqttClientSecure *>(_mqttClient)->setKeepAlive(_state.keepAlive);
static_cast<espMqttClientSecure *>(_mqttClient)->setCleanSession(_state.cleanSession);
return _mqttClient->connect();
}
#endif
// emsesp::EMSESP::logger().info("Configuring MQTT client");
static_cast<espMqttClient *>(_mqttClient)->setServer(retainCstr(_state.host.c_str(), &_retainedHost), _state.port);
static_cast<espMqttClient *>(_mqttClient)->setServer(_state.host.c_str(), _state.port);
if (_state.username.length() > 0) {
static_cast<espMqttClient *>(_mqttClient)
->setCredentials(retainCstr(_state.username.c_str(), &_retainedUsername),
retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword));
} else {
static_cast<espMqttClient *>(_mqttClient)->setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword));
static_cast<espMqttClient *>(_mqttClient)->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr);
}
static_cast<espMqttClient *>(_mqttClient)->setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId));
static_cast<espMqttClient *>(_mqttClient)->setClientId(_state.clientId.c_str());
static_cast<espMqttClient *>(_mqttClient)->setKeepAlive(_state.keepAlive);
static_cast<espMqttClient *>(_mqttClient)->setCleanSession(_state.cleanSession);
return _mqttClient->connect();
@@ -265,33 +256,33 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
#endif
newSettings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
newSettings.host = root["host"] | FACTORY_MQTT_HOST;
newSettings.port = root["port"] | FACTORY_MQTT_PORT;
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"] | FACTORY_MQTT_CLIENT_ID;
newSettings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE;
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 = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS;
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 = root["publish_time_boiler"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_thermostat = root["publish_time_thermostat"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_solar = root["publish_time_solar"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_mixer = root["publish_time_mixer"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_water = root["publish_time_water"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_other = root["publish_time_other"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_sensor = root["publish_time_sensor"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_heartbeat = root["publish_time_heartbeat"] | EMSESP_DEFAULT_PUBLISH_HEARTBEAT;
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);
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 = root["nested_format"] | EMSESP_DEFAULT_NESTED_FORMAT;
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 = root["discovery_type"] | EMSESP_DEFAULT_DISCOVERY_TYPE;
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 = root["entity_format"] | EMSESP_DEFAULT_ENTITY_FORMAT;
newSettings.entity_format = static_cast<uint8_t>(root["entity_format"] | EMSESP_DEFAULT_ENTITY_FORMAT);
if (newSettings.enabled != settings.enabled) {
changed = true;

View File

@@ -1,11 +1,11 @@
#ifndef MqttSettingsService_h
#define MqttSettingsService_h
#include <StatefulService.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include "StatefulService.h"
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#include <espMqttClient.h>
#include <ESPUtils.h>
#include <uuid/common.h>
@@ -38,13 +38,6 @@
#define FACTORY_MQTT_PASSWORD ""
#endif
#ifndef FACTORY_MQTT_CLIENT_ID
#define FACTORY_MQTT_CLIENT_ID generateClientId()
static String generateClientId() {
return ESPUtils::defaultDeviceValue("esp32-");
}
#endif
#ifndef FACTORY_MQTT_KEEP_ALIVE
#define FACTORY_MQTT_KEEP_ALIVE 16
#endif
@@ -59,21 +52,14 @@ static String generateClientId() {
class MqttSettings {
public:
// host and port - if enabled
bool enabled;
String host;
uint16_t port;
String rootCA;
bool enableTLS;
// username and password
String username;
String password;
// client id settings
String clientId;
// connection settings
String username;
String password;
String clientId;
uint16_t keepAlive;
bool cleanSession;
@@ -124,14 +110,6 @@ class MqttSettingsService : public StatefulService<MqttSettings> {
HttpEndpoint<MqttSettings> _httpEndpoint;
FSPersistence<MqttSettings> _fsPersistence;
// Pointers to hold retained copies of the mqtt client connection strings.
// This is required as espMqttClient holds references to the supplied connection strings.
char * _retainedHost;
char * _retainedClientId;
char * _retainedUsername;
char * _retainedPassword;
char * _retainedRootCA;
// variable to help manage connection
bool _reconfigureMqtt;
unsigned long _disconnectedAt;
@@ -142,11 +120,12 @@ class MqttSettingsService : public StatefulService<MqttSettings> {
// the MQTT client instance
MqttClient * _mqttClient;
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
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();
};

View File

@@ -1,19 +1,17 @@
#include <MqttStatus.h>
#include "MqttStatus.h"
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
MqttStatus::MqttStatus(AsyncWebServer * server, MqttSettingsService * mqttSettingsService, SecurityManager * securityManager)
: _mqttSettingsService(mqttSettingsService) {
server->on(MQTT_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { mqttStatus(request); }, AuthenticationPredicates::IS_AUTHENTICATED));
}
void MqttStatus::mqttStatus(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["enabled"] = _mqttSettingsService->isEnabled();
root["connected"] = _mqttSettingsService->isConnected();

View File

@@ -2,10 +2,11 @@
#define MqttStatus_h
#include <WiFi.h>
#include <MqttSettingsService.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include "MqttSettingsService.h"
#include "SecurityManager.h"
#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus"

View File

@@ -1,20 +1,20 @@
#include <NTPSettingsService.h>
#include "NTPSettingsService.h"
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
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)
, _timeHandler(TIME_PATH, securityManager->wrapCallback(std::bind(&NTPSettingsService::configureTime, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) {
, _timeHandler(TIME_PATH,
securityManager->wrapCallback([this](AsyncWebServerRequest * request, JsonVariant json) { configureTime(request, json); },
AuthenticationPredicates::IS_ADMIN))
, _connected(false) {
_timeHandler.setMethod(HTTP_POST);
_timeHandler.setMaxContentLength(MAX_TIME_SIZE);
server->addHandler(&_timeHandler);
WiFi.onEvent(std::bind(&NTPSettingsService::WiFiEvent, this, _1));
addUpdateHandler([&](const String & originId) { configureNTP(); }, false);
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
addUpdateHandler([this] { configureNTP(); }, false);
}
void NTPSettingsService::begin() {
@@ -27,9 +27,9 @@ void NTPSettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
case ARDUINO_EVENT_ETH_DISCONNECTED:
if (connected_) {
if (_connected) {
emsesp::EMSESP::logger().info("WiFi connection dropped, stopping NTP");
connected_ = false;
_connected = false;
configureNTP();
}
break;
@@ -37,7 +37,7 @@ void NTPSettingsService::WiFiEvent(WiFiEvent_t event) {
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;
_connected = true;
configureNTP();
break;
@@ -49,7 +49,7 @@ void NTPSettingsService::WiFiEvent(WiFiEvent_t event) {
// https://werner.rothschopf.net/microcontroller/202103_arduino_esp32_ntp_en.htm
void NTPSettingsService::configureNTP() {
emsesp::EMSESP::system_.ntp_connected(false);
if (connected_ && _state.enabled) {
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);
@@ -63,13 +63,13 @@ void NTPSettingsService::configureNTP() {
void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVariant json) {
if (json.is<JsonObject>()) {
struct tm tm = {0};
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};
struct timeval now = {.tv_sec = time, .tv_usec = {}};
settimeofday(&now, nullptr);
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
@@ -82,6 +82,22 @@ void NTPSettingsService::configureTime(AsyncWebServerRequest * request, JsonVari
}
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

@@ -1,10 +1,10 @@
#ifndef NTPSettingsService_h
#define NTPSettingsService_h
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#include <time.h>
#include <ctime>
#include <esp_sntp.h>
#ifndef FACTORY_NTP_ENABLED
@@ -36,20 +36,8 @@ class NTPSettings {
String tzFormat;
String server;
static void read(NTPSettings & settings, JsonObject root) {
root["enabled"] = settings.enabled;
root["server"] = settings.server;
root["tz_label"] = settings.tzLabel;
root["tz_format"] = settings.tzFormat;
}
static StateUpdateResult 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;
}
static void read(NTPSettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, NTPSettings & settings);
};
class NTPSettingsService : public StatefulService<NTPSettings> {
@@ -63,8 +51,8 @@ class NTPSettingsService : public StatefulService<NTPSettings> {
HttpEndpoint<NTPSettings> _httpEndpoint;
FSPersistence<NTPSettings> _fsPersistence;
AsyncCallbackJsonWebHandler _timeHandler;
bool _connected;
bool connected_ = false;
void WiFiEvent(WiFiEvent_t event);
void configureNTP();
void configureTime(AsyncWebServerRequest * request, JsonVariant json);

View File

@@ -1,12 +1,13 @@
#include <NTPStatus.h>
#include "NTPStatus.h"
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
#include <array>
NTPStatus::NTPStatus(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(NTP_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { ntpStatus(request); }, AuthenticationPredicates::IS_AUTHENTICATED));
}
/*
@@ -15,9 +16,9 @@ NTPStatus::NTPStatus(AsyncWebServer * server, SecurityManager * securityManager)
* Uses a 25 byte buffer, large enough to fit an ISO time string with offset.
*/
String formatTime(tm * time, const char * format) {
char time_string[25];
strftime(time_string, 25, format, time);
return String(time_string);
std::array<char, 25> time_string{};
strftime(time_string.data(), time_string.size(), format, time);
return {time_string.data()};
}
String toUTCTimeString(tm * time) {
@@ -29,14 +30,23 @@ String toLocalTimeString(tm * time) {
}
void NTPStatus::ntpStatus(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
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"] = esp_sntp_enabled() ? emsesp::EMSESP::system_.ntp_connected() ? 2 : 1 : 0;
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));
@@ -49,4 +59,4 @@ void NTPStatus::ntpStatus(AsyncWebServerRequest * request) {
response->setLength();
request->send(response);
}
}

View File

@@ -1,14 +1,14 @@
#ifndef NTPStatus_h
#define NTPStatus_h
#include <time.h>
#include <ctime>
#include <WiFi.h>
#include <esp_sntp.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include "SecurityManager.h"
#include <uuid/common.h>
#define NTP_STATUS_SERVICE_PATH "/rest/ntpStatus"

View File

@@ -1,13 +1,25 @@
#include <NetworkSettingsService.h>
#include "NetworkSettingsService.h"
using namespace std::placeholders; // for `_1` etc
#include "../../src/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) {
addUpdateHandler([&](const String & originId) { reconfigureWiFiConnection(); }, false);
WiFi.onEvent(std::bind(&NetworkSettingsService::WiFiEvent, this, _1));
, _lastConnectionAttempt(0)
, _stopping(false) {
addUpdateHandler([this] { reconfigureWiFiConnection(); }, false);
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() {
@@ -44,7 +56,7 @@ void NetworkSettingsService::reconfigureWiFiConnection() {
void NetworkSettingsService::loop() {
unsigned long currentMillis = millis();
if (!_lastConnectionAttempt || (uint32_t)(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY) {
if (!_lastConnectionAttempt || static_cast<uint32_t>(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY) {
_lastConnectionAttempt = currentMillis;
manageSTA();
}
@@ -65,49 +77,379 @@ void NetworkSettingsService::manageSTA() {
// www.esp32.com/viewtopic.php?t=12055
if (_state.bandwidth20) {
esp_wifi_set_bandwidth((wifi_interface_t)ESP_IF_WIFI_STA, WIFI_BW_HT20);
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT20);
} else {
esp_wifi_set_bandwidth((wifi_interface_t)ESP_IF_WIFI_STA, WIFI_BW_HT40);
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
uint mac[6];
if (!_state.bssid.isEmpty() && sscanf(_state.bssid.c_str(), "%X:%X:%X:%X:%X:%X", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) == 6) {
uint8_t mac1[6];
for (uint8_t i = 0; i < 6; i++) {
mac1[i] = (uint8_t)mac[i];
}
WiFi.begin(_state.ssid.c_str(), _state.password.c_str(), 0, mac1);
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());
}
// set power after wifi is startet, fixed value for C3_V1
// if (WiFi.isConnected()) {
#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
WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
// https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
WiFi.setTxPower(WIFI_POWER_8_5dBm);
#else
// esp_wifi_set_max_tx_power(_state.tx_power * 4);
// TODO make it dynamic
WiFi.setTxPower((wifi_power_t)(_state.tx_power * 4));
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();
}
}
// handles if wifi stopped
void NetworkSettingsService::WiFiEvent(WiFiEvent_t event) {
if (event == ARDUINO_EVENT_WIFI_STA_STOP) {
// 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 + 70; // 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;
}
uint8_t set_power = min_tx_pwr / 10; // this is the recommended power setting to use
// 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;
#ifdef EMSESP_DEBUG
emsesp::EMSESP::logger().debug("Recommended set WiFi Tx Power (set_power %d, new power %d, rssi %d, threshold %d)", set_power, p, rssi, threshold);
#else
char result[10];
emsesp::EMSESP::logger().info("Setting WiFi Tx Power to %s dBm", emsesp::Helpers::render_value(result, ((double)(p) / 4), 1));
#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:
emsesp::EMSESP::logger().warning("WiFi disconnected. Reason: %s (%d)",
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 (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:
ETH.setHostname(emsesp::EMSESP::system_.hostname().c_str());
// configure for static IP
if (_state.staticIPConfig) {
ETH.config(_state.localIP, _state.gatewayIP, _state.subnetMask, _state.dnsIP1, _state.dnsIP2);
}
break;
case ARDUINO_EVENT_ETH_GOT_IP:
// prevent double calls
if (!emsesp::EMSESP::system_.ethernet_connected()) {
emsesp::EMSESP::logger().info("Ethernet connected (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");
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 (_state.enableIPv6) {
WiFi.enableIpV6();
}
break;
case ARDUINO_EVENT_ETH_CONNECTED:
if (_state.enableIPv6) {
ETH.enableIpV6();
}
break;
// IPv6 specific
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
case ARDUINO_EVENT_ETH_GOT_IP6:
if (emsesp::EMSESP::system_.ethernet_connected()) {
emsesp::EMSESP::logger().info("Ethernet connected (IPv6=%s, speed %d Mbps)", ETH.localIPv6().toString().c_str(), ETH.linkSpeed());
} else {
emsesp::EMSESP::logger().info("WiFi connected (IPv6=%s, hostname=%s, TxPower=%s dBm)",
WiFi.localIPv6().toString().c_str(),
WiFi.getHostname(),
emsesp::Helpers::render_value(result, ((double)(WiFi.getTxPower()) / 4), 1));
}
mDNS_start();
emsesp::EMSESP::system_.has_ipv6(true);
break;
default:
break;
}
// if (!_stopping && (event == ARDUINO_EVENT_WIFI_STA_LOST_IP || event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED)) {
// reconfigureWiFiConnection();
// }
#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["enableIPv6"] = settings.enableIPv6;
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"] | false;
settings.enableIPv6 = root["enableIPv6"] | false;
settings.bandwidth20 = root["bandwidth20"] | false;
settings.tx_power = static_cast<uint8_t>(root["tx_power"] | 0);
settings.nosleep = root["nosleep"] | false;
settings.enableMDNS = root["enableMDNS"] | true;
settings.enableCORS = root["enableCORS"] | false;
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

@@ -1,19 +1,22 @@
#ifndef NetworkSettingsService_h
#define NetworkSettingsService_h
#include <StatefulService.h>
#include <FSPersistence.h>
#include <HttpEndpoint.h>
#include <JsonUtils.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
#define WIFI_RECONNECTION_DELAY (1000 * 3)
#ifndef FACTORY_WIFI_SSID
#define FACTORY_WIFI_SSID ""
@@ -27,21 +30,52 @@
#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 enableIPv6;
bool bandwidth20;
int8_t tx_power;
bool nosleep;
bool enableMDNS;
bool enableCORS;
String CORSOrigin;
String ssid;
String bssid;
String password;
String hostname;
bool staticIPConfig;
bool enableIPv6;
bool bandwidth20;
uint8_t tx_power;
bool nosleep;
bool enableMDNS;
bool enableCORS;
String CORSOrigin;
// optional configuration for static IP address
IPAddress localIP;
@@ -50,73 +84,11 @@ class NetworkSettings {
IPAddress dnsIP1;
IPAddress dnsIP2;
static void 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["enableIPv6"] = settings.enableIPv6;
root["bandwidth20"] = settings.bandwidth20;
root["tx_power"] = settings.tx_power;
root["nosleep"] = settings.nosleep;
root["enableMDNS"] = settings.enableMDNS;
root["enableCORS"] = settings.enableCORS;
root["CORSOrigin"] = settings.CORSOrigin;
// 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);
}
static StateUpdateResult update(JsonObject root, NetworkSettings & settings) {
auto enableCORS = settings.enableCORS;
auto CORSOrigin = settings.CORSOrigin;
auto ssid = settings.ssid;
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"] | false;
settings.enableIPv6 = root["enableIPv6"] | false;
settings.bandwidth20 = root["bandwidth20"] | false;
settings.tx_power = root["tx_power"] | 20;
settings.nosleep = root["nosleep"] | false;
settings.enableMDNS = root["enableMDNS"] | true;
settings.enableCORS = root["enableCORS"] | false;
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;
}
if (enableCORS != settings.enableCORS || CORSOrigin != settings.CORSOrigin || (ssid != settings.ssid && settings.ssid == "")) {
return StateUpdateResult::CHANGED_RESTART; // tell WebUI that a restart is needed
}
return StateUpdateResult::CHANGED;
}
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);
@@ -127,13 +99,16 @@ class NetworkSettingsService : public StatefulService<NetworkSettings> {
private:
HttpEndpoint<NetworkSettings> _httpEndpoint;
FSPersistence<NetworkSettings> _fsPersistence;
unsigned long _lastConnectionAttempt;
bool _stopping;
void WiFiEvent(WiFiEvent_t event);
unsigned long _lastConnectionAttempt;
bool _stopping;
void reconfigureWiFiConnection();
void manageSTA();
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

@@ -1,18 +1,16 @@
#include <NetworkStatus.h>
#include "NetworkStatus.h"
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
NetworkStatus::NetworkStatus(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(NETWORK_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&NetworkStatus::networkStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { networkStatus(request); }, AuthenticationPredicates::IS_AUTHENTICATED));
}
void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
bool ethernet_connected = emsesp::EMSESP::system_.ethernet_connected();
wl_status_t wifi_status = WiFi.status();
@@ -22,7 +20,7 @@ void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {
root["status"] = 10; // custom code #10 - ETHERNET_STATUS_CONNECTED
root["hostname"] = ETH.getHostname();
} else {
root["status"] = (uint8_t)wifi_status;
root["status"] = static_cast<uint8_t>(wifi_status);
root["hostname"] = WiFi.getHostname();
}

View File

@@ -1,16 +1,12 @@
#ifndef NetworkStatus_h
#define NetworkStatus_h
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ETH.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <IPAddress.h>
#include <IPUtils.h>
#include <SecurityManager.h>
#include "IPUtils.h"
#include "SecurityManager.h"
#define MAX_NETWORK_STATUS_SIZE 1024
#define NETWORK_STATUS_SERVICE_PATH "/rest/networkStatus"

View File

@@ -1,15 +1,13 @@
#include <OTASettingsService.h>
#include "OTASettingsService.h"
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
OTASettingsService::OTASettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(OTASettings::read, OTASettings::update, this, server, OTA_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(OTASettings::read, OTASettings::update, this, fs, OTA_SETTINGS_FILE)
, _arduinoOTA(nullptr) {
WiFi.onEvent(std::bind(&OTASettingsService::WiFiEvent, this, _1, _2));
addUpdateHandler([&](const String & originId) { configureArduinoOTA(); }, false);
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) { WiFiEvent(event); });
addUpdateHandler([this] { configureArduinoOTA(); }, false);
}
void OTASettingsService::begin() {
@@ -32,11 +30,11 @@ void OTASettingsService::configureArduinoOTA() {
if (_state.enabled) {
_arduinoOTA = new ArduinoOTAClass;
_arduinoOTA->setPort(_state.port);
_arduinoOTA->setPort(static_cast<uint16_t>(_state.port));
_arduinoOTA->setPassword(_state.password.c_str());
_arduinoOTA->onStart([]() { emsesp::EMSESP::system_.upload_status(true); });
_arduinoOTA->onEnd([]() { emsesp::EMSESP::system_.upload_status(false); });
_arduinoOTA->onStart([] { emsesp::EMSESP::system_.upload_status(true); });
_arduinoOTA->onEnd([] { emsesp::EMSESP::system_.upload_status(false); });
_arduinoOTA->onProgress([](unsigned int progress, unsigned int total) {
// Serial.printf("Progress: %u%%\r\n", (progress / (total / 100)));
@@ -64,7 +62,7 @@ void OTASettingsService::configureArduinoOTA() {
}
}
void OTASettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
void OTASettingsService::WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
case ARDUINO_EVENT_ETH_GOT_IP:
@@ -74,3 +72,16 @@ void OTASettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
break;
}
}
void OTASettings::read(OTASettings & settings, JsonObject root) {
root["enabled"] = settings.enabled;
root["port"] = settings.port;
root["password"] = settings.password;
}
StateUpdateResult OTASettings::update(JsonObject root, OTASettings & settings) {
settings.enabled = root["enabled"] | FACTORY_OTA_ENABLED;
settings.port = root["port"] | FACTORY_OTA_PORT;
settings.password = root["password"] | FACTORY_OTA_PASSWORD;
return StateUpdateResult::CHANGED;
}

View File

@@ -1,8 +1,8 @@
#ifndef OTASettingsService_h
#define OTASettingsService_h
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#include <ArduinoOTA.h>
#include <WiFiUdp.h>
@@ -28,18 +28,8 @@ class OTASettings {
int port;
String password;
static void read(OTASettings & settings, JsonObject root) {
root["enabled"] = settings.enabled;
root["port"] = settings.port;
root["password"] = settings.password;
}
static StateUpdateResult update(JsonObject root, OTASettings & settings) {
settings.enabled = root["enabled"] | FACTORY_OTA_ENABLED;
settings.port = root["port"] | FACTORY_OTA_PORT;
settings.password = root["password"] | FACTORY_OTA_PASSWORD;
return StateUpdateResult::CHANGED;
}
static void read(OTASettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, OTASettings & settings);
};
class OTASettingsService : public StatefulService<OTASettings> {
@@ -55,7 +45,7 @@ class OTASettingsService : public StatefulService<OTASettings> {
ArduinoOTAClass * _arduinoOTA;
void configureArduinoOTA();
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
void WiFiEvent(WiFiEvent_t event);
};
#endif

View File

@@ -1,15 +1,22 @@
#include <RestartService.h>
#include "RestartService.h"
#include <esp_ota_ops.h>
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
RestartService::RestartService(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(RESTART_SERVICE_PATH, HTTP_POST, securityManager->wrapRequest(std::bind(&RestartService::restart, this, _1), AuthenticationPredicates::IS_ADMIN));
server->on(RESTART_SERVICE_PATH,
HTTP_POST,
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { restart(request); }, AuthenticationPredicates::IS_ADMIN));
server->on(PARTITION_SERVICE_PATH,
HTTP_POST,
securityManager->wrapRequest(std::bind(&RestartService::partition, this, _1), AuthenticationPredicates::IS_ADMIN));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { partition(request); }, AuthenticationPredicates::IS_ADMIN));
}
void RestartService::restartNow() {
WiFi.disconnect(true);
delay(500);
ESP.restart();
}
void RestartService::restart(AsyncWebServerRequest * request) {
@@ -19,7 +26,7 @@ void RestartService::restart(AsyncWebServerRequest * request) {
}
void RestartService::partition(AsyncWebServerRequest * request) {
const esp_partition_t * factory_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL);
const esp_partition_t * factory_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
if (factory_partition) {
esp_ota_set_boot_partition(factory_partition);
emsesp::EMSESP::system_.store_nvs_values();
@@ -27,7 +34,7 @@ void RestartService::partition(AsyncWebServerRequest * request) {
request->send(200);
return;
}
const esp_partition_t * ota_partition = esp_ota_get_next_update_partition(NULL);
const esp_partition_t * ota_partition = esp_ota_get_next_update_partition(nullptr);
if (!ota_partition) {
request->send(400); // bad request
return;

View File

@@ -3,9 +3,9 @@
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include "SecurityManager.h"
#define RESTART_SERVICE_PATH "/rest/restart"
#define PARTITION_SERVICE_PATH "/rest/partition"
@@ -14,11 +14,7 @@ class RestartService {
public:
RestartService(AsyncWebServer * server, SecurityManager * securityManager);
static void restartNow() {
WiFi.disconnect(true);
delay(500);
ESP.restart();
}
static void restartNow();
private:
void restart(AsyncWebServerRequest * request);

View File

@@ -1,17 +1,13 @@
#ifndef SecurityManager_h
#define SecurityManager_h
#include <Features.h>
#include <ArduinoJsonJWT.h>
#include "Features.h"
#include "ArduinoJsonJWT.h"
#include <ESPAsyncWebServer.h>
#include <ESPUtils.h>
#include <AsyncJson.h>
#include <list>
#ifndef FACTORY_JWT_SECRET
#define FACTORY_JWT_SECRET ESPUtils::defaultDeviceValue()
#endif
#define ACCESS_TOKEN_PARAMATER "access_token"
#define AUTHORIZATION_HEADER "Authorization"
@@ -26,28 +22,27 @@ class User {
public:
User(String username, String password, bool admin)
: username(username)
, password(password)
: username(std::move(username))
, password(std::move(password))
, admin(admin) {
}
};
class Authentication {
public:
User * user;
boolean authenticated;
User * user = nullptr;
boolean authenticated = false;
public:
Authentication(User & user)
explicit Authentication(const User & user)
: user(new User(user))
, authenticated(true) {
}
Authentication()
: user(nullptr)
, authenticated(false) {
}
Authentication() = default;
~Authentication() {
delete (user);
delete user;
}
};
@@ -55,13 +50,14 @@ typedef std::function<boolean(Authentication & authentication)> AuthenticationPr
class AuthenticationPredicates {
public:
static bool NONE_REQUIRED(Authentication & authentication) {
static bool NONE_REQUIRED(const Authentication & authentication) {
(void)authentication;
return true;
};
static bool IS_AUTHENTICATED(Authentication & authentication) {
static bool IS_AUTHENTICATED(const Authentication & authentication) {
return authentication.authenticated;
};
static bool IS_ADMIN(Authentication & authentication) {
static bool IS_ADMIN(const Authentication & authentication) {
return authentication.authenticated && authentication.user->admin;
};
};
@@ -76,7 +72,7 @@ class SecurityManager {
/*
* Generate a JWT for the user provided
*/
virtual String generateJWT(User * user) = 0;
virtual String generateJWT(const User * user) = 0;
/*
* Check the request header for the Authorization token

View File

@@ -1,15 +1,13 @@
#include <SecuritySettingsService.h>
#include "../../src/emsesp_stub.hpp"
#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([&](const String & originId) { configureJWTHandler(); }, false);
addUpdateHandler([this] { configureJWTHandler(); }, false);
server->on(GENERATE_TOKEN_PATH,
HTTP_GET,
wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
SecuritySettingsService::wrapRequest([this](AsyncWebServerRequest * request) { generateToken(request); }, AuthenticationPredicates::IS_ADMIN));
}
void SecuritySettingsService::begin() {
@@ -30,7 +28,7 @@ Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerReques
String value = tokenParamater->value();
return authenticateJWT(value);
}
return Authentication();
return {};
}
void SecuritySettingsService::configureJWTHandler() {
@@ -43,37 +41,37 @@ Authentication SecuritySettingsService::authenticateJWT(String & jwt) {
if (payloadDocument.is<JsonObject>()) {
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
String username = parsedPayload["username"];
for (User _user : _state.users) {
for (const User & _user : _state.users) {
if (_user.username == username && validatePayload(parsedPayload, &_user)) {
return Authentication(_user);
}
}
}
return Authentication();
return {};
}
Authentication SecuritySettingsService::authenticate(const String & username, const String & password) {
for (User _user : _state.users) {
for (const User & _user : _state.users) {
if (_user.username == username && _user.password == password) {
return Authentication(_user);
}
}
return Authentication();
return {};
}
inline void populateJWTPayload(JsonObject payload, User * user) {
inline void populateJWTPayload(JsonObject payload, const User * user) {
payload["username"] = user->username;
payload["admin"] = user->admin;
}
boolean SecuritySettingsService::validatePayload(JsonObject parsedPayload, User * user) {
boolean SecuritySettingsService::validatePayload(JsonObject parsedPayload, const User * user) {
JsonDocument jsonDocument;
JsonObject payload = jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return payload == parsedPayload;
}
String SecuritySettingsService::generateJWT(User * user) {
String SecuritySettingsService::generateJWT(const User * user) {
JsonDocument jsonDocument;
JsonObject payload = jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
@@ -111,11 +109,11 @@ ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequest
void SecuritySettingsService::generateToken(AsyncWebServerRequest * request) {
AsyncWebParameter * usernameParam = request->getParam("username");
for (User _user : _state.users) {
for (const User & _user : _state.users) {
if (_user.username == usernameParam->value()) {
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["token"] = generateJWT(&_user);
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["token"] = generateJWT(&_user);
response->setLength();
request->send(response);
return;

View File

@@ -1,10 +1,10 @@
#ifndef SecuritySettingsService_h
#define SecuritySettingsService_h
#include <Features.h>
#include <SecurityManager.h>
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#include "Features.h"
#include "SecurityManager.h"
#include "HttpEndpoint.h"
#include "FSPersistence.h"
#ifndef FACTORY_ADMIN_USERNAME
#define FACTORY_ADMIN_USERNAME "admin"
@@ -32,7 +32,6 @@ class SecuritySettings {
public:
String jwtSecret;
std::vector<User> users;
// std::list<User> users;
static void read(SecuritySettings & settings, JsonObject root) {
// secret
@@ -40,7 +39,7 @@ class SecuritySettings {
// users
JsonArray users = root["users"].to<JsonArray>();
for (User user : settings.users) {
for (const User & user : settings.users) {
JsonObject userRoot = users.add<JsonObject>();
userRoot["username"] = user.username;
userRoot["password"] = user.password;
@@ -56,29 +55,29 @@ class SecuritySettings {
settings.users.clear();
if (root["users"].is<JsonArray>()) {
for (JsonVariant user : root["users"].as<JsonArray>()) {
settings.users.push_back(User(user["username"], user["password"], user["admin"]));
settings.users.emplace_back(user["username"], user["password"], user["admin"]);
}
} else {
settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true));
settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false));
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 : public StatefulService<SecuritySettings>, public SecurityManager {
class SecuritySettingsService final : public StatefulService<SecuritySettings>, public SecurityManager {
public:
SecuritySettingsService(AsyncWebServer * server, FS * fs);
void begin();
// Functions to implement SecurityManager
Authentication authenticate(const String & username, const String & password);
Authentication authenticateRequest(AsyncWebServerRequest * request);
String generateJWT(User * user);
ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate);
ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate);
ArJsonRequestHandlerFunction wrapCallback(ArJsonRequestHandlerFunction callback, AuthenticationPredicate predicate);
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;
@@ -97,7 +96,7 @@ class SecuritySettingsService : public StatefulService<SecuritySettings>, public
/*
* Verify the payload is correct
*/
boolean validatePayload(JsonObject parsedPayload, User * user);
boolean validatePayload(JsonObject parsedPayload, const User * user);
};
#endif

View File

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

View File

@@ -23,8 +23,8 @@ using JsonStateUpdater = std::function<StateUpdateResult(JsonObject root, T & se
template <typename T>
using JsonStateReader = std::function<void(T & settings, JsonObject root)>;
typedef size_t update_handler_id_t;
typedef std::function<void(const String & originId)> StateUpdateCallback;
typedef size_t update_handler_id_t;
typedef std::function<void()> StateUpdateCallback;
typedef struct StateUpdateHandlerInfo {
static update_handler_id_t currentUpdatedHandlerId;
@@ -33,7 +33,7 @@ typedef struct StateUpdateHandlerInfo {
bool _allowRemove;
StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove)
: _id(++currentUpdatedHandlerId)
, _cb(cb)
, _cb(std::move(cb))
, _allowRemove(allowRemove){};
} StateUpdateHandlerInfo_t;
@@ -41,7 +41,7 @@ template <class T>
class StatefulService {
public:
template <typename... Args>
StatefulService(Args &&... args)
explicit StatefulService(Args &&... args)
: _state(std::forward<Args>(args)...)
, _accessMutex(xSemaphoreCreateRecursiveMutex()) {
}
@@ -50,27 +50,28 @@ class StatefulService {
if (!cb) {
return 0;
}
StateUpdateHandlerInfo_t updateHandler(cb, allowRemove);
_updateHandlers.push_back(updateHandler);
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 i = _updateHandlers.begin(); i != _updateHandlers.end();) {
if ((*i)._allowRemove && (*i)._id == id) {
i = _updateHandlers.erase(i);
for (auto it = _updateHandlers.begin(); it != _updateHandlers.end();) {
auto & elem = *it;
if (elem._allowRemove && elem._id == id) {
it = _updateHandlers.erase(it);
} else {
++i;
++it;
}
}
}
StateUpdateResult update(std::function<StateUpdateResult(T &)> stateUpdater, const String & originId) {
StateUpdateResult update(std::function<StateUpdateResult(T &)> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(_state);
endTransaction();
if (result == StateUpdateResult::CHANGED) {
callUpdateHandlers(originId);
callUpdateHandlers();
}
return result;
}
@@ -82,12 +83,12 @@ class StatefulService {
return result;
}
StateUpdateResult update(JsonObject jsonObject, JsonStateUpdater<T> stateUpdater, const String & originId) {
StateUpdateResult update(JsonObject jsonObject, JsonStateUpdater<T> stateUpdater) {
beginTransaction();
StateUpdateResult result = stateUpdater(jsonObject, _state);
endTransaction();
if (result == StateUpdateResult::CHANGED) {
callUpdateHandlers(originId);
callUpdateHandlers();
}
return result;
}
@@ -111,9 +112,9 @@ class StatefulService {
endTransaction();
}
void callUpdateHandlers(const String & originId) {
void callUpdateHandlers() {
for (const StateUpdateHandlerInfo_t & updateHandler : _updateHandlers) {
updateHandler._cb(originId);
updateHandler._cb();
}
}
@@ -129,9 +130,8 @@ class StatefulService {
}
private:
SemaphoreHandle_t _accessMutex;
SemaphoreHandle_t _accessMutex;
std::vector<StateUpdateHandlerInfo_t> _updateHandlers;
// std::list<StateUpdateHandlerInfo_t> _updateHandlers;
};
#endif

View File

@@ -1,23 +1,30 @@
#include <SystemStatus.h>
#include "SystemStatus.h"
#include <esp_ota_ops.h>
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
SystemStatus::SystemStatus(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(SYSTEM_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { systemStatus(request); }, AuthenticationPredicates::IS_AUTHENTICATED));
}
void SystemStatus::systemStatus(AsyncWebServerRequest * request) {
emsesp::EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["emsesp_version"] = EMSESP_APP_VERSION;
#ifdef EMSESP_DEBUG
root["emsesp_version"] = std::string(EMSESP_APP_VERSION) + " (DEBUG)";
#else
#ifdef EMSESP_TEST
root["emsesp_version"] = std::string(EMSESP_APP_VERSION) + " (TEST)";
#else
root["emsesp_version"] = EMSESP_APP_VERSION;
#endif
#endif
root["esp_platform"] = EMSESP_PLATFORM;
root["cpu_type"] = ESP.getChipModel();
root["cpu_rev"] = ESP.getChipRevision();
@@ -41,11 +48,11 @@ void SystemStatus::systemStatus(AsyncWebServerRequest * request) {
root["psram_size"] = emsesp::EMSESP::system_.PSram();
root["free_psram"] = ESP.getFreePsram() / 1024;
}
const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL);
const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
if (partition != NULL) { // factory partition found
root["has_loader"] = true;
} else { // check for not empty, smaller OTA partition
partition = esp_ota_get_next_update_partition(NULL);
partition = esp_ota_get_next_update_partition(nullptr);
if (partition) {
uint64_t buffer;
esp_partition_read(partition, 0, &buffer, 8);

View File

@@ -5,10 +5,10 @@
#include <AsyncTCP.h>
#include <FS.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include "SecurityManager.h"
#define SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus"

View File

@@ -1,20 +1,27 @@
#include <UploadFileService.h>
#include <esp_ota_ops.h>
#include <esp_app_format.h>
#include "UploadFileService.h"
#include "../../src/emsesp_stub.hpp"
using namespace std::placeholders; // for `_1` etc
#include <esp_app_format.h>
static bool is_firmware = false;
static char md5[33] = "\0";
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) {
server->on(UPLOAD_FILE_PATH,
HTTP_POST,
std::bind(&UploadFileService::uploadComplete, this, _1),
std::bind(&UploadFileService::handleUpload, this, _1, _2, _3, _4, _5, _6));
: _securityManager(securityManager)
, _is_firmware(false)
, _md5() {
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) {
@@ -28,29 +35,27 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
// at init
if (!index) {
// check details of the file, to see if its a valid bin or json file
std::string fname(filename.c_str());
auto position = fname.find_last_of(".");
std::string extension = fname.substr(position + 1);
size_t fsize = request->contentLength();
const String extension = getFilenameExtension(filename);
const std::size_t filesize = request->contentLength();
is_firmware = false;
if ((extension == "bin") && (fsize > 1000000)) {
is_firmware = true;
_is_firmware = false;
if ((extension == "bin") && (filesize > 1000000)) {
_is_firmware = true;
} else if (extension == "json") {
md5[0] = '\0'; // clear md5
_md5[0] = '\0'; // clear md5
} else if (extension == "md5") {
if (len == 32) {
memcpy(md5, data, 32);
md5[32] = '\0';
if (len == _md5.size() - 1) {
std::memcpy(_md5.data(), data, _md5.size() - 1);
_md5.back() = '\0';
}
return;
} else {
md5[0] = '\0';
_md5.front() = '\0';
handleError(request, 406); // Not Acceptable - unsupported file type
return;
}
if (is_firmware) {
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)) {
@@ -74,12 +79,12 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
}
#endif
// it's firmware - initialize the ArduinoOTA updater
if (Update.begin(fsize - sizeof(esp_image_header_t))) {
if (strlen(md5) == 32) {
Update.setMD5(md5);
md5[0] = '\0';
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(UploadFileService::handleEarlyDisconnect); // success, let's make sure we end the update if the client hangs up
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;
@@ -90,23 +95,17 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
}
}
if (!is_firmware) {
if (len) {
if (len != request->_tempFile.write(data, len)) { // stream the incoming chunk to the opened file
handleError(request, 507); // 507-Insufficient Storage
}
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 we haven't delt with an error, continue with the firmware update
if (!request->_tempObject) {
if (Update.write(data, len) != len) {
handleError(request, 500);
}
if (final) {
if (!Update.end(true)) {
handleError(request, 500);
}
}
} 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);
return;
}
if (final && !Update.end(true)) {
handleError(request, 500);
}
}
}
@@ -124,7 +123,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
// check if it was a firmware upgrade
// if no error, send the success response as a JSON
if (is_firmware && !request->_tempObject) {
if (_is_firmware && !request->_tempObject) {
emsesp::EMSESP::system_.store_nvs_values();
request->onDisconnect(RestartService::restartNow);
AsyncWebServerResponse * response = request->beginResponse(200);
@@ -132,10 +131,10 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
return;
}
if (strlen(md5) == 32) {
if (strlen(_md5.data()) == _md5.size() - 1) {
auto * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
root["md5"] = md5;
root["md5"] = _md5.data();
response->setLength();
request->send(response);
return;
@@ -163,6 +162,6 @@ void UploadFileService::handleError(AsyncWebServerRequest * request, int code) {
}
void UploadFileService::handleEarlyDisconnect() {
is_firmware = false;
_is_firmware = false;
Update.abort();
}
}

View File

@@ -1,16 +1,16 @@
#ifndef UploadFileService_h
#define UploadFileService_h
#include <Arduino.h>
#include "RestartService.h"
#include "SecurityManager.h"
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
#include <Update.h>
#include <WiFi.h>
#include <LittleFS.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <RestartService.h>
#include <array>
#define UPLOAD_FILE_PATH "/rest/uploadFile"
#define TEMP_FILENAME_PATH "/tmp_upload"
@@ -20,11 +20,14 @@ class UploadFileService {
UploadFileService(AsyncWebServer * server, SecurityManager * securityManager);
private:
SecurityManager * _securityManager;
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);
static void handleEarlyDisconnect();
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
#endif

View File

@@ -1,14 +1,12 @@
#include <WiFiScanner.h>
using namespace std::placeholders; // for `_1` etc
#include "WiFiScanner.h"
WiFiScanner::WiFiScanner(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(SCAN_NETWORKS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, _1), AuthenticationPredicates::IS_ADMIN));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { scanNetworks(request); }, AuthenticationPredicates::IS_ADMIN));
server->on(LIST_NETWORKS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, _1), AuthenticationPredicates::IS_ADMIN));
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { listNetworks(request); }, AuthenticationPredicates::IS_ADMIN));
};
void WiFiScanner::scanNetworks(AsyncWebServerRequest * request) {
@@ -21,18 +19,18 @@ void WiFiScanner::scanNetworks(AsyncWebServerRequest * request) {
}
void WiFiScanner::listNetworks(AsyncWebServerRequest * request) {
int numNetworks = WiFi.scanComplete();
const int numNetworks = WiFi.scanComplete();
if (numNetworks > -1) {
AsyncJsonResponse * response = new AsyncJsonResponse(false);
JsonObject root = response->getRoot();
JsonArray networks = root["networks"].to<JsonArray>();
for (int i = 0; i < numNetworks; i++) {
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"] = (uint8_t)WiFi.encryptionType(i);
network["encryption_type"] = static_cast<uint8_t>(WiFi.encryptionType(i));
}
response->setLength();
request->send(response);

View File

@@ -3,10 +3,10 @@
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include "SecurityManager.h"
#define SCAN_NETWORKS_SERVICE_PATH "/rest/scanNetworks"
#define LIST_NETWORKS_SERVICE_PATH "/rest/listNetworks"