mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-05-04 04:55:53 +00:00
sync with dev-16
This commit is contained in:
@@ -70,12 +70,16 @@ void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument & jsonDocument) {
|
||||
*/
|
||||
String ArduinoJsonJWT::sign(String & payload) {
|
||||
std::array<unsigned char, 32> hmacResult{};
|
||||
mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),
|
||||
reinterpret_cast<const unsigned char *>(_secret.c_str()),
|
||||
_secret.length(),
|
||||
reinterpret_cast<const unsigned char *>(payload.c_str()),
|
||||
payload.length(),
|
||||
hmacResult.data());
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,75 @@
|
||||
|
||||
#include "WWWData.h" // include auto-generated static web resources
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static constexpr const char CACHE_CONTROL[] = "public,max-age=60";
|
||||
|
||||
// Single static-content handler serving all assets embedded in WWWData.h.
|
||||
class StaticContentHandler : public AsyncWebHandler {
|
||||
public:
|
||||
bool canHandle(AsyncWebServerRequest * request) const override {
|
||||
const auto method = request->method();
|
||||
return method == HTTP_GET || method == HTTP_HEAD || method == HTTP_OPTIONS;
|
||||
}
|
||||
|
||||
void handleRequest(AsyncWebServerRequest * request) override {
|
||||
// OPTIONS is handled generically - the server-level CORS headers are
|
||||
// attached via DefaultHeaders in ESP32React::begin().
|
||||
if (request->method() == HTTP_OPTIONS) {
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
|
||||
const char * url = request->url().c_str();
|
||||
const WWWAsset * found = lookup(url);
|
||||
const WWWAsset * asset = found ? found : index_asset();
|
||||
|
||||
if (asset == nullptr) {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the client already has this exact ETag, respond 304 Not Modified without sending the body.
|
||||
const String & inm = request->header(asyncsrv::T_INM);
|
||||
if (inm.length() != 0 && strcmp(inm.c_str(), asset->etag) == 0) {
|
||||
request->send(304);
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncWebServerResponse * response = request->beginResponse(200, asset->contentType, asset->content, asset->len);
|
||||
response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false);
|
||||
response->addHeader(asyncsrv::T_ETag, asset->etag, false);
|
||||
response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false);
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
private:
|
||||
// Exact-match lookup in the asset table
|
||||
static const WWWAsset * lookup(const char * url) {
|
||||
for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) {
|
||||
if (strcmp(WWW_ASSETS[i].uri, url) == 0) {
|
||||
return &WWW_ASSETS[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns the /index.html asset, used as the SPA fallback for any GET
|
||||
// that didn't match an embedded asset (React Router handles routing on
|
||||
// the client side).
|
||||
static const WWWAsset * index_asset() {
|
||||
static const WWWAsset * cached = nullptr;
|
||||
if (cached == nullptr) {
|
||||
cached = lookup("/index.html");
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
};
|
||||
|
||||
ESP32React::ESP32React(AsyncWebServer * server, FS * fs)
|
||||
: _securitySettingsService(server, fs)
|
||||
: _server(server)
|
||||
, _securitySettingsService(server, fs)
|
||||
, _networkSettingsService(server, fs, &_securitySettingsService)
|
||||
, _wifiScanner(server, &_securitySettingsService)
|
||||
, _networkStatus(server, &_securitySettingsService)
|
||||
@@ -17,50 +82,6 @@ ESP32React::ESP32React(AsyncWebServer * server, FS * fs)
|
||||
, _mqttSettingsService(server, fs, &_securitySettingsService)
|
||||
, _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService)
|
||||
, _authenticationService(server, &_securitySettingsService) {
|
||||
//
|
||||
// Serve static web resources
|
||||
//
|
||||
|
||||
ArRequestHandlerFunction indexHtmlHandler = nullptr;
|
||||
|
||||
WWWData::registerRoutes([server, &indexHtmlHandler](const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash) {
|
||||
String etag = "\"" + hash + "\""; // RFC9110: ETag must be enclosed in double quotes
|
||||
|
||||
ArRequestHandlerFunction requestHandler = [contentType, content, len, etag](AsyncWebServerRequest * request) {
|
||||
if (request->header(asyncsrv::T_INM) == etag) {
|
||||
request->send(304);
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncWebServerResponse * response = request->beginResponse(200, contentType, content, len);
|
||||
response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false);
|
||||
response->addHeader(asyncsrv::T_ETag, etag, false);
|
||||
response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false);
|
||||
request->send(response);
|
||||
};
|
||||
|
||||
server->on(uri, HTTP_GET, requestHandler);
|
||||
|
||||
// Capture index.html handler to set onNotFound once after all routes are registered
|
||||
if (strcmp(uri, "/index.html") == 0) {
|
||||
indexHtmlHandler = requestHandler;
|
||||
}
|
||||
});
|
||||
|
||||
// Set onNotFound handler once after all routes are registered
|
||||
// Serving non matching get requests with "/index.html"
|
||||
// OPTIONS get a straight up 200 response
|
||||
if (indexHtmlHandler != nullptr) {
|
||||
server->onNotFound([indexHtmlHandler](AsyncWebServerRequest * request) {
|
||||
if (request->method() == HTTP_GET) {
|
||||
indexHtmlHandler(request);
|
||||
} else if (request->method() == HTTP_OPTIONS) {
|
||||
request->send(200);
|
||||
} else {
|
||||
request->send(404); // not found
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32React::begin() {
|
||||
@@ -78,11 +99,11 @@ void ESP32React::begin() {
|
||||
_ntpSettingsService.begin();
|
||||
_mqttSettingsService.begin();
|
||||
_securitySettingsService.begin();
|
||||
_server->addHandler(new StaticContentHandler());
|
||||
}
|
||||
|
||||
void ESP32React::loop() {
|
||||
_networkSettingsService.loop();
|
||||
_apSettingsService.loop();
|
||||
_mqttSettingsService.loop();
|
||||
_ntpSettingsService.loop();
|
||||
}
|
||||
@@ -68,6 +68,7 @@ class ESP32React {
|
||||
}
|
||||
|
||||
private:
|
||||
AsyncWebServer * _server;
|
||||
SecuritySettingsService _securitySettingsService;
|
||||
NetworkSettingsService _networkSettingsService;
|
||||
WiFiScanner _wifiScanner;
|
||||
|
||||
124
src/core/EMSESP_Version.h
Normal file
124
src/core/EMSESP_Version.h
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* EMS-ESP - https://github.com/emsesp/EMS-ESP
|
||||
* Copyright 2020-2026 emsesp.org
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef EMSESP_Version_H
|
||||
#define EMSESP_Version_H
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Drop-in lightweight replacement for the subset of the semver library actually used by EMS-ESP.
|
||||
// The previous semver library (lib/semver) builds a std::map + std::function-based state machine on
|
||||
// every parse, which fragments the internal heap on the ESP32. This replacement does no heap
|
||||
// allocation beyond the std::string member for the prerelease tag, and matches the API surface
|
||||
// we consume: construction from string, major()/minor()/patch()/prerelease(), and operator>/</==.
|
||||
//
|
||||
// Only strict numeric precedence (major.minor.patch) is used for comparison in EMS-ESP, so we
|
||||
// intentionally ignore prerelease tags during comparison rather than implement the full semver
|
||||
// ordering rules. This is consistent with how the old code was used (callers only check major/
|
||||
// minor/patch numerically; prerelease() is only read for logging).
|
||||
|
||||
namespace version {
|
||||
|
||||
class EMSESP_Version {
|
||||
public:
|
||||
EMSESP_Version() = default;
|
||||
|
||||
// Construct from a version string like "3.9.0-dev.14" or "3.9.0".
|
||||
// Anything past a '-' or '+' is kept as the prerelease string and not interpreted.
|
||||
explicit EMSESP_Version(const std::string & s) {
|
||||
parse(s.c_str());
|
||||
}
|
||||
|
||||
explicit EMSESP_Version(const char * s) {
|
||||
parse(s ? s : "");
|
||||
}
|
||||
|
||||
int major() const {
|
||||
return major_;
|
||||
}
|
||||
int minor() const {
|
||||
return minor_;
|
||||
}
|
||||
int patch() const {
|
||||
return patch_;
|
||||
}
|
||||
const std::string & prerelease() const {
|
||||
return prerelease_;
|
||||
}
|
||||
|
||||
// Numeric-only comparison (major.minor.patch). Prerelease tags are ignored on purpose.
|
||||
friend bool operator<(const EMSESP_Version & a, const EMSESP_Version & b) {
|
||||
if (a.major_ != b.major_)
|
||||
return a.major_ < b.major_;
|
||||
if (a.minor_ != b.minor_)
|
||||
return a.minor_ < b.minor_;
|
||||
return a.patch_ < b.patch_;
|
||||
}
|
||||
|
||||
friend bool operator>(const EMSESP_Version & a, const EMSESP_Version & b) {
|
||||
return b < a;
|
||||
}
|
||||
|
||||
friend bool operator==(const EMSESP_Version & a, const EMSESP_Version & b) {
|
||||
return a.major_ == b.major_ && a.minor_ == b.minor_ && a.patch_ == b.patch_;
|
||||
}
|
||||
|
||||
friend bool operator!=(const EMSESP_Version & a, const EMSESP_Version & b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
friend bool operator>=(const EMSESP_Version & a, const EMSESP_Version & b) {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
friend bool operator<=(const EMSESP_Version & a, const EMSESP_Version & b) {
|
||||
return !(b < a);
|
||||
}
|
||||
|
||||
private:
|
||||
int major_ = 0;
|
||||
int minor_ = 0;
|
||||
int patch_ = 0;
|
||||
std::string prerelease_;
|
||||
|
||||
void parse(const char * s) {
|
||||
major_ = minor_ = patch_ = 0;
|
||||
prerelease_.clear();
|
||||
if (s == nullptr || *s == '\0') {
|
||||
return;
|
||||
}
|
||||
// parse numeric major.minor.patch; accept partial ("3", "3.9", "3.9.0")
|
||||
sscanf(s, "%d.%d.%d", &major_, &minor_, &patch_);
|
||||
// capture prerelease tag after '-' if present (stop at '+' which is build metadata)
|
||||
const char * dash = strchr(s, '-');
|
||||
if (dash != nullptr) {
|
||||
const char * plus = strchr(dash, '+');
|
||||
if (plus != nullptr) {
|
||||
prerelease_.assign(dash + 1, plus - dash - 1);
|
||||
} else {
|
||||
prerelease_.assign(dash + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace version
|
||||
|
||||
#endif
|
||||
@@ -19,6 +19,7 @@
|
||||
#ifndef EMSESP_EMSFACTORY_H_
|
||||
#define EMSESP_EMSFACTORY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory> // for unique_ptr
|
||||
#include <map>
|
||||
|
||||
|
||||
@@ -26,13 +26,12 @@
|
||||
#include <esp_mac.h>
|
||||
#include "esp_efuse.h"
|
||||
#include <nvs.h>
|
||||
#include <mbedtls/base64.h>
|
||||
#endif
|
||||
|
||||
#include <HTTPClient.h>
|
||||
#include <map>
|
||||
|
||||
#include <semver200.h>
|
||||
#include "EMSESP_Version.h"
|
||||
|
||||
#if defined(EMSESP_TEST)
|
||||
#include "../test/test.h"
|
||||
@@ -457,15 +456,16 @@ void System::get_partition_info() {
|
||||
strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d));
|
||||
p_info.install_date = d > 1500000000L ? time_string : "";
|
||||
|
||||
esp_image_metadata_t meta = {};
|
||||
esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size};
|
||||
if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) {
|
||||
p_info.size = meta.image_len / 1024; // actual firmware size in KB
|
||||
} else {
|
||||
p_info.size = 0;
|
||||
if (!p_info.version.empty()) {
|
||||
esp_image_metadata_t meta = {};
|
||||
esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size};
|
||||
if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) {
|
||||
p_info.size = meta.image_len / 1024; // actual firmware size in KB
|
||||
} else {
|
||||
p_info.size = 0;
|
||||
}
|
||||
partition_info_[part->label] = p_info;
|
||||
}
|
||||
|
||||
partition_info_[part->label] = p_info;
|
||||
}
|
||||
|
||||
it = esp_partition_next(it); // loop to next partition
|
||||
@@ -1613,8 +1613,8 @@ bool System::check_upgrade() {
|
||||
settingsVersion = "3.5.0"; // this was the last stable version without version info
|
||||
}
|
||||
|
||||
version::Semver200_version settings_version(settingsVersion);
|
||||
version::Semver200_version this_version(EMSESP_APP_VERSION);
|
||||
version::EMSESP_Version settings_version(settingsVersion);
|
||||
version::EMSESP_Version this_version(EMSESP_APP_VERSION);
|
||||
|
||||
std::string settings_version_type = settings_version.prerelease().empty() ? "" : ("-" + settings_version.prerelease());
|
||||
std::string this_version_type = this_version.prerelease().empty() ? "" : ("-" + this_version.prerelease());
|
||||
@@ -1765,18 +1765,17 @@ void System::exportSettings(const std::string & type, const char * filename, Jso
|
||||
|
||||
File settingsFile = LittleFS.open(filename);
|
||||
if (settingsFile) {
|
||||
JsonDocument jsonDocument;
|
||||
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
|
||||
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
|
||||
JsonObject node = output[section].to<JsonObject>();
|
||||
for (JsonPair kvp : jsonDocument.as<JsonObject>()) {
|
||||
node[kvp.key()] = kvp.value();
|
||||
{
|
||||
JsonDocument jsonDocument;
|
||||
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
|
||||
settingsFile.close(); // close early, we no longer need the file
|
||||
if (error || !jsonDocument.is<JsonObject>()) {
|
||||
LOG_ERROR("Failed to deserialize settings file %s", filename);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Failed to deserialize settings file %s", filename);
|
||||
output[section].set(jsonDocument.as<JsonObject>());
|
||||
}
|
||||
LOG_DEBUG("Exported %s settings from file %s", section, filename);
|
||||
settingsFile.close();
|
||||
} else {
|
||||
LOG_ERROR("No settings file for %s found", filename);
|
||||
}
|
||||
@@ -1828,13 +1827,15 @@ void System::exportSystemBackup(JsonObject output) {
|
||||
if (file) {
|
||||
JsonDocument jsonDocument;
|
||||
DeserializationError error = deserializeJson(jsonDocument, file);
|
||||
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
|
||||
JsonObject node = nodes.add<JsonObject>();
|
||||
node["type"] = "customSupport";
|
||||
node["data"] = jsonDocument.as<JsonObject>();
|
||||
file.close(); // close early, we no longer need the file
|
||||
if (!error && jsonDocument.is<JsonObject>()) {
|
||||
JsonObject support_node = nodes.add<JsonObject>();
|
||||
support_node["type"] = "customSupport";
|
||||
support_node["data"].set(jsonDocument.as<JsonObject>());
|
||||
LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE);
|
||||
} else {
|
||||
LOG_ERROR("Failed to deserialize custom support file");
|
||||
}
|
||||
file.close();
|
||||
LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE);
|
||||
}
|
||||
|
||||
// Backup NVS values
|
||||
|
||||
@@ -253,7 +253,7 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
|
||||
|
||||
// it's a filename with a .bin or .md extension, try and extract the version from it
|
||||
// e.g. EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin -> major=3 minor=8 patch=2
|
||||
version::Semver200_version latest_version;
|
||||
version::EMSESP_Version latest_version;
|
||||
if ((version.find(".bin") != std::string::npos) || (version.find(".md") != std::string::npos)) {
|
||||
std::string filename = version;
|
||||
auto pos = filename.find("EMS-ESP-");
|
||||
@@ -274,18 +274,18 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
|
||||
std::string major_version = filename.substr(pos, underscore1 - pos);
|
||||
std::string minor_version = filename.substr(underscore1 + 1, underscore2 - underscore1 - 1);
|
||||
std::string patch_version = filename.substr(underscore2 + 1, dash - underscore2 - 1);
|
||||
latest_version = version::Semver200_version(major_version + "." + minor_version + "." + patch_version);
|
||||
latest_version = version::EMSESP_Version(major_version + "." + minor_version + "." + patch_version);
|
||||
} else {
|
||||
// if it's .json file exit
|
||||
if (version.find(".json") != std::string::npos) {
|
||||
return 0;
|
||||
} else {
|
||||
// treat it like a version string like "3.9.0"
|
||||
latest_version = version::Semver200_version(version);
|
||||
latest_version = version::EMSESP_Version(version);
|
||||
}
|
||||
}
|
||||
|
||||
version::Semver200_version current_version(current_version_s); // get current version
|
||||
version::EMSESP_Version current_version(current_version_s); // get current version
|
||||
|
||||
if (latest_version > current_version && current_version.minor() < latest_version.minor()) {
|
||||
return 0; // if it's just a minor version upgrade return 0
|
||||
@@ -306,9 +306,9 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
|
||||
// versions holds the latest development version and stable version in one string, comma separated
|
||||
bool WebStatusService::checkUpgrade(JsonObject root, std::string & version) {
|
||||
if (!version.empty()) {
|
||||
version::Semver200_version current_version(current_version_s);
|
||||
version::Semver200_version latest_dev_version(version.substr(0, version.find(',')));
|
||||
version::Semver200_version latest_stable_version(version.substr(version.find(',') + 1));
|
||||
version::EMSESP_Version current_version(current_version_s);
|
||||
version::EMSESP_Version latest_dev_version(version.substr(0, version.find(',')));
|
||||
version::EMSESP_Version latest_stable_version(version.substr(version.find(',') + 1));
|
||||
|
||||
bool dev_upgradeable = latest_dev_version > current_version;
|
||||
bool stable_upgradeable = latest_stable_version > current_version;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#define EMSESP_SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus"
|
||||
#define EMSESP_ACTION_SERVICE_PATH "/rest/action"
|
||||
|
||||
#include <semver200.h> // for version checking
|
||||
#include "../core/EMSESP_Version.h"
|
||||
#include "../emsesp_version.h"
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
Reference in New Issue
Block a user