Skip to content

Commit 00e026c

Browse files
netmindzsofthack007
authored andcommitted
Merge pull request wled#5126 from wled/copilot/backport-version-reporting-0-15-x
Backport version reporting (PR wled#5093 and wled#5111) to 0.15.x
1 parent f423710 commit 00e026c

File tree

5 files changed

+407
-6
lines changed

5 files changed

+407
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ compile_commands.json
2727
/wled00/Release
2828
/wled00/wled00.ino.cpp
2929
/wled00/html_*.h
30+
_codeql_detected_source_root

wled00/json.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "wled.h"
2+
#include "ota_update.h"
23

34
#include "palettes.h"
45

@@ -938,7 +939,7 @@ void serializeInfo(JsonObject root)
938939
//root[F("cn")] = F(WLED_CODENAME); //WLEDMM removed
939940
root[F("release")] = FPSTR(releaseString);
940941
root[F("rel")] = FPSTR(releaseString); //WLEDMM to add bin name
941-
942+
//root[F("repo")] = repoString; // WLEDMM not availeable
942943
root[F("deviceId")] = getDeviceId();
943944

944945
JsonObject leds = root.createNestedObject("leds");
@@ -1083,6 +1084,9 @@ void serializeInfo(JsonObject root)
10831084

10841085
root[F("lwip")] = 0; //deprecated
10851086
root[F("totalheap")] = ESP.getHeapSize(); //WLEDMM
1087+
#ifndef WLED_DISABLE_OTA
1088+
root[F("bootloaderSHA256")] = getBootloaderSHA256Hex();
1089+
#endif
10861090
#else
10871091
root[F("arch")] = "esp8266";
10881092
root[F("core")] = ESP.getCoreVersion();

wled00/ota_update.cpp

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
#include "ota_update.h"
2+
#include "wled.h"
3+
4+
#ifdef ESP32
5+
#include <esp_ota_ops.h>
6+
#include <esp_spi_flash.h>
7+
#include <mbedtls/sha256.h>
8+
#endif
9+
10+
// Platform-specific metadata locations
11+
#ifdef ESP32
12+
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
13+
#define UPDATE_ERROR errorString
14+
15+
// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB
16+
// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064
17+
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
18+
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
19+
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
20+
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
21+
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
22+
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
23+
#else
24+
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
25+
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
26+
#endif
27+
28+
#elif defined(ESP8266)
29+
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
30+
#define UPDATE_ERROR getErrorString
31+
#endif
32+
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
33+
34+
35+
/**
36+
* Check if OTA should be allowed based on release compatibility using custom description
37+
* @param binaryData Pointer to binary file data (not modified)
38+
* @param dataSize Size of binary data in bytes
39+
* @param errorMessage Buffer to store error message if validation fails
40+
* @param errorMessageLen Maximum length of error message buffer
41+
* @return true if OTA should proceed, false if it should be blocked
42+
*/
43+
44+
static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) {
45+
// Clear error message
46+
if (errorMessage && errorMessageLen > 0) {
47+
errorMessage[0] = '\0';
48+
}
49+
50+
// Try to extract WLED structure directly from binary data
51+
wled_metadata_t extractedDesc;
52+
bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc);
53+
54+
if (hasDesc) {
55+
return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen);
56+
} else {
57+
// No custom description - this could be a legacy binary
58+
if (errorMessage && errorMessageLen > 0) {
59+
strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1);
60+
errorMessage[errorMessageLen - 1] = '\0';
61+
}
62+
return false;
63+
}
64+
}
65+
66+
struct UpdateContext {
67+
// State flags
68+
// FUTURE: the flags could be replaced by a state machine
69+
bool replySent = false;
70+
bool needsRestart = false;
71+
bool updateStarted = false;
72+
bool uploadComplete = false;
73+
bool releaseCheckPassed = false;
74+
String errorMessage;
75+
76+
// Buffer to hold block data across posts, if needed
77+
std::vector<uint8_t> releaseMetadataBuffer;
78+
};
79+
80+
81+
static void endOTA(AsyncWebServerRequest *request) {
82+
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
83+
request->_tempObject = nullptr;
84+
85+
DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0);
86+
if (context) {
87+
if (context->updateStarted) { // We initialized the update
88+
// We use Update.end() because not all forms of Update() support an abort.
89+
// If the upload is incomplete, Update.end(false) should error out.
90+
if (Update.end(context->uploadComplete)) {
91+
// Update successful!
92+
#ifndef ESP8266
93+
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
94+
#endif
95+
doReboot = true;
96+
context->needsRestart = false;
97+
}
98+
}
99+
100+
if (context->needsRestart) {
101+
strip.resume();
102+
UsermodManager::onUpdateBegin(false);
103+
#if WLED_WATCHDOG_TIMEOUT > 0
104+
WLED::instance().enableWatchdog();
105+
#endif
106+
}
107+
delete context;
108+
}
109+
};
110+
111+
static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
112+
{
113+
#ifdef ESP8266
114+
Update.runAsync(true);
115+
#endif
116+
117+
if (Update.isRunning()) {
118+
request->send(503);
119+
setOTAReplied(request);
120+
return false;
121+
}
122+
123+
#if WLED_WATCHDOG_TIMEOUT > 0
124+
WLED::instance().disableWatchdog();
125+
#endif
126+
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
127+
128+
strip.suspend();
129+
strip.resetSegments(); // free as much memory as you can
130+
context->needsRestart = true;
131+
backupConfig(); // backup current config in case the update ends badly
132+
133+
DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
134+
135+
auto skipValidationParam = request->getParam("skipValidation", true);
136+
if (skipValidationParam && (skipValidationParam->value() == "1")) {
137+
context->releaseCheckPassed = true;
138+
DEBUG_PRINTLN(F("OTA validation skipped by user"));
139+
}
140+
141+
// Begin update with the firmware size from content length
142+
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
143+
if (!Update.begin(updateSize)) {
144+
context->errorMessage = Update.UPDATE_ERROR();
145+
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str());
146+
return false;
147+
}
148+
149+
context->updateStarted = true;
150+
return true;
151+
}
152+
153+
// Create an OTA context object on an AsyncWebServerRequest
154+
// Returns true if successful, false on failure.
155+
bool initOTA(AsyncWebServerRequest *request) {
156+
// Allocate update context
157+
UpdateContext* context = new (std::nothrow) UpdateContext {};
158+
if (context) {
159+
request->_tempObject = context;
160+
request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure
161+
};
162+
163+
DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
164+
return (context != nullptr);
165+
}
166+
167+
void setOTAReplied(AsyncWebServerRequest *request) {
168+
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
169+
if (!context) return;
170+
context->replySent = true;
171+
};
172+
173+
// Returns pointer to error message, or nullptr if OTA was successful.
174+
std::pair<bool, String> getOTAResult(AsyncWebServerRequest* request) {
175+
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
176+
if (!context) return { true, F("OTA context unexpectedly missing") };
177+
if (context->replySent) return { false, {} };
178+
if (context->errorMessage.length()) return { true, context->errorMessage };
179+
180+
if (context->updateStarted) {
181+
// Release the OTA context now.
182+
endOTA(request);
183+
if (Update.hasError()) {
184+
return { true, Update.UPDATE_ERROR() };
185+
} else {
186+
return { true, {} };
187+
}
188+
}
189+
190+
// Should never happen
191+
return { true, F("Internal software failure") };
192+
}
193+
194+
195+
196+
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal)
197+
{
198+
UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);
199+
if (!context) return;
200+
201+
//DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal);
202+
203+
if (context->replySent || (context->errorMessage.length())) return;
204+
205+
if (index == 0) {
206+
if (!beginOTA(request, context)) return;
207+
}
208+
209+
// Perform validation if we haven't done it yet and we have reached the metadata offset
210+
if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) {
211+
// Current chunk contains the metadata offset
212+
size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;
213+
214+
DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);
215+
216+
if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {
217+
// We have enough data to validate, one way or another
218+
const uint8_t* search_data = data;
219+
size_t search_len = len;
220+
221+
// If we have saved data, use that instead
222+
if (context->releaseMetadataBuffer.size()) {
223+
// Add this data
224+
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
225+
search_data = context->releaseMetadataBuffer.data();
226+
search_len = context->releaseMetadataBuffer.size();
227+
}
228+
229+
// Do the checking
230+
char errorMessage[128];
231+
bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));
232+
233+
// Release buffer if there was one
234+
context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};
235+
236+
if (!OTA_ok) {
237+
DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage);
238+
context->errorMessage = errorMessage;
239+
context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway.");
240+
return;
241+
} else {
242+
DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed"));
243+
context->releaseCheckPassed = true;
244+
}
245+
} else {
246+
// Store the data we just got for next pass
247+
context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);
248+
}
249+
}
250+
251+
// Check if validation was still pending (shouldn't happen normally)
252+
// This is done before writing the last chunk, so endOTA can abort
253+
if (isFinal && !context->releaseCheckPassed) {
254+
DEBUG_PRINTLN(F("OTA failed: Validation never completed"));
255+
// Don't write the last chunk to the updater: this will trip an error later
256+
context->errorMessage = F("Release check data never arrived?");
257+
return;
258+
}
259+
260+
// Write chunk data to OTA update (only if release check passed or still pending)
261+
if (!Update.hasError()) {
262+
if (Update.write(data, len) != len) {
263+
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR());
264+
}
265+
}
266+
267+
if(isFinal) {
268+
DEBUG_PRINTLN(F("OTA Update End"));
269+
// Upload complete
270+
context->uploadComplete = true;
271+
}
272+
}
273+
274+
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
275+
static String bootloaderSHA256HexCache = "";
276+
277+
// Calculate and cache the bootloader SHA256 digest as hex string
278+
void calculateBootloaderSHA256() {
279+
if (!bootloaderSHA256HexCache.isEmpty()) return;
280+
281+
// Calculate SHA256
282+
uint8_t sha256[32];
283+
mbedtls_sha256_context ctx;
284+
mbedtls_sha256_init(&ctx);
285+
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
286+
287+
const size_t chunkSize = 256;
288+
uint8_t buffer[chunkSize];
289+
290+
for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) {
291+
size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize);
292+
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
293+
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { // use esp_flash_read for V4 framework (-S2, -S3, -C3)
294+
#else
295+
if (spi_flash_read(BOOTLOADER_OFFSET + offset, buffer, readSize) == ESP_OK) { // use spi_flash_read for old V3 framework (legacy esp32)
296+
#endif
297+
mbedtls_sha256_update(&ctx, buffer, readSize);
298+
}
299+
}
300+
301+
mbedtls_sha256_finish(&ctx, sha256);
302+
mbedtls_sha256_free(&ctx);
303+
304+
// Convert to hex string and cache it
305+
char hex[65];
306+
for (int i = 0; i < 32; i++) {
307+
sprintf(hex + (i * 2), "%02x", sha256[i]);
308+
}
309+
hex[64] = '\0';
310+
bootloaderSHA256HexCache = hex;
311+
}
312+
313+
// Get bootloader SHA256 as hex string
314+
String getBootloaderSHA256Hex() {
315+
calculateBootloaderSHA256();
316+
return bootloaderSHA256HexCache;
317+
}
318+
319+
// Invalidate cached bootloader SHA256 (call after bootloader update)
320+
void invalidateBootloaderSHA256Cache() {
321+
bootloaderSHA256HexCache = "";
322+
}
323+
#endif

0 commit comments

Comments
 (0)