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