diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 783a609ece..8c360c7809 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -19,12 +19,26 @@ } function GetV() {/*injected values here*/} + - +

WLED Software Update

Installed version: WLED ##VERSION##
@@ -37,6 +51,16 @@

WLED Software Update


+
Updating...
Please do not close or refresh the page :)
\ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 1d81655d6d..ecd65b7018 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -545,6 +545,9 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& h void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error); void serveSettings(AsyncWebServerRequest* request, bool post = false); void serveSettingsJS(AsyncWebServerRequest* request); +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +String getBootloaderSHA256Hex(); +#endif //ws.cpp void handleWs(); diff --git a/wled00/json.cpp b/wled00/json.cpp index d2b771c590..b2f1072975 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -817,6 +817,9 @@ void serializeInfo(JsonObject root) root[F("resetReason1")] = (int)rtc_get_reset_reason(1); #endif root[F("lwip")] = 0; //deprecated + #ifndef WLED_DISABLE_OTA + root[F("bootloaderSHA256")] = getBootloaderSHA256Hex(); + #endif #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 75b4ae3f5a..5fa773b83d 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -18,6 +18,14 @@ #endif #include "html_cpal.h" +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + #include + #include + #include + #include + #include +#endif + // define flash strings once (saves flash memory) static const char s_redirecting[] PROGMEM = "Redirecting..."; static const char s_content_enc[] PROGMEM = "Content-Encoding"; @@ -28,6 +36,12 @@ static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; static const char _common_js[] PROGMEM = "/common.js"; +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +// Cache for bootloader SHA256 digest +static uint8_t bootloaderSHA256[32]; +static bool bootloaderSHA256Cached = false; +#endif + //Is this an IP? static bool isIp(const String &str) { for (size_t i = 0; i < str.length(); i++) { @@ -176,6 +190,61 @@ static String msgProcessor(const String& var) return String(); } +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +// Calculate and cache the bootloader SHA256 digest +static void calculateBootloaderSHA256() { + if (bootloaderSHA256Cached) return; + + // Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB + const uint32_t bootloaderOffset = 0x1000; + const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size + + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) + + const size_t chunkSize = 256; + uint8_t buffer[chunkSize]; + + for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { + size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); + if (esp_flash_read(NULL, buffer, bootloaderOffset + offset, readSize) == ESP_OK) { + mbedtls_sha256_update(&ctx, buffer, readSize); + } + } + + mbedtls_sha256_finish(&ctx, bootloaderSHA256); + mbedtls_sha256_free(&ctx); + bootloaderSHA256Cached = true; +} + +// Get bootloader SHA256 as hex string +String getBootloaderSHA256Hex() { + calculateBootloaderSHA256(); + + char hex[65]; + for (int i = 0; i < 32; i++) { + sprintf(hex + (i * 2), "%02x", bootloaderSHA256[i]); + } + hex[64] = '\0'; + return String(hex); +} + +// Verify if uploaded data is a valid ESP32 bootloader +static bool isValidBootloader(const uint8_t* data, size_t len) { + if (len < 32) return false; + + // Check for ESP32 bootloader magic byte (0xE9) + if (data[0] != 0xE9) return false; + + // Additional validation: check segment count is reasonable + uint8_t segmentCount = data[1]; + if (segmentCount > 16) return false; + + return true; +} +#endif + static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) { if (!correctPIN) { if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); @@ -466,6 +535,106 @@ void initServer() server.on(_update, HTTP_POST, notSupported, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){}); #endif +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + // ESP32 bootloader update endpoint + server.on(F("/updatebootloader"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveSettings(request, true); // handle PIN page POST request + return; + } + if (otaLock) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254); + return; + } + if (Update.hasError()) { + serveMessage(request, 500, F("Bootloader update failed!"), F("Please check your file and retry!"), 254); + } else { + serveMessage(request, 200, F("Bootloader updated successfully!"), FPSTR(s_rebooting), 131); + doReboot = true; + } + },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){ + IPAddress client = request->client()->remoteIP(); + if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) { + DEBUG_PRINTLN(F("Attempted bootloader update from different/non-local subnet!")); + request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); + return; + } + if (!correctPIN || otaLock) return; + + static size_t bootloaderBytesWritten = 0; + static bool bootloaderErased = false; + const uint32_t bootloaderOffset = 0x1000; + const uint32_t maxBootloaderSize = 0x8000; // 32KB max + + if (!index) { + DEBUG_PRINTLN(F("Bootloader Update Start")); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().disableWatchdog(); + #endif + lastEditTime = millis(); // make sure PIN does not lock during update + strip.suspend(); + strip.resetSegments(); + bootloaderBytesWritten = 0; + bootloaderErased = false; + + // Verify bootloader magic on first chunk + if (!isValidBootloader(data, len)) { + DEBUG_PRINTLN(F("Invalid bootloader file!")); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + Update.abort(); + return; + } + + // Erase bootloader region (32KB) + DEBUG_PRINTLN(F("Erasing bootloader region...")); + esp_err_t err = esp_flash_erase_region(NULL, bootloaderOffset, maxBootloaderSize); + if (err != ESP_OK) { + DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + Update.abort(); + return; + } + bootloaderErased = true; + } + + // Write data to flash at bootloader offset + if (bootloaderErased && bootloaderBytesWritten + len <= maxBootloaderSize) { + esp_err_t err = esp_flash_write(NULL, data, bootloaderOffset + bootloaderBytesWritten, len); + if (err != ESP_OK) { + DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err); + Update.abort(); + } else { + bootloaderBytesWritten += len; + } + } else if (!bootloaderErased) { + DEBUG_PRINTLN(F("Bootloader region not erased!")); + Update.abort(); + } else { + DEBUG_PRINTLN(F("Bootloader size exceeds maximum!")); + Update.abort(); + } + + if (isFinal) { + if (!Update.hasError() && bootloaderBytesWritten > 0) { + DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written\n"), bootloaderBytesWritten); + bootloaderSHA256Cached = false; // Invalidate cached bootloader hash + } else { + DEBUG_PRINTLN(F("Bootloader Update Failed")); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + } + } + }); +#endif + #ifdef WLED_ENABLE_DMX server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor);