Implement OTA release compatibility checking system
Implement a comprehensive solution for validating a firmware before an OTA updated is committed. WLED metadata such as version and release is moved to a data structure located at near the start of the firmware binary, where it can be identified and validated. Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
This commit is contained in:
committed by
Will Miles
parent
e4cabf8de6
commit
a073bf32e4
@@ -388,12 +388,6 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()=====";
|
|||||||
name: "PAGE_update",
|
name: "PAGE_update",
|
||||||
method: "gzip",
|
method: "gzip",
|
||||||
filter: "html-minify",
|
filter: "html-minify",
|
||||||
mangle: (str) =>
|
|
||||||
str
|
|
||||||
.replace(
|
|
||||||
/function GetV().*\<\/script\>/gms,
|
|
||||||
"</script><script src=\"/settings/s.js?p=9\"></script>"
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
file: "welcome.htm",
|
file: "welcome.htm",
|
||||||
|
|||||||
@@ -17,7 +17,26 @@
|
|||||||
}
|
}
|
||||||
window.open(getURL("/update?revert"),"_self");
|
window.open(getURL("/update?revert"),"_self");
|
||||||
}
|
}
|
||||||
function GetV() {/*injected values here*/}
|
function GetV() {
|
||||||
|
// Fetch device info via JSON API instead of compiling it in
|
||||||
|
fetch('/json/info')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.querySelector('.installed-version').textContent = `${data.brand} ${data.ver} (${data.vid})`;
|
||||||
|
document.querySelector('.release-name').textContent = data.release;
|
||||||
|
// TODO - assemble update URL
|
||||||
|
// TODO - can this be done at build time?
|
||||||
|
if (data.arch == "esp8266") {
|
||||||
|
toggle('rev');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log('Could not fetch device info:', error);
|
||||||
|
// Fallback to compiled-in value if API call fails
|
||||||
|
document.querySelector('.installed-version').textContent = 'Unknown';
|
||||||
|
document.querySelector('.release-name').textContent = 'Unknown';
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@import url("style.css");
|
@import url("style.css");
|
||||||
@@ -27,11 +46,15 @@
|
|||||||
<body onload="GetV()">
|
<body onload="GetV()">
|
||||||
<h2>WLED Software Update</h2>
|
<h2>WLED Software Update</h2>
|
||||||
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
|
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
|
||||||
Installed version: <span class="sip">WLED ##VERSION##</span><br>
|
Installed version: <span class="sip installed-version">Loading...</span><br>
|
||||||
|
Release: <span class="sip release-name">Loading...</span><br>
|
||||||
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
|
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
|
||||||
style="vertical-align: text-bottom; display: inline-flex;">
|
style="vertical-align: text-bottom; display: inline-flex;">
|
||||||
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
|
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
|
||||||
|
<input type="hidden" name="skipValidation" value="" id="sV">
|
||||||
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
|
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
|
||||||
|
<input type='checkbox' onchange="sV.value=checked?1:''" id="skipValidation">
|
||||||
|
<label for='skipValidation'>Ignore firmware validation</label><br>
|
||||||
<button type="submit">Update!</button><br>
|
<button type="submit">Update!</button><br>
|
||||||
<hr class="sml">
|
<hr class="sml">
|
||||||
<button id="rev" type="button" onclick="cR()">Revert update</button><br>
|
<button id="rev" type="button" onclick="cR()">Revert update</button><br>
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ static dmx_config_t createConfig()
|
|||||||
config.software_version_id = VERSION;
|
config.software_version_id = VERSION;
|
||||||
strcpy(config.device_label, "WLED_MM");
|
strcpy(config.device_label, "WLED_MM");
|
||||||
|
|
||||||
const std::string versionString = "WLED_V" + std::to_string(VERSION);
|
const std::string dmxWledVersionString = "WLED_V" + std::to_string(VERSION);
|
||||||
strncpy(config.software_version_label, versionString.c_str(), 32);
|
strncpy(config.software_version_label, dmxWledVersionString.c_str(), 32);
|
||||||
config.software_version_label[32] = '\0'; // zero termination in case versionString string was longer than 32 chars
|
config.software_version_label[32] = '\0'; // zero termination in case versionString string was longer than 32 chars
|
||||||
|
|
||||||
config.personalities[0].description = "SINGLE_RGB";
|
config.personalities[0].description = "SINGLE_RGB";
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) {
|
|||||||
|
|
||||||
reply->reply_port = ARTNET_DEFAULT_PORT;
|
reply->reply_port = ARTNET_DEFAULT_PORT;
|
||||||
|
|
||||||
char * numberEnd = versionString;
|
char * numberEnd = (char*) versionString; // strtol promises not to try to edit this.
|
||||||
reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
||||||
numberEnd++;
|
numberEnd++;
|
||||||
reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);
|
||||||
|
|||||||
257
wled00/ota_update.cpp
Normal file
257
wled00/ota_update.cpp
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#include "ota_update.h"
|
||||||
|
#include "wled.h"
|
||||||
|
|
||||||
|
#ifdef ESP32
|
||||||
|
#include <esp_app_format.h>
|
||||||
|
#include <esp_ota_ops.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Platform-specific metadata locations
|
||||||
|
#ifdef ESP32
|
||||||
|
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
|
||||||
|
#define UPDATE_ERROR errorString
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
||||||
|
#define UPDATE_ERROR getErrorString
|
||||||
|
#endif
|
||||||
|
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if OTA should be allowed based on release compatibility using custom description
|
||||||
|
* @param binaryData Pointer to binary file data (not modified)
|
||||||
|
* @param dataSize Size of binary data in bytes
|
||||||
|
* @param errorMessage Buffer to store error message if validation fails
|
||||||
|
* @param errorMessageLen Maximum length of error message buffer
|
||||||
|
* @return true if OTA should proceed, false if it should be blocked
|
||||||
|
*/
|
||||||
|
|
||||||
|
static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) {
|
||||||
|
// Clear error message
|
||||||
|
if (errorMessage && errorMessageLen > 0) {
|
||||||
|
errorMessage[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract WLED structure directly from binary data
|
||||||
|
wled_custom_desc_t extractedDesc;
|
||||||
|
bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc);
|
||||||
|
|
||||||
|
if (hasDesc) {
|
||||||
|
return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen);
|
||||||
|
} else {
|
||||||
|
// No custom description - this could be a legacy binary
|
||||||
|
if (errorMessage && errorMessageLen > 0) {
|
||||||
|
strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1);
|
||||||
|
errorMessage[errorMessageLen - 1] = '\0';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpdateContext {
|
||||||
|
// State flags
|
||||||
|
// FUTURE: the flags could be replaced by a state machine
|
||||||
|
bool replySent = false;
|
||||||
|
bool needsRestart = false;
|
||||||
|
bool updateStarted = false;
|
||||||
|
bool uploadComplete = false;
|
||||||
|
bool releaseCheckPassed = false;
|
||||||
|
String errorMessage;
|
||||||
|
|
||||||
|
// Buffer to hold block data across posts, if needed
|
||||||
|
std::vector<uint8_t> releaseMetadataBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static void endOTA(AsyncWebServerRequest *request) {
|
||||||
|
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||||
|
request->_tempObject = nullptr;
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0);
|
||||||
|
if (context) {
|
||||||
|
if (context->updateStarted) { // We initialized the update
|
||||||
|
// We use Update.end() because not all forms of Update() support an abort.
|
||||||
|
// If the upload is incomplete, Update.end(false) should error out.
|
||||||
|
if (Update.end(context->uploadComplete)) {
|
||||||
|
// Update successful!
|
||||||
|
#ifndef ESP8266
|
||||||
|
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
|
||||||
|
#endif
|
||||||
|
doReboot = true;
|
||||||
|
context->needsRestart = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context->needsRestart) {
|
||||||
|
strip.resume();
|
||||||
|
UsermodManager::onUpdateBegin(false);
|
||||||
|
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||||
|
WLED::instance().enableWatchdog();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
delete context;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||||
|
{
|
||||||
|
#ifdef ESP8266
|
||||||
|
Update.runAsync(true);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (Update.isRunning()) {
|
||||||
|
request->send(503);
|
||||||
|
setOTAReplied(request);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||||
|
WLED::instance().disableWatchdog();
|
||||||
|
#endif
|
||||||
|
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
|
||||||
|
|
||||||
|
strip.suspend();
|
||||||
|
backupConfig(); // backup current config in case the update ends badly
|
||||||
|
strip.resetSegments(); // free as much memory as you can
|
||||||
|
context->needsRestart = true;
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
|
||||||
|
|
||||||
|
auto skipValidationParam = request->getParam("skipValidation", true);
|
||||||
|
if (skipValidationParam && (skipValidationParam->value() == "1")) {
|
||||||
|
context->releaseCheckPassed = true;
|
||||||
|
DEBUG_PRINTLN(F("OTA validation skipped by user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin update with the firmware size from content length
|
||||||
|
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
||||||
|
if (!Update.begin(updateSize)) {
|
||||||
|
context->errorMessage = Update.UPDATE_ERROR();
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context->updateStarted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an OTA context object on an AsyncWebServerRequest
|
||||||
|
// Returns true if successful, false on failure.
|
||||||
|
bool initOTA(AsyncWebServerRequest *request) {
|
||||||
|
// Allocate update context
|
||||||
|
UpdateContext* context = new (std::nothrow) UpdateContext {};
|
||||||
|
if (context) {
|
||||||
|
request->_tempObject = context;
|
||||||
|
request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure
|
||||||
|
};
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
|
||||||
|
return (context != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOTAReplied(AsyncWebServerRequest *request) {
|
||||||
|
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||||
|
if (!context) return;
|
||||||
|
context->replySent = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns pointer to error message, or nullptr if OTA was successful.
|
||||||
|
std::pair<bool, String> getOTAResult(AsyncWebServerRequest* request) {
|
||||||
|
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||||
|
if (!context) return { true, F("OTA context unexpectedly missing") };
|
||||||
|
if (context->replySent) return { false, {} };
|
||||||
|
if (context->errorMessage.length()) return { true, context->errorMessage };
|
||||||
|
|
||||||
|
if (context->updateStarted) {
|
||||||
|
// Release the OTA context now.
|
||||||
|
endOTA(request);
|
||||||
|
if (Update.hasError()) {
|
||||||
|
return { true, Update.UPDATE_ERROR() };
|
||||||
|
} else {
|
||||||
|
return { true, {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never happen
|
||||||
|
return { true, F("Internal software failure") };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal)
|
||||||
|
{
|
||||||
|
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
|
||||||
|
if (!context) return;
|
||||||
|
|
||||||
|
//DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal);
|
||||||
|
|
||||||
|
if (context->replySent || (context->errorMessage.length())) return;
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
if (!beginOTA(request, context)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform validation if we haven't done it yet and we have reached the metadata offset
|
||||||
|
if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) {
|
||||||
|
// Current chunk contains the metadata offset
|
||||||
|
size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);
|
||||||
|
|
||||||
|
if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {
|
||||||
|
// We have enough data to validate, one way or another
|
||||||
|
const uint8_t* search_data = data;
|
||||||
|
size_t search_len = len;
|
||||||
|
|
||||||
|
// If we have saved data, use that instead
|
||||||
|
if (context->releaseMetadataBuffer.size()) {
|
||||||
|
// Add this data
|
||||||
|
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||||
|
search_data = context->releaseMetadataBuffer.data();
|
||||||
|
search_len = context->releaseMetadataBuffer.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the checking
|
||||||
|
char errorMessage[128];
|
||||||
|
bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));
|
||||||
|
|
||||||
|
// Release buffer if there was one
|
||||||
|
context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};
|
||||||
|
|
||||||
|
if (!OTA_ok) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
|
||||||
|
context->errorMessage = errorMessage;
|
||||||
|
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
|
||||||
|
context->releaseCheckPassed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Store the data we just got for next pass
|
||||||
|
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if validation was still pending (shouldn't happen normally)
|
||||||
|
// This is done before writing the last chunk, so endOTA can abort
|
||||||
|
if (isFinal && !context->releaseCheckPassed) {
|
||||||
|
DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
|
||||||
|
// Don't write the last chunk to the updater: this will trip an error later
|
||||||
|
context->errorMessage = F("Release check data never arrived?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write chunk data to OTA update (only if release check passed or still pending)
|
||||||
|
if (!Update.hasError()) {
|
||||||
|
if (Update.write(data, len) != len) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isFinal) {
|
||||||
|
DEBUG_PRINTLN(F("OTA Update End"));
|
||||||
|
// Upload complete
|
||||||
|
context->uploadComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
wled00/ota_update.h
Normal file
52
wled00/ota_update.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// WLED OTA update interface
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#ifdef ESP8266
|
||||||
|
#include <Updater.h>
|
||||||
|
#else
|
||||||
|
#include <Update.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Platform-specific metadata locations
|
||||||
|
#ifdef ESP32
|
||||||
|
#define BUILD_METADATA_SECTION ".rodata_custom_desc"
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
#define BUILD_METADATA_SECTION ".ver_number"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncWebServerRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an OTA context object on an AsyncWebServerRequest
|
||||||
|
* @param request Pointer to web request object
|
||||||
|
* @return true if allocation was successful, false if not
|
||||||
|
*/
|
||||||
|
bool initOTA(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate to the OTA subsystem that a reply has already been generated
|
||||||
|
* @param request Pointer to web request object
|
||||||
|
*/
|
||||||
|
void setOTAReplied(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the OTA result.
|
||||||
|
* @param request Pointer to web request object
|
||||||
|
* @return bool indicating if a reply is necessary; string with error message if the update failed.
|
||||||
|
*/
|
||||||
|
std::pair<bool, String> getOTAResult(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a block of OTA data. This is a passthrough of an ArUploadHandlerFunction.
|
||||||
|
* Requires that initOTA be called on the handler object before any work will be done.
|
||||||
|
* @param request Pointer to web request object
|
||||||
|
* @param index Offset in to uploaded file
|
||||||
|
* @param data New data bytes
|
||||||
|
* @param len Length of new data bytes
|
||||||
|
* @param isFinal Indicates that this is the last block
|
||||||
|
* @return bool indicating if a reply is necessary; string with error message if the update failed.
|
||||||
|
*/
|
||||||
|
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp!
|
#define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp!
|
||||||
#include "wled.h"
|
#include "wled.h"
|
||||||
#include "wled_ethernet.h"
|
#include "wled_ethernet.h"
|
||||||
|
#include "ota_update.h"
|
||||||
#ifdef WLED_ENABLE_AOTA
|
#ifdef WLED_ENABLE_AOTA
|
||||||
#define NO_OTA_PORT
|
#define NO_OTA_PORT
|
||||||
#include <ArduinoOTA.h>
|
#include <ArduinoOTA.h>
|
||||||
@@ -174,8 +175,8 @@ void WLED::loop()
|
|||||||
uint32_t heap = getFreeHeapSize();
|
uint32_t heap = getFreeHeapSize();
|
||||||
if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) {
|
if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) {
|
||||||
DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
|
DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
|
||||||
forceReconnect = true;
|
|
||||||
strip.resetSegments(); // remove all but one segments from memory
|
strip.resetSegments(); // remove all but one segments from memory
|
||||||
|
if (!Update.isRunning()) forceReconnect = true;
|
||||||
} else if (heap < MIN_HEAP_SIZE) {
|
} else if (heap < MIN_HEAP_SIZE) {
|
||||||
DEBUG_PRINTLN(F("Heap low, purging segments."));
|
DEBUG_PRINTLN(F("Heap low, purging segments."));
|
||||||
strip.purgeSegments();
|
strip.purgeSegments();
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
|
|||||||
#include "colors.h"
|
#include "colors.h"
|
||||||
#include "bus_manager.h"
|
#include "bus_manager.h"
|
||||||
#include "FX.h"
|
#include "FX.h"
|
||||||
|
#include "wled_metadata.h"
|
||||||
|
|
||||||
#ifndef CLIENT_SSID
|
#ifndef CLIENT_SSID
|
||||||
#define CLIENT_SSID DEFAULT_CLIENT_SSID
|
#define CLIENT_SSID DEFAULT_CLIENT_SSID
|
||||||
@@ -270,20 +271,6 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
|
|||||||
#define STRINGIFY(X) #X
|
#define STRINGIFY(X) #X
|
||||||
#define TOSTRING(X) STRINGIFY(X)
|
#define TOSTRING(X) STRINGIFY(X)
|
||||||
|
|
||||||
#ifndef WLED_VERSION
|
|
||||||
#define WLED_VERSION dev
|
|
||||||
#endif
|
|
||||||
#ifndef WLED_RELEASE_NAME
|
|
||||||
#define WLED_RELEASE_NAME "Custom"
|
|
||||||
#endif
|
|
||||||
#ifndef WLED_REPO
|
|
||||||
#define WLED_REPO "unknown"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Global Variable definitions
|
|
||||||
WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION));
|
|
||||||
WLED_GLOBAL char releaseString[] _INIT(WLED_RELEASE_NAME); // must include the quotes when defining, e.g -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\"
|
|
||||||
WLED_GLOBAL char repoString[] _INIT(WLED_REPO);
|
|
||||||
#define WLED_CODENAME "Niji"
|
#define WLED_CODENAME "Niji"
|
||||||
|
|
||||||
// AP and OTA default passwords (for maximum security change them!)
|
// AP and OTA default passwords (for maximum security change them!)
|
||||||
|
|||||||
144
wled00/wled_metadata.cpp
Normal file
144
wled00/wled_metadata.cpp
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
#include "ota_update.h"
|
||||||
|
#include "wled.h"
|
||||||
|
|
||||||
|
#define WLED_CUSTOM_DESC_MAGIC 0x57535453 // "WSTS" (WLED System Tag Structure)
|
||||||
|
#define WLED_CUSTOM_DESC_VERSION 1
|
||||||
|
#define WLED_RELEASE_NAME_MAX_LEN 48
|
||||||
|
|
||||||
|
// Compile-time validation that release name doesn't exceed maximum length
|
||||||
|
static_assert(sizeof(WLED_RELEASE_NAME) <= WLED_RELEASE_NAME_MAX_LEN,
|
||||||
|
"WLED_RELEASE_NAME exceeds maximum length of WLED_RELEASE_NAME_MAX_LEN characters");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DJB2 hash function (C++11 compatible constexpr)
|
||||||
|
* Used for compile-time hash computation to validate structure contents
|
||||||
|
* Recursive for compile time: not usable at runtime due to stack depth
|
||||||
|
*
|
||||||
|
* Note that this only works on strings; there is no way to produce a compile-time
|
||||||
|
* hash of a struct in C++11 without explicitly listing all the struct members.
|
||||||
|
* So for now, we hash only the release name. This suffices for a "did you find
|
||||||
|
* valid structure" check.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constexpr uint32_t djb2_hash_constexpr(const char* str, uint32_t hash = 5381) {
|
||||||
|
return (*str == '\0') ? hash : djb2_hash_constexpr(str + 1, ((hash << 5) + hash) + *str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime DJB2 hash function for validation
|
||||||
|
*/
|
||||||
|
inline uint32_t djb2_hash_runtime(const char* str) {
|
||||||
|
uint32_t hash = 5381;
|
||||||
|
while (*str) {
|
||||||
|
hash = ((hash << 5) + hash) + *str++;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// GLOBAL VARIABLES
|
||||||
|
// ------------------------------------
|
||||||
|
// Structure instantiation for this build
|
||||||
|
const wled_custom_desc_t __attribute__((section(BUILD_METADATA_SECTION))) WLED_BUILD_DESCRIPTION = {
|
||||||
|
WLED_CUSTOM_DESC_MAGIC, // magic
|
||||||
|
WLED_CUSTOM_DESC_VERSION, // version
|
||||||
|
TOSTRING(WLED_VERSION),
|
||||||
|
WLED_RELEASE_NAME, // release_name
|
||||||
|
std::integral_constant<uint32_t, djb2_hash_constexpr(WLED_RELEASE_NAME)>::value, // hash - computed at compile time; integral_constant enforces this
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char repoString_s[] PROGMEM = WLED_REPO;
|
||||||
|
const __FlashStringHelper* repoString = FPSTR(repoString_s);
|
||||||
|
|
||||||
|
static const char productString_s[] PROGMEM = WLED_PRODUCT_NAME;
|
||||||
|
const __FlashStringHelper* productString = FPSTR(productString_s);
|
||||||
|
|
||||||
|
static const char brandString_s [] PROGMEM = WLED_BRAND;
|
||||||
|
const __FlashStringHelper* brandString = FPSTR(brandString_s);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract WLED custom description structure from binary
|
||||||
|
* @param binaryData Pointer to binary file data
|
||||||
|
* @param dataSize Size of binary data in bytes
|
||||||
|
* @param extractedDesc Buffer to store extracted custom description structure
|
||||||
|
* @return true if structure was found and extracted, false otherwise
|
||||||
|
*/
|
||||||
|
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_custom_desc_t* extractedDesc) {
|
||||||
|
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_custom_desc_t)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t offset = 0; offset <= dataSize - sizeof(wled_custom_desc_t); offset++) {
|
||||||
|
const wled_custom_desc_t* custom_desc = (const wled_custom_desc_t*)(binaryData + offset);
|
||||||
|
|
||||||
|
// Check for magic number
|
||||||
|
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
|
||||||
|
// Found potential match, validate version
|
||||||
|
if (custom_desc->desc_version != WLED_CUSTOM_DESC_VERSION) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
|
||||||
|
offset, custom_desc->version);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hash using runtime function
|
||||||
|
uint32_t expected_hash = djb2_hash_runtime(custom_desc->release_name);
|
||||||
|
if (custom_desc->hash != expected_hash) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid structure found - copy entire structure
|
||||||
|
memcpy(extractedDesc, custom_desc, sizeof(wled_custom_desc_t));
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
|
||||||
|
offset, extractedDesc->release_name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if OTA should be allowed based on release compatibility using custom description
|
||||||
|
* @param binaryData Pointer to binary file data (not modified)
|
||||||
|
* @param dataSize Size of binary data in bytes
|
||||||
|
* @param errorMessage Buffer to store error message if validation fails
|
||||||
|
* @param errorMessageLen Maximum length of error message buffer
|
||||||
|
* @return true if OTA should proceed, false if it should be blocked
|
||||||
|
*/
|
||||||
|
|
||||||
|
bool shouldAllowOTA(const wled_custom_desc_t& firmwareDescription, char* errorMessage, size_t errorMessageLen) {
|
||||||
|
// Clear error message
|
||||||
|
if (errorMessage && errorMessageLen > 0) {
|
||||||
|
errorMessage[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate compatibility using extracted release name
|
||||||
|
// We make a stack copy so we can print it safely
|
||||||
|
char safeFirmwareRelease[WLED_RELEASE_NAME_MAX_LEN];
|
||||||
|
strncpy(safeFirmwareRelease, firmwareDescription.release_name, WLED_RELEASE_NAME_MAX_LEN - 1);
|
||||||
|
safeFirmwareRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
|
||||||
|
|
||||||
|
if (strlen(safeFirmwareRelease) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strncmp_P(safeFirmwareRelease, releaseString, WLED_RELEASE_NAME_MAX_LEN) != 0) {
|
||||||
|
if (errorMessage && errorMessageLen > 0) {
|
||||||
|
snprintf_P(errorMessage, errorMessageLen, PSTR("Firmware compatibility mismatch: current='%s', uploaded='%s'."),
|
||||||
|
releaseString, safeFirmwareRelease);
|
||||||
|
errorMessage[errorMessageLen - 1] = '\0'; // Ensure null termination
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: additional checks go here
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
71
wled00/wled_metadata.h
Normal file
71
wled00/wled_metadata.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
WLED build metadata
|
||||||
|
|
||||||
|
Manages and exports information about the current WLED build.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string.h>
|
||||||
|
#include <WString.h>
|
||||||
|
|
||||||
|
#ifndef WLED_VERSION
|
||||||
|
#define WLED_VERSION dev
|
||||||
|
#endif
|
||||||
|
#ifndef WLED_RELEASE_NAME
|
||||||
|
#define WLED_RELEASE_NAME "Custom"
|
||||||
|
#endif
|
||||||
|
#ifndef WLED_REPO
|
||||||
|
#define WLED_REPO "unknown"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define WLED_VERSION_MAX_LEN 48
|
||||||
|
#define WLED_RELEASE_NAME_MAX_LEN 48
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WLED Custom Description Structure
|
||||||
|
* This structure is embedded in platform-specific sections at an approximately
|
||||||
|
* fixed offset in ESP32/ESP8266 binaries, where it can be found and validated
|
||||||
|
* by the OTA process.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint32_t magic; // Magic number to identify WLED custom description
|
||||||
|
uint32_t desc_version; // Structure version for future compatibility
|
||||||
|
char wled_version[WLED_VERSION_MAX_LEN];
|
||||||
|
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
|
||||||
|
uint32_t hash; // Structure sanity check
|
||||||
|
} __attribute__((packed)) wled_custom_desc_t;
|
||||||
|
|
||||||
|
|
||||||
|
// Global build description
|
||||||
|
extern const wled_custom_desc_t WLED_BUILD_DESCRIPTION;
|
||||||
|
|
||||||
|
// Convenient metdata pointers
|
||||||
|
#define versionString (WLED_BUILD_DESCRIPTION.wled_version) // Build version, WLED_VERSION
|
||||||
|
#define releaseString (WLED_BUILD_DESCRIPTION.release_name) // Release name, WLED_RELEASE_NAME
|
||||||
|
extern const __FlashStringHelper* repoString; // Github repository (if available)
|
||||||
|
extern const __FlashStringHelper* productString; // Product, WLED_PRODUCT_NAME -- deprecated, use WLED_RELEASE_NAME
|
||||||
|
extern const __FlashStringHelper* brandString ; // Brand
|
||||||
|
|
||||||
|
|
||||||
|
// Metadata analysis functions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract WLED custom description structure from binary data
|
||||||
|
* @param binaryData Pointer to binary file data
|
||||||
|
* @param dataSize Size of binary data in bytes
|
||||||
|
* @param extractedDesc Buffer to store extracted custom description structure
|
||||||
|
* @return true if structure was found and extracted, false otherwise
|
||||||
|
*/
|
||||||
|
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_custom_desc_t* extractedDesc);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if OTA should be allowed based on release compatibility
|
||||||
|
* @param firmwareDescription Pointer to firmware description
|
||||||
|
* @param errorMessage Buffer to store error message if validation fails
|
||||||
|
* @param errorMessageLen Maximum length of error message buffer
|
||||||
|
* @return true if OTA should proceed, false if it should be blocked
|
||||||
|
*/
|
||||||
|
bool shouldAllowOTA(const wled_custom_desc_t& firmwareDescription, char* errorMessage, size_t errorMessageLen);
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
#include "wled.h"
|
#include "wled.h"
|
||||||
|
|
||||||
#ifndef WLED_DISABLE_OTA
|
#ifndef WLED_DISABLE_OTA
|
||||||
#ifdef ESP8266
|
#include "ota_update.h"
|
||||||
#include <Updater.h>
|
|
||||||
#else
|
|
||||||
#include <Update.h>
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
#include "html_ui.h"
|
#include "html_ui.h"
|
||||||
#include "html_settings.h"
|
#include "html_settings.h"
|
||||||
@@ -404,59 +400,47 @@ void initServer()
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){
|
server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){
|
||||||
if (!correctPIN) {
|
if (request->_tempObject) {
|
||||||
serveSettings(request, true); // handle PIN page POST request
|
auto ota_result = getOTAResult(request);
|
||||||
return;
|
if (ota_result.first) {
|
||||||
}
|
if (ota_result.second.length() > 0) {
|
||||||
if (otaLock) {
|
serveMessage(request, 500, F("Update failed!"), ota_result.second, 254);
|
||||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Update.hasError()) {
|
|
||||||
serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254);
|
|
||||||
} else {
|
} else {
|
||||||
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
|
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
|
||||||
#ifndef ESP8266
|
}
|
||||||
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
|
}
|
||||||
#endif
|
} else {
|
||||||
doReboot = true;
|
// No context structure - something's gone horribly wrong
|
||||||
|
serveMessage(request, 500, F("Update failed!"), F("Internal server fault"), 254);
|
||||||
}
|
}
|
||||||
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
|
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
|
||||||
|
if (index == 0) {
|
||||||
|
// Allocate the context structure
|
||||||
|
if (!initOTA(request)) {
|
||||||
|
return; // Error will be dealt with after upload in response handler, above
|
||||||
|
}
|
||||||
|
|
||||||
|
// Privilege checks
|
||||||
IPAddress client = request->client()->remoteIP();
|
IPAddress client = request->client()->remoteIP();
|
||||||
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
|
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
|
||||||
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
|
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
|
||||||
request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied));
|
serveMessage(request, 401, FPSTR(s_accessdenied), F("Client is not on local subnet."), 254);
|
||||||
|
setOTAReplied(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!correctPIN || otaLock) return;
|
if (!correctPIN) {
|
||||||
if(!index){
|
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||||
DEBUG_PRINTLN(F("OTA Update Start"));
|
setOTAReplied(request);
|
||||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
return;
|
||||||
WLED::instance().disableWatchdog();
|
};
|
||||||
#endif
|
if (otaLock) {
|
||||||
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
|
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
|
||||||
lastEditTime = millis(); // make sure PIN does not lock during update
|
setOTAReplied(request);
|
||||||
strip.suspend();
|
return;
|
||||||
backupConfig(); // backup current config in case the update ends badly
|
|
||||||
strip.resetSegments(); // free as much memory as you can
|
|
||||||
#ifdef ESP8266
|
|
||||||
Update.runAsync(true);
|
|
||||||
#endif
|
|
||||||
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
|
||||||
}
|
|
||||||
if(!Update.hasError()) Update.write(data, len);
|
|
||||||
if(isFinal){
|
|
||||||
if(Update.end(true)){
|
|
||||||
DEBUG_PRINTLN(F("Update Success"));
|
|
||||||
} else {
|
|
||||||
DEBUG_PRINTLN(F("Update Failed"));
|
|
||||||
strip.resume();
|
|
||||||
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
|
|
||||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
|
||||||
WLED::instance().enableWatchdog();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleOTAData(request, index, data, len, isFinal);
|
||||||
});
|
});
|
||||||
#else
|
#else
|
||||||
const auto notSupported = [](AsyncWebServerRequest *request){
|
const auto notSupported = [](AsyncWebServerRequest *request){
|
||||||
|
|||||||
@@ -671,16 +671,6 @@ void getSettingsJS(byte subPage, Print& settingsScript)
|
|||||||
UsermodManager::appendConfigData(settingsScript);
|
UsermodManager::appendConfigData(settingsScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subPage == SUBPAGE_UPDATE) // update
|
|
||||||
{
|
|
||||||
char tmp_buf[128];
|
|
||||||
fillWLEDVersion(tmp_buf,sizeof(tmp_buf));
|
|
||||||
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);
|
|
||||||
#ifndef ARDUINO_ARCH_ESP32
|
|
||||||
settingsScript.print(F("toggle('rev');")); // hide revert button on ESP8266
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subPage == SUBPAGE_2D) // 2D matrices
|
if (subPage == SUBPAGE_2D) // 2D matrices
|
||||||
{
|
{
|
||||||
printSetFormValue(settingsScript,PSTR("SOMP"),strip.isMatrix);
|
printSetFormValue(settingsScript,PSTR("SOMP"),strip.isMatrix);
|
||||||
|
|||||||
Reference in New Issue
Block a user