Implement OTA release compatibility checking system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
This commit is contained in:
@@ -95,6 +95,12 @@ function adoptVersionAndRepo(html) {
|
|||||||
if (version) {
|
if (version) {
|
||||||
html = html.replaceAll("##VERSION##", version);
|
html = html.replaceAll("##VERSION##", version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace ##RELEASE## with a placeholder that will be replaced at runtime
|
||||||
|
// Since we can't determine the exact release name at build time (it depends on build flags),
|
||||||
|
// we'll use a special marker that gets replaced in the server code
|
||||||
|
html = html.replaceAll("##RELEASE##", "%RELEASE%");
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,13 @@
|
|||||||
<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">WLED ##VERSION##</span><br>
|
||||||
|
Release: <span class="sip">##RELEASE##</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='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' name='ignoreRelease' id='ignoreRelease'>
|
||||||
|
<label for='ignoreRelease'>Ignore release name check</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>
|
||||||
|
|||||||
151
wled00/ota_release_check.cpp
Normal file
151
wled00/ota_release_check.cpp
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#include "ota_release_check.h"
|
||||||
|
#include "wled.h"
|
||||||
|
|
||||||
|
// Maximum size to scan in binary (we don't need to scan the entire file)
|
||||||
|
#define MAX_SCAN_SIZE 32768
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a string pattern in binary data using Boyer-Moore-like approach
|
||||||
|
*/
|
||||||
|
static int findStringInBinary(const uint8_t* data, size_t dataSize, const char* pattern) {
|
||||||
|
size_t patternLen = strlen(pattern);
|
||||||
|
if (patternLen == 0 || patternLen > dataSize) return -1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i <= dataSize - patternLen; i++) {
|
||||||
|
if (memcmp(data + i, pattern, patternLen) == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract null-terminated string from binary data starting at position
|
||||||
|
*/
|
||||||
|
static int extractNullTerminatedString(const uint8_t* data, size_t dataSize, int position, char* output, size_t maxLen) {
|
||||||
|
if (position < 0 || position >= dataSize) return 0;
|
||||||
|
|
||||||
|
size_t len = 0;
|
||||||
|
for (size_t i = position; i < dataSize && len < (maxLen - 1); i++) {
|
||||||
|
if (data[i] == 0) break; // null terminator
|
||||||
|
output[len++] = data[i];
|
||||||
|
}
|
||||||
|
output[len] = '\0';
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool extractReleaseNameFromBinary(const uint8_t* binaryData, size_t dataSize, char* extractedRelease) {
|
||||||
|
if (!binaryData || !extractedRelease || dataSize == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit scan size to avoid performance issues with large binaries
|
||||||
|
size_t scanSize = (dataSize > MAX_SCAN_SIZE) ? MAX_SCAN_SIZE : dataSize;
|
||||||
|
|
||||||
|
// Known WLED release name patterns - we'll look for these in the binary
|
||||||
|
// Order by specificity (more specific first)
|
||||||
|
const char* releasePatterns[] = {
|
||||||
|
"ESP32_Ethernet",
|
||||||
|
"ESP32_USERMODS",
|
||||||
|
"ESP32_WROVER",
|
||||||
|
"ESP32-S3_16MB_opi",
|
||||||
|
"ESP32-S3_8MB_opi",
|
||||||
|
"ESP32-S3_WROOM-2",
|
||||||
|
"ESP32-S3_4M_qspi",
|
||||||
|
"ESP32-S3",
|
||||||
|
"ESP32-S2",
|
||||||
|
"ESP32-C3",
|
||||||
|
"ESP32_V4",
|
||||||
|
"ESP32_8M",
|
||||||
|
"ESP32_16M",
|
||||||
|
"ESP32",
|
||||||
|
"ESP8266_160",
|
||||||
|
"ESP8266_compat",
|
||||||
|
"ESP8266",
|
||||||
|
"ESP02_compat",
|
||||||
|
"ESP02_160",
|
||||||
|
"ESP02",
|
||||||
|
"ESP01_compat",
|
||||||
|
"ESP01_160",
|
||||||
|
"ESP01",
|
||||||
|
"Custom",
|
||||||
|
NULL // sentinel
|
||||||
|
};
|
||||||
|
|
||||||
|
// First try to find the exact current releaseString in the binary
|
||||||
|
// This is the most reliable method if the string exists
|
||||||
|
int pos = findStringInBinary(binaryData, scanSize, releaseString);
|
||||||
|
if (pos >= 0) {
|
||||||
|
strcpy(extractedRelease, releaseString);
|
||||||
|
DEBUG_PRINTF_P(PSTR("Found exact current release string in binary: %s\n"), extractedRelease);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search through the binary for known release patterns
|
||||||
|
// We'll search through the entire scanSize, not just fixed positions
|
||||||
|
for (int i = 0; releasePatterns[i] != NULL; i++) {
|
||||||
|
pos = findStringInBinary(binaryData, scanSize, releasePatterns[i]);
|
||||||
|
if (pos >= 0) {
|
||||||
|
// Found a potential release name, extract full null-terminated string
|
||||||
|
int len = extractNullTerminatedString(binaryData, scanSize, pos, extractedRelease, 64);
|
||||||
|
if (len > 0) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("Found release name pattern: %s\n"), extractedRelease);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_PRINTLN(F("Could not extract release name from binary"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateReleaseCompatibility(const char* extractedRelease) {
|
||||||
|
if (!extractedRelease || strlen(extractedRelease) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple string comparison - releases must match exactly
|
||||||
|
bool match = strcmp(releaseString, extractedRelease) == 0;
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("Release compatibility check: current='%s', uploaded='%s', match=%s\n"),
|
||||||
|
releaseString, extractedRelease, match ? "YES" : "NO");
|
||||||
|
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage) {
|
||||||
|
// Clear error message
|
||||||
|
if (errorMessage) {
|
||||||
|
errorMessage[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user chose to ignore release check, allow OTA
|
||||||
|
if (ignoreReleaseCheck) {
|
||||||
|
DEBUG_PRINTLN(F("OTA release check bypassed by user"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract release name from binary
|
||||||
|
char extractedRelease[64];
|
||||||
|
if (!extractReleaseNameFromBinary(binaryData, dataSize, extractedRelease)) {
|
||||||
|
if (errorMessage) {
|
||||||
|
strcpy(errorMessage, "Could not determine release type of uploaded file. Check 'Ignore release name check' to proceed.");
|
||||||
|
}
|
||||||
|
DEBUG_PRINTLN(F("OTA blocked: Could not extract release name"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate compatibility
|
||||||
|
if (!validateReleaseCompatibility(extractedRelease)) {
|
||||||
|
if (errorMessage) {
|
||||||
|
snprintf(errorMessage, 127, "Release mismatch: current='%s', uploaded='%s'. Check 'Ignore release name check' to proceed.",
|
||||||
|
releaseString, extractedRelease);
|
||||||
|
}
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA blocked: Release mismatch current='%s', uploaded='%s'\n"),
|
||||||
|
releaseString, extractedRelease);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_PRINTLN(F("OTA allowed: Release names match"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
37
wled00/ota_release_check.h
Normal file
37
wled00/ota_release_check.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef WLED_OTA_RELEASE_CHECK_H
|
||||||
|
#define WLED_OTA_RELEASE_CHECK_H
|
||||||
|
|
||||||
|
/*
|
||||||
|
* OTA Release Compatibility Checking
|
||||||
|
* Functions to extract and validate release names from uploaded binary files
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract release name from ESP32/ESP8266 binary data
|
||||||
|
* @param binaryData Pointer to binary file data
|
||||||
|
* @param dataSize Size of binary data in bytes
|
||||||
|
* @param extractedRelease Buffer to store extracted release name (should be at least 64 bytes)
|
||||||
|
* @return true if release name was extracted successfully, false otherwise
|
||||||
|
*/
|
||||||
|
bool extractReleaseNameFromBinary(const uint8_t* binaryData, size_t dataSize, char* extractedRelease);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if extracted release name matches current release
|
||||||
|
* @param extractedRelease Release name from uploaded binary
|
||||||
|
* @return true if releases match (OTA should proceed), false if they don't match
|
||||||
|
*/
|
||||||
|
bool validateReleaseCompatibility(const char* extractedRelease);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if OTA should be allowed based on release compatibility
|
||||||
|
* @param binaryData Pointer to binary file data
|
||||||
|
* @param dataSize Size of binary data in bytes
|
||||||
|
* @param ignoreReleaseCheck If true, skip release validation
|
||||||
|
* @param errorMessage Buffer to store error message if validation fails (should be at least 128 bytes)
|
||||||
|
* @return true if OTA should proceed, false if it should be blocked
|
||||||
|
*/
|
||||||
|
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage);
|
||||||
|
|
||||||
|
#endif // WLED_OTA_RELEASE_CHECK_H
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#else
|
#else
|
||||||
#include <Update.h>
|
#include <Update.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include "ota_release_check.h"
|
||||||
#endif
|
#endif
|
||||||
#include "html_ui.h"
|
#include "html_ui.h"
|
||||||
#include "html_settings.h"
|
#include "html_settings.h"
|
||||||
@@ -176,6 +177,14 @@ static String msgProcessor(const String& var)
|
|||||||
return String();
|
return String();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String updateProcessor(const String& var)
|
||||||
|
{
|
||||||
|
if (var == F("RELEASE")) {
|
||||||
|
return String(releaseString);
|
||||||
|
}
|
||||||
|
return String();
|
||||||
|
}
|
||||||
|
|
||||||
static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) {
|
static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) {
|
||||||
if (!correctPIN) {
|
if (!correctPIN) {
|
||||||
if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
|
if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
|
||||||
@@ -424,32 +433,105 @@ void initServer()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!correctPIN || otaLock) return;
|
if (!correctPIN || otaLock) return;
|
||||||
|
|
||||||
|
// Static variables to track release check state across chunks
|
||||||
|
static bool releaseCheckDone = false;
|
||||||
|
static bool releaseCheckPassed = false;
|
||||||
|
static uint8_t* releaseCheckBuffer = nullptr;
|
||||||
|
static size_t bufferPos = 0;
|
||||||
|
static const size_t RELEASE_CHECK_BUFFER_SIZE = 8192; // Check first 8KB for release name
|
||||||
|
|
||||||
if(!index){
|
if(!index){
|
||||||
DEBUG_PRINTLN(F("OTA Update Start"));
|
DEBUG_PRINTLN(F("OTA Update Start"));
|
||||||
|
|
||||||
|
// Reset release check state
|
||||||
|
releaseCheckDone = false;
|
||||||
|
releaseCheckPassed = false;
|
||||||
|
bufferPos = 0;
|
||||||
|
|
||||||
|
// Allocate buffer for release check
|
||||||
|
if (releaseCheckBuffer) free(releaseCheckBuffer);
|
||||||
|
releaseCheckBuffer = (uint8_t*)malloc(RELEASE_CHECK_BUFFER_SIZE);
|
||||||
|
if (!releaseCheckBuffer) {
|
||||||
|
DEBUG_PRINTLN(F("OTA Failed: Could not allocate buffer for release check"));
|
||||||
|
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("OTA Failed: Memory allocation error"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||||
WLED::instance().disableWatchdog();
|
WLED::instance().disableWatchdog();
|
||||||
#endif
|
#endif
|
||||||
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
|
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
|
||||||
lastEditTime = millis(); // make sure PIN does not lock during update
|
lastEditTime = millis(); // make sure PIN does not lock during update
|
||||||
strip.suspend();
|
|
||||||
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){
|
// Accumulate data for release check if we haven't checked yet
|
||||||
if(Update.end(true)){
|
if (!releaseCheckDone && releaseCheckBuffer) {
|
||||||
DEBUG_PRINTLN(F("Update Success"));
|
size_t copyLen = min(len, RELEASE_CHECK_BUFFER_SIZE - bufferPos);
|
||||||
} else {
|
if (copyLen > 0) {
|
||||||
DEBUG_PRINTLN(F("Update Failed"));
|
memcpy(releaseCheckBuffer + bufferPos, data, copyLen);
|
||||||
strip.resume();
|
bufferPos += copyLen;
|
||||||
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
|
}
|
||||||
#if WLED_WATCHDOG_TIMEOUT > 0
|
|
||||||
WLED::instance().enableWatchdog();
|
// Check if we have enough data or this is the final chunk
|
||||||
|
if (bufferPos >= RELEASE_CHECK_BUFFER_SIZE || isFinal) {
|
||||||
|
releaseCheckDone = true;
|
||||||
|
|
||||||
|
// Check if user wants to ignore release check
|
||||||
|
bool ignoreRelease = request->hasParam("ignoreRelease", true);
|
||||||
|
|
||||||
|
char errorMessage[128];
|
||||||
|
releaseCheckPassed = shouldAllowOTA(releaseCheckBuffer, bufferPos, ignoreRelease, errorMessage);
|
||||||
|
|
||||||
|
if (!releaseCheckPassed) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("OTA blocked: %s\n"), errorMessage);
|
||||||
|
free(releaseCheckBuffer);
|
||||||
|
releaseCheckBuffer = nullptr;
|
||||||
|
strip.resume();
|
||||||
|
UsermodManager::onUpdateBegin(false);
|
||||||
|
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||||
|
WLED::instance().enableWatchdog();
|
||||||
|
#endif
|
||||||
|
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_PRINTLN(F("Release check passed, starting OTA update"));
|
||||||
|
|
||||||
|
// Now start the actual OTA update
|
||||||
|
strip.suspend();
|
||||||
|
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
|
#endif
|
||||||
|
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only write data if release check passed (or was bypassed)
|
||||||
|
if (releaseCheckDone && releaseCheckPassed && !Update.hasError()) {
|
||||||
|
Update.write(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isFinal){
|
||||||
|
// Clean up release check buffer
|
||||||
|
if (releaseCheckBuffer) {
|
||||||
|
free(releaseCheckBuffer);
|
||||||
|
releaseCheckBuffer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseCheckPassed) {
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -697,5 +779,18 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
|
|||||||
case SUBPAGE_WELCOME : content = PAGE_welcome; len = PAGE_welcome_length; break;
|
case SUBPAGE_WELCOME : content = PAGE_welcome; len = PAGE_welcome_length; break;
|
||||||
default: content = PAGE_settings; len = PAGE_settings_length; break;
|
default: content = PAGE_settings; len = PAGE_settings_length; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef WLED_DISABLE_OTA
|
||||||
|
// Use processor for update page to replace %RELEASE% placeholder
|
||||||
|
if (subPage == SUBPAGE_UPDATE) {
|
||||||
|
if (handleIfNoneMatchCacheHeader(request, code, 0)) return;
|
||||||
|
AsyncWebServerResponse *response = request->beginResponse_P(code, contentType, content, len, updateProcessor);
|
||||||
|
if (content != PAGE_settingsCss) response->addHeader(FPSTR(s_content_enc), F("gzip"));
|
||||||
|
setStaticContentCacheHeaders(response, code, 0);
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
handleStaticContent(request, "", code, contentType, content, len);
|
handleStaticContent(request, "", code, contentType, content, len);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user