diff --git a/.github/templates/firmware.test-suite.json b/.github/templates/firmware.test-suite.json index 35554c9..41ea691 100644 --- a/.github/templates/firmware.test-suite.json +++ b/.github/templates/firmware.test-suite.json @@ -1,9 +1,23 @@ [{ "type": "FIRMWARE_TYPE", - "version": 1, + "version": "1.1", "host": "FIRMWARE_HOST", "port": FIRMWARE_PORT, - "bin": "FIRMWARE_PATH/1.nosecurity.ino.bin?raw=true" + "bin": "FIRMWARE_PATH/1.1.nosecurity.ino.bin?raw=true" +}, +{ + "type": "FIRMWARE_TYPE", + "version": "1.2", + "host": "FIRMWARE_HOST", + "port": FIRMWARE_PORT, + "bin": "FIRMWARE_PATH/1.2.nosecurity.gz.ino.bin.gz?raw=true" +}, +{ + "type": "FIRMWARE_TYPE", + "version": "1.3", + "host": "FIRMWARE_HOST", + "port": FIRMWARE_PORT, + "bin": "FIRMWARE_PATH/1.3.nosecurity.zz.ino.bin.zz?raw=true" }, { "type": "FIRMWARE_TYPE", diff --git a/.github/workflows/gen-test-suite.yml b/.github/workflows/gen-test-suite.yml index b8ac852..8d3fad0 100644 --- a/.github/workflows/gen-test-suite.yml +++ b/.github/workflows/gen-test-suite.yml @@ -70,6 +70,18 @@ on: - large_spiffs default: 'default' + debug_level: + description: 'Core Debug Level' + required: true + type: choice + options: + - none + - error + - warning + - info + - debug + - verbose + default: 'none' jobs: @@ -172,7 +184,9 @@ jobs: matrix: sketch: - - 1.nosecurity.ino + - 1.1.nosecurity.ino + - 1.2.nosecurity.gz.ino + - 1.3.nosecurity.zz.ino - 2.cert-in-spiffs.ino - 3.cert-in-progmem.ino - 4.cert-in-littlefs.ino @@ -182,7 +196,7 @@ jobs: steps: - - name: Checkout + - name: Checkout Current uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.head.sha }} @@ -208,18 +222,33 @@ jobs: cp ${{env.cert_path}}/root_ca.h $root_ca_c_path cp ${{env.cert_path}}/pub_key.h $pub_key_c_path + - name: Checkout esp32-flashz + uses: actions/checkout@v2 + with: + repository: vortigont/esp32-flashz + ref: main + path: CustomflashZ # must contain string "Custom" + + - name: Checkout ESP32-targz + uses: actions/checkout@v2 + with: + repository: tobozo/ESP32-targz + ref: master + path: CustomESP32-targz # must contain string "Custom" + + - name: Compile ${{ matrix.sketch }} uses: ArminJo/arduino-test-compile@v3 with: platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json - arduino-board-fqbn: ${{inputs.board_fqbn}}:PartitionScheme=${{inputs.partition_scheme}} + arduino-board-fqbn: ${{inputs.board_fqbn}}:PartitionScheme=${{inputs.partition_scheme}},DebugLevel=${{inputs.debug_level}} required-libraries: ArduinoJson extra-arduino-lib-install-args: --no-deps extra-arduino-cli-args: "--warnings default " # see https://github.com/ArminJo/arduino-test-compile/issues/28 sketch-names: ${{ matrix.sketch }} set-build-path: true - - name: Sign and Save compiled binary + - name: Sign and Save binaries run: | mkdir -p ${{env.artifact_path}} full_ino_bin_path=`find ${{env.work_path}}/ | grep "build/${{ matrix.sketch }}.bin"` @@ -227,6 +256,8 @@ jobs: cat firmware.sign $full_ino_bin_path > $full_ino_bin_path.img cp $full_ino_bin_path ${{env.artifact_path}}/${{ matrix.sketch }}.bin cp $full_ino_bin_path.img ${{env.artifact_path}}/${{ matrix.sketch }}.signed.bin + gzip -c $full_ino_bin_path > ${{env.artifact_path}}/${{ matrix.sketch }}.bin.gz + pigz -9kzc $full_ino_bin_path > ${{env.artifact_path}}/${{ matrix.sketch }}.bin.zz - name: Prepare data folder @@ -252,6 +283,12 @@ jobs: # Create the partition binaries ${{env.mkspiffs_esp32}} -c ${{env.data_path}} -p 256 -b 4096 -s $${{inputs.partition_scheme}}_size ${{env.spiffs_bin_path}} ${{env.mklittlefs_esp32}} -c ${{env.data_path}} -p 256 -b 4096 -s $${{inputs.partition_scheme}}_size ${{env.littlefs_bin_path}} + # Create gzipped versions + gzip -c ${{env.littlefs_bin_path}} > ${{env.littlefs_bin_path}}.gz + gzip -c ${{env.spiffs_bin_path}} > ${{env.spiffs_bin_path}}.gz + # Create flashZ versions + pigz -9kzc ${{env.littlefs_bin_path}} > ${{env.littlefs_bin_path}}.zz + pigz -9kzc ${{env.spiffs_bin_path}} > ${{env.spiffs_bin_path}}.zz # Sign partition binaries openssl dgst -sign ${{env.privkey_path}} -keyform PEM -sha256 -out firmware.sign -binary ${{env.spiffs_bin_path}} diff --git a/.gitignore b/.gitignore index 2cec19e..151434f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ _Notes .pio test/stage1 +.directory diff --git a/README.md b/README.md index d529cfa..5bb3bad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ [![PlatformIO](https://github.com/chrisjoyce911/esp32FOTA/workflows/PlatformIO/badge.svg)](https://github.com/chrisjoyce911/esp32FOTA/actions/) +[![arduino-library-badge](https://www.ardu-badge.com/badge/esp32FOTA.svg?)](https://www.ardu-badge.com/esp32FOTA) +[![PlatformIO Registry](https://badges.registry.platformio.org/packages/chrisjoyce911/library/esp32FOTA.svg)](https://registry.platformio.org/libraries/chrisjoyce911/esp32FOTA) + + + # esp32FOTA library for Arduino ## Purpose @@ -90,12 +95,15 @@ A single JSON file can also contain several versions of a single firmware type: { "type":"esp32-fota-http", "version":"0.0.3", - "url":"http://192.168.0.100/fota/esp32-fota-0.0.3.bin" + "url":"http://192.168.0.100/fota/esp32-fota-0.0.3.bin", + "spiffs":"http://192.168.0.100/fota/esp32-fota-0.0.3.spiffs.bin" } ] ``` + + #### Filesystem image (spiffs/littlefs) Adding `spiffs` key to the JSON entry will end up with the filesystem being updated first, then the firmware. @@ -168,10 +176,12 @@ void setup_wifi() void loop() { - bool updatedNeeded = esp32FOTA.execHTTPcheck(); - if (updatedNeeded) { - esp32FOTA.execOTA(); - } + esp32FOTA.handle(); + // or ... + // bool updatedNeeded = esp32FOTA.execHTTPcheck(); + // if (updatedNeeded) { + // esp32FOTA.execOTA(); + // } delay(2000); } ``` @@ -209,16 +219,67 @@ void setup() void loop() { - bool updatedNeeded = esp32FOTA.execHTTPcheck(); - if (updatedNeeded) { - esp32FOTA.execOTA(); - } + esp32FOTA.handle(); + // or ... + // bool updatedNeeded = esp32FOTA.execHTTPcheck(); + // if (updatedNeeded) { + // esp32FOTA.execOTA(); + // } delay(2000); } ``` +### Zlib/gzip support + +⚠️ This feature cannot be used with signature check. + + +For firmwares compressed with `pigz` utility (see , file extension must be `.zz`: + +```cpp +#include // http://github.com/vortigont/esp32-flashz +#include +``` + +```bash +$ pigz -9kzc esp32-fota-http-2.bin > esp32-fota-http-2.bin.zz +``` + +```json +{ + "type": "esp32-fota-http", + "version": "2.5.1", + "url": "http://192.168.0.100/fota/esp32-fota-http-2.bin.zz" +} +``` + + +For firmwares compressed with `gzip` utility, file extension must be `.gz` + +```cpp +#include // http://github.com/tobozo/ESP32-targz +#include +``` + +```bash +$ gzip -c esp32-fota-http-2.bin > esp32-fota-http-2.bin.gz +``` + +```json +{ + "type": "esp32-fota-http", + "version": "2.5.1", + "url": "http://192.168.0.100/fota/esp32-fota-http-2.bin.gz" +} +``` + + + + + + ### Root Certificates Certificates and signatures can be stored in different places: any fs::FS filesystem or progmem as const char*. @@ -428,11 +489,14 @@ On the next update-check the ESP32 will download the `firmware.img` extract the [#92]: https://github.com/chrisjoyce911/esp32FOTA/pull/92 +### Libraries +This library relies on [semver.c by h2non](https://github.com/h2non/semver.c) for semantic versioning support. semver.c is licensed under [MIT](https://github.com/h2non/semver.c/blob/master/LICENSE). -### Libraries +Optional dependencies (zlib/gzip support): +* [esp32-flashz](https://github.com/vortigont/esp32-flashz) +* [esp32-targz](https://github.com/tobozo/ESP32-targz) -This relies on [semver.c by h2non](https://github.com/h2non/semver.c) for semantic versioning support. semver.c is licensed under [MIT](https://github.com/h2non/semver.c/blob/master/LICENSE). ### Thanks to @@ -441,4 +505,5 @@ This relies on [semver.c by h2non](https://github.com/h2non/semver.c) for semant * @tuan-karma * @hpsaturn * @tobozo +* @vortigont diff --git a/examples/anyFS/anyFS.ino b/examples/anyFS/anyFS.ino index c38fee5..ef0c051 100644 --- a/examples/anyFS/anyFS.ino +++ b/examples/anyFS/anyFS.ino @@ -13,9 +13,12 @@ #include //#include +//#include // optional esp32-flashz for zlib compressed firmwares +//#include // optional ESP32-targz for gzip compressed firmwares #include // fota pulls WiFi library + // esp32fota settings const int firmware_version = 1; #if !defined FOTA_URL @@ -36,11 +39,14 @@ CryptoFileAsset *MyRSAKey = new CryptoFileAsset( "/rsa_key.pub", &LittleFS ); // CryptoMemAsset *MyRSAKey = new CryptoMemAsset("RSA Public Key", rsa_key_pub, strlen(rsa_key_pub)+1 ); -esp32FOTA FOTA; -//esp32FOTA esp32FOTA( String(firmware_name), firmware_version, check_signature, disable_security ); +esp32FOTA FOTA; // empty constructor + +bool WiFiConnected() +{ + return (WiFi.status() == WL_CONNECTED); +} -//esp32FOTA esp32FOTA("esp32-fota-http", 1, false ); void setup_wifi() { @@ -50,7 +56,7 @@ void setup_wifi() WiFi.begin(); // no WiFi creds in this demo :-) - while (WiFi.status() != WL_CONNECTED) + while ( !WiFiConnected() ) { delay(500); Serial.print("."); @@ -86,6 +92,10 @@ void setup() FOTA.setConfig( cfg ); } + + // FOTA.setStatusChecker( WiFiConnected ); + + // /!\ FOTA.checkURL is deprecated, use setManifestURL( String ) instead //FOTA.setManifestURL( FOTA_URL ); //FOTA.setRootCA( MyRootCA ); diff --git a/examples/anyFS/test/1.1.nosecurity/1.1.nosecurity.ino b/examples/anyFS/test/1.1.nosecurity/1.1.nosecurity.ino new file mode 100644 index 0000000..d5d095f --- /dev/null +++ b/examples/anyFS/test/1.1.nosecurity/1.1.nosecurity.ino @@ -0,0 +1,110 @@ +/** + esp32 firmware OTA + + Purpose: Perform an OTA update to both firmware and filesystem from binaries located + on a webserver (HTTPS) without checking for certificate validity + + Usage: If the ESP32 had a previous successful WiFi connection, then no need to set the ssid/password + to run this sketch, the credentials are still cached :-) + Sketch 1 will FOTA to Sketch 2, then Sketch 3, and so on until all versions in firmware.json are + exhausted. + + +*/ + +#include // optional ESP32-targz for gzip compressed firmwares +#include + +// esp32fota settings +int firmware_version_major = 1; +int firmware_version_minor = 1; +int firmware_version_patch = 0; + +#if !defined FOTA_URL + #define FOTA_URL "http://server/fota/fota.json" +#endif +const char* firmware_name = "esp32-fota-http"; +const bool check_signature = false; +const bool disable_security = true; +// for debug only +const char* description = "Basic example with no security and no filesystem"; + +const char* fota_debug_fmt = R"DBG_FMT( + +***************** STAGE %s ***************** + + Description : %s + Firmware type : %s + Firmware version : %i.%i.%i + Signature check : %s + TLS Cert check : %s + Compression : %s + +******************************************** + +)DBG_FMT"; + + +// esp32fota esp32fota("", , , ); +// esp32FOTA esp32FOTA( String(firmware_name), firmware_version, check_signature, disable_security ); + + +esp32FOTA FOTA; + + +void setup_wifi() +{ + delay(10); + + Serial.print("MAC Address "); + Serial.println( WiFi.macAddress() ); + + WiFi.begin(); // no WiFi creds in this demo :-) + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println(WiFi.localIP()); +} + + +void setup() +{ + Serial.begin(115200); + + Serial.printf( fota_debug_fmt, + "1.1", + description, + firmware_name, + firmware_version_major, + firmware_version_minor, + firmware_version_patch, + check_signature ?"Enabled":"Disabled", + disable_security ?"Disabled":"Enabled", + FOTA.zlibSupported() ?"Enabled":"Disabled" + ); + + { + auto cfg = FOTA.getConfig(); + cfg.name = firmware_name; + cfg.manifest_url = FOTA_URL; + cfg.sem = SemverClass( firmware_version_major, firmware_version_minor, firmware_version_patch ); + cfg.check_sig = check_signature; + cfg.unsafe = disable_security; + //cfg.root_ca = MyRootCA; + //cfg.pub_key = MyRSAKey; + FOTA.setConfig( cfg ); + } + + setup_wifi(); +} + +void loop() +{ + FOTA.handle(); + delay(20000); +} diff --git a/examples/anyFS/test/1.2.nosecurity.gz/1.2.nosecurity.gz.ino b/examples/anyFS/test/1.2.nosecurity.gz/1.2.nosecurity.gz.ino new file mode 100644 index 0000000..97e02e4 --- /dev/null +++ b/examples/anyFS/test/1.2.nosecurity.gz/1.2.nosecurity.gz.ino @@ -0,0 +1,115 @@ +/** + esp32 firmware OTA + + Purpose: Perform an OTA update to both firmware and filesystem from binaries located + on a webserver (HTTPS) without checking for certificate validity + + Usage: If the ESP32 had a previous successful WiFi connection, then no need to set the ssid/password + to run this sketch, the credentials are still cached :-) + Sketch 1 will FOTA to Sketch 2, then Sketch 3, and so on until all versions in firmware.json are + exhausted. + + +*/ + +#include // optional esp32-flashz for gzipped firmwares +#include + +// esp32fota settings +int firmware_version_major = 1; +int firmware_version_minor = 2; +int firmware_version_patch = 0; + +#if !defined FOTA_URL + #define FOTA_URL "http://server/fota/fota.json" +#endif +const char* firmware_name = "esp32-fota-http"; +const bool check_signature = false; +const bool disable_security = true; +// for debug only +const char* description = "Basic *gzipped* example with no security and no filesystem"; + +const char* fota_debug_fmt = R"DBG_FMT( + +***************** STAGE %s ***************** + + Description : %s + Firmware type : %s + Firmware version : %i.%i.%i + Signature check : %s + TLS Cert check : %s + Compression : %s + +******************************************** + +)DBG_FMT"; + + +// esp32fota esp32fota("", , , ); +// esp32FOTA esp32FOTA( String(firmware_name), firmware_version, check_signature, disable_security ); + + +esp32FOTA FOTA; + + +void setup_wifi() +{ + delay(10); + + Serial.print("MAC Address "); + Serial.println( WiFi.macAddress() ); + + WiFi.begin(); // no WiFi creds in this demo :-) + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println(WiFi.localIP()); +} + + +void setup() +{ + Serial.begin(115200); + + Serial.printf( fota_debug_fmt, + "1.2", + description, + firmware_name, + firmware_version_major, + firmware_version_minor, + firmware_version_patch, + check_signature ?"Enabled":"Disabled", + disable_security ?"Disabled":"Enabled", + FOTA.zlibSupported() ?"Enabled":"Disabled" + ); + + { + auto cfg = FOTA.getConfig(); + cfg.name = firmware_name; + cfg.manifest_url = FOTA_URL; + cfg.sem = SemverClass( firmware_version_major, firmware_version_minor, firmware_version_patch ); + cfg.check_sig = check_signature; + cfg.unsafe = disable_security; + //cfg.root_ca = MyRootCA; + //cfg.pub_key = MyRSAKey; + FOTA.setConfig( cfg ); + } + + setup_wifi(); +} + +void loop() +{ + bool updatedNeeded = FOTA.execHTTPcheck(); + if (updatedNeeded) { + FOTA.execOTA(); + } + + delay(20000); +} + diff --git a/examples/anyFS/test/1.nosecurity/1.nosecurity.ino b/examples/anyFS/test/1.3.nosecurity.zz/1.3.nosecurity.zz.ino similarity index 80% rename from examples/anyFS/test/1.nosecurity/1.nosecurity.ino rename to examples/anyFS/test/1.3.nosecurity.zz/1.3.nosecurity.zz.ino index 2a302dc..994722f 100644 --- a/examples/anyFS/test/1.nosecurity/1.nosecurity.ino +++ b/examples/anyFS/test/1.3.nosecurity.zz/1.3.nosecurity.zz.ino @@ -16,7 +16,7 @@ // esp32fota settings int firmware_version_major = 1; -int firmware_version_minor = 0; +int firmware_version_minor = 3; int firmware_version_patch = 0; #if !defined FOTA_URL @@ -26,17 +26,18 @@ const char* firmware_name = "esp32-fota-http"; const bool check_signature = false; const bool disable_security = true; // for debug only -const char* description = "Basic example with no security and no filesystem"; +const char* description = "Basic *gzipped* example with no security and no filesystem"; const char* fota_debug_fmt = R"DBG_FMT( -***************** STAGE %i ***************** +***************** STAGE %s ***************** Description : %s Firmware type : %s - Firmware version : %i + Firmware version : %i.%i.%i Signature check : %s TLS Cert check : %s + Compression : %s ******************************************** @@ -73,7 +74,18 @@ void setup_wifi() void setup() { Serial.begin(115200); - Serial.printf( fota_debug_fmt, firmware_version_major, description, firmware_name, firmware_version_major, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + + Serial.printf( fota_debug_fmt, + "1.3", + description, + firmware_name, + firmware_version_major, + firmware_version_minor, + firmware_version_patch, + check_signature ?"Enabled":"Disabled", + disable_security ?"Disabled":"Enabled", + FOTA.zlibSupported() ?"Enabled":"Disabled" + ); { auto cfg = FOTA.getConfig(); @@ -99,3 +111,4 @@ void loop() delay(20000); } + diff --git a/examples/anyFS/test/2.cert-in-spiffs/2.cert-in-spiffs.ino b/examples/anyFS/test/2.cert-in-spiffs/2.cert-in-spiffs.ino index b8e8c4e..370268e 100644 --- a/examples/anyFS/test/2.cert-in-spiffs/2.cert-in-spiffs.ino +++ b/examples/anyFS/test/2.cert-in-spiffs/2.cert-in-spiffs.ino @@ -25,13 +25,14 @@ const char* description = "SPIFFS example with security"; const char* fota_debug_fmt = R"DBG_FMT( -***************** STAGE %i ***************** +***************** STAGE %s ***************** Description : %s Firmware type : %s - Firmware version : %i + Firmware version : %i.%i.%i Signature check : %s TLS Cert check : %s + Compression : %s ******************************************** @@ -74,7 +75,18 @@ void setup_wifi() void setup() { Serial.begin(115200); - Serial.printf( fota_debug_fmt, firmware_version_major, description, firmware_name, firmware_version_major, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + + Serial.printf( fota_debug_fmt, + "2", + description, + firmware_name, + firmware_version_major, + firmware_version_minor, + firmware_version_patch, + check_signature ?"Enabled":"Disabled", + disable_security ?"Disabled":"Enabled", + FOTA.zlibSupported() ?"Enabled":"Disabled" + ); // Provide filesystem with root_ca.pem to validate server certificate if( ! SPIFFS.begin( false ) ) { diff --git a/examples/anyFS/test/3.cert-in-progmem/3.cert-in-progmem.ino b/examples/anyFS/test/3.cert-in-progmem/3.cert-in-progmem.ino index a07f35c..3c94353 100644 --- a/examples/anyFS/test/3.cert-in-progmem/3.cert-in-progmem.ino +++ b/examples/anyFS/test/3.cert-in-progmem/3.cert-in-progmem.ino @@ -27,13 +27,14 @@ const char* description = "PROGMEM example with security"; const char* fota_debug_fmt = R"DBG_FMT( -***************** STAGE %i ***************** +***************** STAGE %s ***************** Description : %s Firmware type : %s - Firmware version : %i + Firmware version : %i.%i.%i Signature check : %s TLS Cert check : %s + Compression : %s ******************************************** @@ -78,7 +79,18 @@ void setup_wifi() void setup() { Serial.begin(115200); - Serial.printf( fota_debug_fmt, firmware_version_major, description, firmware_name, firmware_version_major, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + + Serial.printf( fota_debug_fmt, + "3", + description, + firmware_name, + firmware_version_major, + firmware_version_minor, + firmware_version_patch, + check_signature ?"Enabled":"Disabled", + disable_security ?"Disabled":"Enabled", + FOTA.zlibSupported() ?"Enabled":"Disabled" + ); { auto cfg = FOTA.getConfig(); diff --git a/examples/anyFS/test/4.cert-in-littlefs/4.cert-in-littlefs.ino b/examples/anyFS/test/4.cert-in-littlefs/4.cert-in-littlefs.ino index 52f26a2..a75cac0 100644 --- a/examples/anyFS/test/4.cert-in-littlefs/4.cert-in-littlefs.ino +++ b/examples/anyFS/test/4.cert-in-littlefs/4.cert-in-littlefs.ino @@ -25,13 +25,14 @@ const char* description = "LittleFS example with enforced security"; const char* fota_debug_fmt = R"DBG_FMT( -***************** STAGE %i ***************** +***************** STAGE %s ***************** Description : %s Firmware type : %s - Firmware version : %i + Firmware version : %i.%i.%i Signature check : %s TLS Cert check : %s + Compression : %s ******************************************** @@ -75,7 +76,18 @@ void setup_wifi() void setup() { Serial.begin(115200); - Serial.printf( fota_debug_fmt, firmware_version_major, description, firmware_name, firmware_version_major, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + + Serial.printf( fota_debug_fmt, + "4", + description, + firmware_name, + firmware_version_major, + firmware_version_minor, + firmware_version_patch, + check_signature ?"Enabled":"Disabled", + disable_security ?"Disabled":"Enabled", + FOTA.zlibSupported() ?"Enabled":"Disabled" + ); // Provide filesystem with root_ca.pem to validate server certificate if( ! LittleFS.begin( false ) ) { Serial.println("LittleFS Mounting failed, aborting!"); diff --git a/examples/anyFS/test/5.sig-in-progmem/5.sig-in-progmem.ino b/examples/anyFS/test/5.sig-in-progmem/5.sig-in-progmem.ino index 379e171..a6f2c33 100644 --- a/examples/anyFS/test/5.sig-in-progmem/5.sig-in-progmem.ino +++ b/examples/anyFS/test/5.sig-in-progmem/5.sig-in-progmem.ino @@ -28,13 +28,14 @@ const char* description = "PROGMEM example with enforced security"; const char* fota_debug_fmt = R"DBG_FMT( -***************** STAGE %i ***************** +***************** STAGE %s ***************** Description : %s Firmware type : %s - Firmware version : %i + Firmware version : %i.%i.%i Signature check : %s TLS Cert check : %s + Compression : %s ******************************************** @@ -76,7 +77,18 @@ void setup_wifi() void setup() { Serial.begin(115200); - Serial.printf( fota_debug_fmt, firmware_version_major, description, firmware_name, firmware_version_major, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + + Serial.printf( fota_debug_fmt, + "5", + description, + firmware_name, + firmware_version_major, + firmware_version_minor, + firmware_version_patch, + check_signature ?"Enabled":"Disabled", + disable_security ?"Disabled":"Enabled", + FOTA.zlibSupported() ?"Enabled":"Disabled" + ); { auto cfg = FOTA.getConfig(); diff --git a/library.json b/library.json index a50da1f..04bf8d0 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "esp32FOTA", - "version": "0.2.2", + "version": "0.2.3", "keywords": "firmware, OTA, Over The Air Updates, ArduinoOTA", "description": "Allows for firmware to be updated from a webserver, the device can check for updates at any time. Uses a simple JSON file to outline if a new firmware is avaiable.", "examples": "examples/*/*.ino", diff --git a/library.properties b/library.properties index 3bb027b..dc1807c 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=esp32FOTA -version=0.2.2 +version=0.2.3 author=Chris Joyce maintainer=Chris Joyce sentence=A simple library for firmware OTA updates diff --git a/platformio.ini b/platformio.ini index d8aa752..1d1380a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -17,6 +17,8 @@ build_flags = lib_deps = bblanchon/ArduinoJson @ ^6 esp32FOTA +; vortigont/esp32-flashz +; tobozo/ESP32-targz [esp32_common] platform = espressif32 diff --git a/src/esp32FOTA.cpp b/src/esp32FOTA.cpp index ce8b9d1..1856540 100644 --- a/src/esp32FOTA.cpp +++ b/src/esp32FOTA.cpp @@ -41,6 +41,14 @@ #pragma GCC diagnostic ignored "-Wmissing-field-initializers" #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#define FW_SIGNATURE_LENGTH 512 + + +static int64_t getHTTPStream( esp32FOTA* fota, int partition ); +static int64_t getFileStream( esp32FOTA* fota, int partition ); +static int64_t getSerialStream( esp32FOTA* fota, int partition ); +static bool WiFiStatusCheck(); + SemverClass::SemverClass( const char* version ) { @@ -106,7 +114,9 @@ size_t CryptoFileAsset::size() -esp32FOTA::esp32FOTA() { } +esp32FOTA::esp32FOTA(){} +esp32FOTA::~esp32FOTA(){} + esp32FOTA::esp32FOTA( FOTAConfig_t cfg ) { @@ -114,9 +124,9 @@ esp32FOTA::esp32FOTA( FOTAConfig_t cfg ) } -esp32FOTA::esp32FOTA(String firmwareType, int firmwareVersion, bool validate, bool allow_insecure_https) +esp32FOTA::esp32FOTA(const char* firmwareType, int firmwareVersion, bool validate, bool allow_insecure_https) { - _cfg.name = firmwareType.c_str(); + _cfg.name = firmwareType; _cfg.sem = SemverClass( firmwareVersion ); _cfg.check_sig = validate; _cfg.unsafe = allow_insecure_https; @@ -126,21 +136,18 @@ esp32FOTA::esp32FOTA(String firmwareType, int firmwareVersion, bool validate, bo } -esp32FOTA::esp32FOTA(String firmwareType, String firmwareSemanticVersion, bool validate, bool allow_insecure_https) +esp32FOTA::esp32FOTA(const char* firmwareType, const char* firmwareSemanticVersion, bool validate, bool allow_insecure_https) { - _cfg.name = firmwareType.c_str(); + _cfg.name = firmwareType; _cfg.check_sig = validate; _cfg.unsafe = allow_insecure_https; - _cfg.sem = SemverClass( firmwareSemanticVersion.c_str() ); + _cfg.sem = SemverClass( firmwareSemanticVersion ); setupCryptoAssets(); debugSemVer("Current firmware version", _cfg.sem.ver() ); } -esp32FOTA::~esp32FOTA() -{ -} void esp32FOTA::setCertFileSystem( fs::FS *cert_filesystem ) @@ -161,6 +168,15 @@ void esp32FOTA::setupCryptoAssets() } + +void esp32FOTA::handle() +{ + if ( execHTTPcheck() ) { + execOTA(); + } +} + + // SHA-Verify the OTA partition after it's been written // https://techtutorialsx.com/2018/05/10/esp32-arduino-mbed-tls-using-the-sha-256-algorithm/ // https://github.com/ARMmbed/mbedtls/blob/development/programs/pkey/rsa_verify.c @@ -258,7 +274,7 @@ bool esp32FOTA::validate_sig( const esp_partition_t* partition, unsigned char *s } mbedtls_md_finish( &rsa, hash ); - ret = mbedtls_pk_verify( &pk, MBEDTLS_MD_SHA256, hash, mdinfo->size, (unsigned char*)signature, 512 ); + ret = mbedtls_pk_verify( &pk, MBEDTLS_MD_SHA256, hash, mdinfo->size, (unsigned char*)signature, FW_SIGNATURE_LENGTH ); free( hash ); mbedtls_md_free( &rsa ); @@ -276,214 +292,214 @@ bool esp32FOTA::validate_sig( const esp_partition_t* partition, unsigned char *s } -// OTA Logic -bool esp32FOTA::execOTA() -{ - if( _flashFileSystemUrl != "" ) { // a data partition was specified in the json manifest, handle the spiffs partition first - if( _fs ) { // Possible risk of overwriting certs and signatures, cancel flashing! - Serial.println("Cowardly refusing to overwrite U_SPIFFS with "+_flashFileSystemUrl+". Use setCertFileSystem(nullptr) along with setPubKey()/setCAPem() to enable this feature."); - return false; - } else { - log_i("Will check if U_SPIFFS needs updating"); - if( !execOTA( U_SPIFFS, false ) ) return false; - } - } else { - log_i("This update is for U_FLASH only"); - } - // handle the application partition and restart on success - return execOTA( U_FLASH, true ); -} -bool esp32FOTA::execOTA( int partition, bool restart_after ) +bool esp32FOTA::setupHTTP( const char* url ) { - String UpdateURL = ""; - String PartitionLabel = ""; - - switch( partition ) { - case U_SPIFFS: // spiffs/littlefs/fatfs partition - PartitionLabel = "data"; - if( _flashFileSystemUrl == "" ) { - log_i("[SKIP] No spiffs/littlefs/fatfs partition was speficied"); - return true; - } - UpdateURL = _flashFileSystemUrl; - break; - case U_FLASH: // app partition (default) - default: - PartitionLabel = "app" + String( partition ); - partition = U_FLASH; - UpdateURL = _firmwareUrl; - break; - } - - size_t contentLength = 0; - bool isValidContentType = false; const char* rootcastr = nullptr; + _http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - HTTPClient http; - WiFiClientSecure client; - //http.setConnectTimeout( 1000 ); - http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - - log_i("Connecting to: %s\r\n", UpdateURL.c_str() ); - if( UpdateURL.substring( 0, 5 ) == "https" ) { + log_i("Connecting to: %s\r\n", url ); + if( String(url).startsWith("https") ) { if (!_cfg.unsafe) { if( !_cfg.root_ca ) { - Serial.println("A strict security context has been set for "+PartitionLabel+" partition but no RootCA was provided"); + Serial.println("A strict security context has been set but no RootCA was provided"); return false; } rootcastr = _cfg.root_ca->get(); if( _cfg.root_ca->size() == 0 ) { - Serial.println("A strict security context has been set for "+PartitionLabel+" partition but an empty RootCA was provided"); + Serial.println("A strict security context has been set but an empty RootCA was provided"); Serial.println(rootcastr); return false; } if( !rootcastr ) { - Serial.println("Unable to get RootCA for "+PartitionLabel+", aborting"); + Serial.println("Unable to get RootCA, aborting"); return false; } Serial.println("Loading root_ca.pem"); - client.setCACert( rootcastr ); + _client.setCACert( rootcastr ); } else { // We're downloading from a secure URL, but we don't want to validate the root cert. - client.setInsecure(); + _client.setInsecure(); } - http.begin( client, UpdateURL ); + _http.begin( _client, url ); } else { - http.begin( UpdateURL ); + _http.begin( url ); } if( extraHTTPHeaders.size() > 0 ) { - // add custom headers provided by user e.g. http.addHeader("Authorization", "Basic " + auth) + // add custom headers provided by user e.g. _http.addHeader("Authorization", "Basic " + auth) for( const auto &header : extraHTTPHeaders ) { - http.addHeader(header.first, header.second); + _http.addHeader(header.first, header.second); } } // TODO: add more watched headers e.g. Authorization: Signature keyId="rsa-key-1",algorithm="rsa-sha256",signature="Base64(RSA-SHA256(signing string))" - const char* get_headers[] = { "Content-Length", "Content-type" }; - http.collectHeaders( get_headers, 2 ); + const char* get_headers[] = { "Content-Length", "Content-type", "Accept-Ranges" }; + _http.collectHeaders( get_headers, sizeof(get_headers)/sizeof(const char*) ); - int httpCode = http.GET(); + return true; +} - if( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) { - contentLength = http.header( "Content-Length" ).toInt(); - String contentType = http.header( "Content-type" ); - if( contentType == "application/octet-stream" ) { - isValidContentType = true; - } else if( contentType == "application/gzip" ) { - // was gzipped by the server, needs decompression - // TODO: use gzStreamUpdater - } else if( contentType == "application/tar+gz" ) { - // was packaged and compressed, may contain more than one file - // TODO: use tarGzStreamUpdater - } - } else { - switch( httpCode ) { - // 1xx = Hold on - // 2xx = Here you go - // 3xx = Go away - // 4xx = You fucked up - // 5xx = I fucked up - case 204: log_e("Status: 204 (No contents), "); break; - case 401: log_e("Status: 401 (Unauthorized), check setExtraHTTPHeader() values"); break; - case 403: log_e("Status: 403 (Forbidden), check path on webserver?"); break; - case 404: log_e("Status: 404 (Not Found), also a palindrom, check path in manifest?"); break; - case 418: log_e("Status: 418 (I'm a teapot), Brit alert!"); break; - case 429: log_e("Status: 429 (Too many requests), throttle things down?"); break; - case 500: log_e("Status: 500 (Internal Server Error), you broke the webs!"); break; + + +void esp32FOTA::setupStream() +{ + if(!getStream) { + switch( _stream_type ) { + case FOTA_FILE_STREAM: + setStreamGetter( getFileStream ); + break; + case FOTA_SERIAL_STREAM: + setStreamGetter( getSerialStream ); + break; + case FOTA_HTTP_STREAM: default: - // This error may be a false positive or a consequence of the network being disconnected. - // Since the network is controlled from outside this class, only significant error messages are reported. - if( httpCode > 0 ) { - Serial.printf("Server responded with HTTP Status '%i' when calling url '%s'. Please check your setup\n", httpCode, UpdateURL.c_str() ); - } else { - log_d("Unknown HTTP response"); - } + setStreamGetter( getHTTPStream ); break; } + } - http.end(); - return false; + if( !isConnected ) { + setStatusChecker( WiFiStatusCheck ); } +} - // TODO: Not all streams respond with a content length. - // TODO: Set contentLength to UPDATE_SIZE_UNKNOWN when content type is valid. - // check contentLength and content type - if( !contentLength || !isValidContentType ) { - Serial.printf("There was no content in the http response: (length: %i, valid: %s)\n", contentLength, isValidContentType?"true":"false"); - http.end(); +void esp32FOTA::stopStream() +{ + if( endStream ) { // user function provided via ::setStreamEnder( fn ) + endStream( this ); + return; + } + + // no user function provided, apply default behaviour + switch( _stream_type ) { + case FOTA_FILE_STREAM: + if( _file ) _file.close(); + break; + case FOTA_HTTP_STREAM: + _http.end(); + break; + case FOTA_SERIAL_STREAM: + default: + break; + } + +} + + + +// OTA Logic +bool esp32FOTA::execOTA() +{ + setupStream(); + + if( !_flashFileSystemUrl.isEmpty() ) { // a data partition was specified in the json manifest, handle the spiffs partition first + if( _fs ) { // Possible risk of overwriting certs and signatures, cancel flashing! + Serial.println("Cowardly refusing to overwrite U_SPIFFS with "+_flashFileSystemUrl+". Use setCertFileSystem(nullptr) along with setPubKey()/setCAPem() to enable this feature."); + return false; + } else { + log_i("Will check if U_SPIFFS needs updating"); + if( !execOTA( U_SPIFFS, false ) ) return false; + } + } else { + log_i("This update is for U_FLASH only"); + } + // handle the application partition and restart on success + bool ret = execOTA( U_FLASH, true ); + + stopStream(); + + return ret; +} + + +bool esp32FOTA::execOTA( int partition, bool restart_after ) +{ + // health checks + if( partition == U_SPIFFS && _flashFileSystemUrl.isEmpty() ) { + log_i("[SKIP] No spiffs/littlefs/fatfs partition was specified"); + return true; // data partition is optional, so not an error + } else if ( partition == U_FLASH && _firmwareUrl.isEmpty() ) { + log_e("No app partition was specified"); + return false; // app partition is mandatory + } else if( partition != U_SPIFFS && partition != U_FLASH ) { + log_e("Bad partition number: %i or empty URL", partition); + return false; + } + + // call getHTTPStream + int64_t updateSize = getStream( this, partition ); + + if( updateSize<=0 || _stream == nullptr ) { + log_e("HTTP Error"); return false; } - log_d("contentLength : %i, isValidContentType : %s", contentLength, String(isValidContentType)); + mode_z = F_isZlibStream(); - if( _cfg.check_sig && contentLength != UPDATE_SIZE_UNKNOWN ) { - // If firmware is signed, extract signature and decrease content-length by 512 bytes for signature - contentLength -= 512; + log_d("compression: %s", mode_z ? "enabled" : "disabled" ); + + if( _cfg.check_sig ) { + if( mode_z ) { + Serial.println("[ERROR] Compressed && signed image is not (yet) supported"); + return false; + } + if( updateSize == UPDATE_SIZE_UNKNOWN || updateSize <= FW_SIGNATURE_LENGTH ) { + Serial.println("[ERROR] Malformed signature+fw combo"); + return false; + } + updateSize -= FW_SIGNATURE_LENGTH; } - // Check if there is enough available space on the partition to perform the Update - bool canBegin = Update.begin( contentLength, partition ); + + // If using compression, the size is implicitely unknown + size_t fwsize = mode_z ? UPDATE_SIZE_UNKNOWN : updateSize; // fw_size is unknown if we have a compressed image + + bool canBegin = F_canBegin(); if( !canBegin ) { Serial.println("Not enough space to begin OTA, partition size mismatch?"); - http.end(); + F_abort(); if( onUpdateBeginFail ) onUpdateBeginFail( partition ); return false; } if( onOTAProgress ) { - Update.onProgress( onOTAProgress ); + F_Update.onProgress( onOTAProgress ); } else { - Update.onProgress( [](size_t progress, size_t size) { + F_Update.onProgress( [](size_t progress, size_t size) { if( progress >= size ) Serial.println(); else if( progress > 0) Serial.print("."); }); } - Stream& stream = http.getStream(); - - unsigned char signature[512]; + unsigned char signature[FW_SIGNATURE_LENGTH]; if( _cfg.check_sig ) { - stream.readBytes( signature, 512 ); + _stream->readBytes( signature, FW_SIGNATURE_LENGTH ); } + Serial.printf("Begin %s OTA. This may take 2 - 5 mins to complete. Things might be quiet for a while.. Patience!\n", partition==U_FLASH?"Firmware":"Filesystem"); - // Some activity may appear in the Serial monitor during the update (depends on Update.onProgress). - // This may take 2 - 5mins to complete - size_t written = Update.writeStream( stream ); + // Some activity may appear in the Serial monitor during the update (depends on Update.onProgress) + size_t written = F_writeStream(); - if ( written == contentLength) { - Serial.println("Written : " + String(written) + " successfully"); - } else if ( contentLength == UPDATE_SIZE_UNKNOWN ) { + if (fwsize == UPDATE_SIZE_UNKNOWN) // match compressed fw size to responce length + fwsize = updateSize; + + if ( written == fwsize ) { Serial.println("Written : " + String(written) + " successfully"); - contentLength = written; // populate value as it was unknown until now + updateSize = written; // flatten value to prevent overflow when checking signature } else { - Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Premature end of stream?"); - contentLength = written; // flatten value to prevent overflow when checking signature - } - - if (!Update.end()) { - Serial.println("An Update Error Occurred. Error #: " + String(Update.getError())); - // #define UPDATE_ERROR_OK (0) - // #define UPDATE_ERROR_WRITE (1) - // #define UPDATE_ERROR_ERASE (2) - // #define UPDATE_ERROR_READ (3) - // #define UPDATE_ERROR_SPACE (4) - // #define UPDATE_ERROR_SIZE (5) - // #define UPDATE_ERROR_STREAM (6) - // #define UPDATE_ERROR_MD5 (7) - // #define UPDATE_ERROR_MAGIC_BYTE (8) - // #define UPDATE_ERROR_ACTIVATE (9) - // #define UPDATE_ERROR_NO_PARTITION (10) - // #define UPDATE_ERROR_BAD_ARGUMENT (11) - // #define UPDATE_ERROR_ABORT (12) + Serial.println("Written only : " + String(written) + "/" + String(updateSize) + ". Premature end of stream?"); + F_abort(); return false; } - http.end(); + if (!F_UpdateEnd()) { + Serial.println("An Update Error Occurred. Error #: " + String(F_Update.getError())); + return false; + } if( onUpdateEnd ) onUpdateEnd( partition ); @@ -513,11 +529,11 @@ bool esp32FOTA::execOTA( int partition, bool restart_after ) // by temporarily reassigning the bootable flag to the running-partition instead // of the next-partition. esp_ota_set_boot_partition( running_partition ); - // By doing so the ESP will NOT boot any unvalidated partition should a crash occur - // during signature validation. + // By doing so the ESP will NOT boot any unvalidated partition should a reset occur + // during signature validation (crash, oom, power failure). } - if( !validate_sig( _target_partition, signature, contentLength ) ) { + if( !validate_sig( _target_partition, signature, updateSize ) ) { // erase partition esp_partition_erase_range( _target_partition, _target_partition->address, _target_partition->size ); @@ -538,7 +554,7 @@ bool esp32FOTA::execOTA( int partition, bool restart_after ) } } //Serial.println("OTA Update complete!"); - if (Update.isFinished()) { + if (F_Update.isFinished()) { if( onUpdateFinished ) onUpdateFinished( partition, restart_after ); @@ -586,8 +602,8 @@ bool esp32FOTA::checkJSONManifest(JsonVariant doc) } log_i("Payload type in manifest %s matches current firmware %s", doc["type"].as(), _cfg.name ); - _flashFileSystemUrl = ""; - _firmwareUrl = ""; + _flashFileSystemUrl.clear(); + _firmwareUrl.clear(); if(doc["version"].is()) { uint16_t v = doc["version"].as(); @@ -602,30 +618,30 @@ bool esp32FOTA::checkJSONManifest(JsonVariant doc) _payload_sem = SemverClass(0); } - //debugSemVer("Payload firmware version", &_payloadVersion ); debugSemVer("Payload firmware version", _payload_sem.ver() ); // Memoize some values to help with the decision tree - bool has_url = doc.containsKey("url") && doc["url"].is(); - bool has_firmware = doc.containsKey("bin") && doc["bin"].is(); - bool has_hostname = doc.containsKey("host") && doc["host"].is(); - bool has_port = doc.containsKey("port") && doc["port"].is(); - uint16_t portnum = has_port ? doc["port"].as() : 0; + bool has_url = doc["url"].is(); + bool has_firmware = doc["bin"].is(); + bool has_hostname = doc["host"].is(); + //bool has_signature = doc["sig"].is(); + bool has_port = doc["port"].is(); + uint16_t portnum = doc["port"].as(); bool has_tls = has_port ? (portnum == 443 || portnum == 4433) : false; - bool has_spiffs = doc.containsKey("spiffs") && doc["spiffs"].is(); - bool has_littlefs = doc.containsKey("littlefs") && doc["littlefs"].is(); - bool has_fatfs = doc.containsKey("fatfs") && doc["fatfs"].is(); + bool has_spiffs = doc["spiffs"].is(); + bool has_littlefs = doc["littlefs"].is(); + bool has_fatfs = doc["fatfs"].is(); bool has_filesystem = has_littlefs || has_spiffs || has_fatfs; - String protocol = has_tls ? "https" : "http"; + String protocol(has_tls ? "https" : "http"); String flashFSPath = has_filesystem ? ( has_littlefs - ? doc["littlefs"].as() + ? doc["littlefs"].as() : has_spiffs - ? doc["spiffs"].as() - : doc["fatfs"].as() + ? doc["spiffs"].as() + : doc["fatfs"].as() ) : ""; @@ -638,14 +654,14 @@ bool esp32FOTA::checkJSONManifest(JsonVariant doc) ); if( has_url ) { // Basic scenario: a complete URL was provided in the JSON manifest, all other keys will be ignored - _firmwareUrl = doc["url"].as(); + _firmwareUrl = doc["url"].as(); if( has_hostname ) { // If the manifest provides both, warn the user log_w("Manifest provides both url and host - Using URL"); } } else if( has_firmware && has_hostname && has_port ) { // Precise scenario: Hostname, Port and Firmware Path were provided - _firmwareUrl = protocol + "://" + doc["host"].as() + ":" + String( portnum ) + doc["bin"].as(); + _firmwareUrl = protocol + "://" + doc["host"].as() + ":" + portnum + doc["bin"].as(); if( has_filesystem ) { // More complex scenario: the manifest also provides a [spiffs, littlefs or fatfs] partition - _flashFileSystemUrl = protocol + "://" + doc["host"].as() + ":" + String( portnum ) + flashFSPath; + _flashFileSystemUrl = protocol + "://" + doc["host"].as() + ":" + portnum + flashFSPath; } } else { // JSON was malformed - no firmware target was provided log_e("JSON manifest was missing one of the required keys :(" ); @@ -661,18 +677,18 @@ bool esp32FOTA::checkJSONManifest(JsonVariant doc) } + + bool esp32FOTA::execHTTPcheck() { String useURL = String( _cfg.manifest_url ); // being deprecated, soon unsupported! - if( useURL=="" && checkURL!="" ) { + if( useURL.isEmpty() && !checkURL.isEmpty() ) { Serial.println("checkURL will soon be unsupported, use FOTAConfig_t::manifest_url instead!!"); useURL = checkURL; } - const char* rootcastr = nullptr; - // being deprecated, soon unsupported! if( useDeviceID ) { Serial.println("useDeviceID will soon be unsupported, use FOTAConfig_t::use_device_id instead!!"); @@ -685,53 +701,20 @@ bool esp32FOTA::execHTTPcheck() useURL += argseparator + "id=" + getDeviceID(); } - if ((WiFi.status() != WL_CONNECTED)) { //Check the current connection status - log_i("WiFi not connected - skipping HTTP check"); + if ( isConnected && !isConnected() ) { // Check the current connection status + log_i("Connection check requested but network not ready - skipping"); return false; // WiFi not connected } log_i("Getting HTTP: %s", useURL.c_str()); log_i("------"); - HTTPClient http; - WiFiClientSecure client; - http.setFollowRedirects( HTTPC_STRICT_FOLLOW_REDIRECTS ); - - if( useURL.substring( 0, 5 ) == "https" ) { - if( _cfg.unsafe ) { - // We're downloading from a secure port, but we don't want to validate the root cert. - client.setInsecure(); - } else { - // We're downloading from a secure port, and want to validate the root cert. - if( !_cfg.root_ca ) { - Serial.println("A strict security context has been set to fetch json manifest, but no RootCA was provided, aborting"); - return false; - } - rootcastr = _cfg.root_ca->get(); - if( _cfg.root_ca->size() == 0 ) { - Serial.println("A strict security context has been set to fetch json manifest, but an empty RootCA was provided, aborting"); - return false; - } - if( !rootcastr ) { - Serial.println("Unable to get RootCA to fetch json manifest, aborting"); - return false; - } - Serial.println("Loading root_ca.pem"); - client.setCACert( rootcastr ); - } - http.begin( client, useURL ); - } else { - http.begin( useURL ); - } - - if( extraHTTPHeaders.size() > 0 ) { - // add custom headers provided by user e.g. http.addHeader("Authorization", "Basic " + auth) - for( const auto &header : extraHTTPHeaders ) { - http.addHeader(header.first, header.second); - } + if(! setupHTTP( useURL.c_str() ) ) { + log_e("Unable to setup http, aborting!"); + return false; } - int httpCode = http.GET(); //Make the request + int httpCode = _http.GET(); //Make the request // only handle 200/301, fail on everything else if( httpCode != HTTP_CODE_OK && httpCode != HTTP_CODE_MOVED_PERMANENTLY ) { @@ -742,25 +725,22 @@ bool esp32FOTA::execHTTPcheck() } else { log_d("Unknown HTTP response"); } - http.end(); + _http.end(); return false; } // TODO: use payload.length() to speculate on JSONResult buffer size #define JSON_FW_BUFF_SIZE 2048 DynamicJsonDocument JSONResult( JSON_FW_BUFF_SIZE ); - - String payload = http.getString(); - - DeserializationError err = deserializeJson( JSONResult, payload.c_str() ); - - http.end(); // We're done with HTTP - free the resources + DeserializationError err = deserializeJson( JSONResult, _http.getStream() ); if (err) { // Check for errors in parsing, or JSON length may exceed buffer size - Serial.printf("JSON Parsing failed (%s, in=%d bytes, buff=%d bytes):\n%s\n", err.c_str(), payload.length(), JSON_FW_BUFF_SIZE, payload.c_str() ); + Serial.printf("JSON Parsing failed (%s, in=%d bytes, buff=%d bytes):\n", err.c_str(), _http.getSize(), JSON_FW_BUFF_SIZE ); return false; } + _http.end(); // We're done with HTTP - free the resources + if (JSONResult.is()) { // Although improbable given the size on JSONResult buffer, we already received an array of multiple firmware types and/or versions JsonArray arr = JSONResult.as(); @@ -791,7 +771,7 @@ String esp32FOTA::getDeviceID() // Force a firmware update regardless on current version -void esp32FOTA::forceUpdate(String firmwareURL, bool validate ) +void esp32FOTA::forceUpdate(const char* firmwareURL, bool validate ) { _firmwareUrl = firmwareURL; _cfg.check_sig = validate; @@ -799,11 +779,15 @@ void esp32FOTA::forceUpdate(String firmwareURL, bool validate ) } -void esp32FOTA::forceUpdate(String firmwareHost, uint16_t firmwarePort, String firmwarePath, bool validate ) +void esp32FOTA::forceUpdate(const char* firmwareHost, uint16_t firmwarePort, const char* firmwarePath, bool validate ) { - String firmwareURL = ( firmwarePort == 443 || firmwarePort == 4433 ) ? "https" : "http"; - firmwareURL += firmwareHost + ":" + String( firmwarePort ) + firmwarePath; - forceUpdate( firmwareURL, validate ); + String firmwareURL("http"); + if ( firmwarePort == 443 || firmwarePort == 4433 ) firmwareURL += "s"; + firmwareURL += String(firmwareHost); + firmwareURL += ":"; + firmwareURL += String(firmwarePort); + firmwareURL += firmwarePath; + forceUpdate( firmwareURL.c_str(), validate ); } @@ -846,4 +830,141 @@ void esp32FOTA::debugSemVer( const char* label, semver_t* version ) log_i("%s: %s", label, version_no ); } + + + + + +static int64_t getHTTPStream( esp32FOTA* fota, int partition ) +{ + + const char* url = partition==U_SPIFFS ? fota->getFlashFS_URL() : fota->getFirmwareURL(); + + Serial.printf("Opening item %s\n", url ); + + if(! fota->setupHTTP( url ) ) { // certs + log_e("unable to setup http, aborting!"); + return -1; + } + + int64_t updateSize = 0; + int httpCode = fota->getHTTPCLient()->GET(); + String contentType; + + fota->setFotaStream( nullptr ); + + if( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) { + updateSize = fota->getHTTPCLient()->getSize(); + contentType = fota->getHTTPCLient()->header( "Content-type" ); + String acceptRange = fota->getHTTPCLient()->header( "Accept-Ranges" ); + if( !acceptRange.isEmpty() ) { + Serial.printf("This server supports resume! Accept-Ranges: %s\n", acceptRange.c_str() ); + } else { + Serial.println("This server does not support resume!" ); + } + } else { + switch( httpCode ) { + // 1xx = Hold on + // 2xx = Here you go + // 3xx = Go away + // 4xx = You fucked up + // 5xx = I fucked up + + case 204: log_e("Status: 204 (No contents), "); break; + case 401: log_e("Status: 401 (Unauthorized), check setExtraHTTPHeader() values"); break; + case 403: log_e("Status: 403 (Forbidden), check path on webserver?"); break; + case 404: log_e("Status: 404 (Not Found), also a palindrom, check path in manifest?"); break; + case 418: log_e("Status: 418 (I'm a teapot), Brit alert!"); break; + case 429: log_e("Status: 429 (Too many requests), throttle things down?"); break; + case 500: log_e("Status: 500 (Internal Server Error), you broke the webs!"); break; + default: + // This error may be a false positive or a consequence of the network being disconnected. + // Since the network is controlled from outside this class, only significant error messages are reported. + if( httpCode > 0 ) { + Serial.printf("Server responded with HTTP Status '%i'. Please check your setup\n", httpCode ); + } else { + log_d("Unknown HTTP response"); + } + break; + } + + return -1; + } + + // TODO: Not all streams respond with a content length. + // TODO: Set updateSize to UPDATE_SIZE_UNKNOWN when content type is valid. + + // check updateSize and content type + if( updateSize<=0 ) { + Serial.printf("There was no content in the http response: (length: %" PRId64 ", contentType: %s)\n", updateSize, contentType.c_str()); + return -1; + } + + log_d("updateSize : %" PRId64 ", contentType: %s", updateSize, contentType.c_str()); + + fota->setFotaStream( fota->getHTTPCLient()->getStreamPtr() ); + + return updateSize; +} + + + +static int64_t getFileStream( esp32FOTA* fota, int partition) +{ + fs::FS* fs = fota->getFotaFS(); + + if(!fs ) { + Serial.println("[ERROR] No filesystem defined, use ::setCertFileSystem( &SD ) "); + return -1; + } + + const char* path = partition==U_SPIFFS ? fota->getFlashFS_URL() : fota->getFirmwareURL(); + Serial.printf("Opening item %s\n", path ); + + fs::File* file = (fs::File*)fota->getFotaStreamPtr(); + *file = fs->open( path ); + + if(! file ) { + log_e("unable to access filesystem, aborting!"); + return -1; + } + + int64_t updateSize = file->size(); + + // check updateSize and content type + if( !updateSize ) { + Serial.println("[ERROR] Empty file"); + file->close(); + fota->setFotaStream( nullptr ); + return -1; + } + + log_d("updateSize : %i", updateSize); + + return updateSize; +} + + + +static int64_t getSerialStream( esp32FOTA* fota, int partition) +{ + return -1; +} + + + +static bool WiFiStatusCheck() +{ + return (WiFi.status() == WL_CONNECTED); +} + +/* +static bool EthernetStatusCheck() +{ + return eth_connected; +} +*/ + + + #pragma GCC diagnostic pop diff --git a/src/esp32FOTA.hpp b/src/esp32FOTA.hpp index b7e8aa7..358873f 100644 --- a/src/esp32FOTA.hpp +++ b/src/esp32FOTA.hpp @@ -40,7 +40,6 @@ extern "C" { #include #include #include -#include #include #include @@ -74,6 +73,40 @@ extern "C" { + +#if __has_include() + #pragma message "Using FlashZ as Update agent" + #include + #define F_Update FlashZ::getInstance() + #define F_hasZlib() true + #define F_isZlibStream() (_stream->peek() == ZLIB_HEADER && ((partition == U_SPIFFS && _flashFileSystemUrl.indexOf("zz")>-1) || (partition == U_FLASH && _firmwareUrl.indexOf("zz")>-1))) + #define F_canBegin() (mode_z ? F_Update.beginz(UPDATE_SIZE_UNKNOWN, partition) : F_Update.begin(updateSize, partition)) + #define F_UpdateEnd() (mode_z ? F_Update.endz() : F_Update.end()) + #define F_abort() if (mode_z) F_Update.abortz(); else F_Update.abort() + #define F_writeStream() (mode_z ? F_Update.writezStream(*_stream, updateSize) : F_Update.writeStream(*_stream)) +#elif __has_include("ESP32-targz.h") + #pragma message "Using GzUpdateClass as Update agent" + #include + #define F_Update GzUpdateClass::getInstance() + #define F_hasZlib() true + #define F_isZlibStream() (_stream->peek() == 0x1f && ((partition == U_SPIFFS && _flashFileSystemUrl.indexOf("gz")>-1) || (partition == U_FLASH && _firmwareUrl.indexOf("gz")>-1)) ) + #define F_canBegin() (mode_z ? F_Update.begingz(UPDATE_SIZE_UNKNOWN, partition) : F_Update.begin(updateSize, partition)) + #define F_UpdateEnd() (mode_z ? F_Update.endgz() : F_Update.end()) + #define F_abort() if (mode_z) F_Update.abortgz(); else F_Update.abort() + #define F_writeStream() (mode_z ? F_Update.writeGzStream(*_stream, updateSize) : F_Update.writeStream(*_stream)) +#else + #include + #define F_Update Update + #define F_hasZlib() false + #define F_isZlibStream() false + #define F_canBegin() F_Update.begin(updateSize, partition) + #define F_UpdateEnd() F_Update.end() + #define F_abort() F_Update.abort() + #define F_writeStream() F_Update.writeStream(*_stream); +#endif + + + struct SemverClass { public: @@ -140,6 +173,13 @@ struct FOTAConfig_t }; +enum FOTAStreamType_t +{ + FOTA_HTTP_STREAM, + FOTA_FILE_STREAM, + FOTA_SERIAL_STREAM +}; + // Main Class class esp32FOTA @@ -147,29 +187,46 @@ class esp32FOTA public: esp32FOTA(); - esp32FOTA( FOTAConfig_t cfg ); - esp32FOTA(String firwmareType, int firwmareVersion, bool validate = false, bool allow_insecure_https = false ); - esp32FOTA(String firwmareType, String firmwareSemanticVersion, bool validate = false, bool allow_insecure_https = false ); ~esp32FOTA(); + esp32FOTA( FOTAConfig_t cfg ); + esp32FOTA(const char* firwmareType, int firwmareVersion, bool validate = false, bool allow_insecure_https = false ); + esp32FOTA(const String &firwmareType, int firwmareVersion, bool validate = false, bool allow_insecure_https = false ) + : esp32FOTA(firwmareType.c_str(), firwmareVersion, validate, allow_insecure_https){}; + esp32FOTA(const char* firwmareType, const char* firmwareSemanticVersion, bool validate = false, bool allow_insecure_https = false ); + esp32FOTA(const String &firwmareType, const String &firmwareSemanticVersion, bool validate = false, bool allow_insecure_https = false ) + : esp32FOTA(firwmareType.c_str(), firmwareSemanticVersion.c_str(), validate, allow_insecure_https){}; + template void setPubKey( T* asset ) { _cfg.pub_key = (CryptoAsset*)asset; _cfg.check_sig = true; } template void setRootCA( T* asset ) { _cfg.root_ca = (CryptoAsset*)asset; _cfg.unsafe = false; } - void forceUpdate(String firmwareHost, uint16_t firmwarePort, String firmwarePath, bool validate ); - void forceUpdate(String firmwareURL, bool validate ); + void forceUpdate(const char* firmwareHost, uint16_t firmwarePort, const char* firmwarePath, bool validate ); + void forceUpdate(const char* firmwareURL, bool validate ); void forceUpdate(bool validate ); + + void handle(); + bool execOTA(); bool execOTA( int partition, bool restart_after = true ); bool execHTTPcheck(); - int getPayloadVersion(); - void getPayloadVersion(char * version_string); - void setManifestURL( String manifest_url ) { _cfg.manifest_url = manifest_url.c_str(); } void useDeviceId( bool use=true ) { _cfg.use_device_id = use; } - bool validate_sig( const esp_partition_t* partition, unsigned char *signature, uint32_t firmware_size ); + + // config setter + void setConfig( FOTAConfig_t cfg ) { _cfg = cfg; } + + // Manually specify the manifest url, this is provided as a transition between legagy and new config system + void setManifestURL( const String &manifest_url ) { _cfg.manifest_url = manifest_url.c_str(); } + + // use this to set "Authorization: Basic" or other specific headers to be sent with the queries + void setExtraHTTPHeader( String name, String value ) { extraHTTPHeaders[name] = value; } + + // /!\ Only use this to change filesystem for **default** RootCA and PubKey paths. + // Otherwise use setPubKey() and setRootCA() + void setCertFileSystem( fs::FS *cert_filesystem = nullptr ); // this is passed to Update.onProgress() - typedef std::function ProgressCallback_cb; // size_t progress, size_t size + typedef std::function ProgressCallback_cb; // size_t progress, size_t size void setProgressCb(ProgressCallback_cb fn) { onOTAProgress = fn; } // callback setter // when Update.begin() returned false @@ -188,26 +245,66 @@ class esp32FOTA typedef std::function UpdateFinished_cb; // int partition (U_FLASH or U_SPIFFS), bool restart_after void setUpdateFinishedCb(UpdateFinished_cb fn) { onUpdateFinished = fn; } // callback setter - // use this to set "Authorization: Basic" or other specific headers to be sent with the queries - void setExtraHTTPHeader( String name, String value ) { extraHTTPHeaders[name] = value; } + // stream getter + typedef std::function getStream_cb; // esp32FOTA* this, int partition (U_FLASH or U_SPIFFS), returns stream size + void setStreamGetter( getStream_cb fn ) { getStream = fn; } // callback setter - // /!\ Only use this to change filesystem for **default** RootCA and PubKey paths. - // Otherwise use setPubKey() and setRootCA() - void setCertFileSystem( fs::FS *cert_filesystem = nullptr ); + // stream ender + typedef std::function endStream_cb; // esp32FOTA* this + void setStreamEnder( endStream_cb fn ) { endStream = fn; } // callback setter - // config getters and setters - FOTAConfig_t getConfig() { return _cfg; }; - void setConfig( FOTAConfig_t cfg ) { _cfg = cfg; } + // connection check + typedef std::function isConnected_cb; // + void setStatusChecker( isConnected_cb fn ) { isConnected = fn; } // callback setter + + // updating from a File or from Serial? + void setStreamType( FOTAStreamType_t stream_type ) { _stream_type = stream_type; } + + const char* getManifestURL() { return _manifestUrl.c_str(); } + const char* getFirmwareURL() { return _firmwareUrl.c_str(); } + const char* getFlashFS_URL() { return _flashFileSystemUrl.c_str(); } + const char* getPath(int part) { return part==U_SPIFFS ? getFlashFS_URL() : getFirmwareURL(); } + + bool zlibSupported() { return mode_z; } + + int getPayloadVersion(); + void getPayloadVersion(char * version_string); + + FOTAConfig_t getConfig() { return _cfg; }; + FOTAStreamType_t getStreamType() { return _stream_type; } + HTTPClient* getHTTPCLient() { return &_http; } + WiFiClientSecure* getWiFiClient() { return &_client; } + fs::File* getFotaFilePtr() { return &_file; } + Stream* getFotaStreamPtr() { return _stream; } + fs::FS* getFotaFS() { return _fs; } + + // internals but need to be exposed to the callbacks + bool setupHTTP( const char* url ); + void setFotaStream( Stream* stream ) { _stream = stream; } [[deprecated("Use setManifestURL( String ) or cfg.manifest_url with setConfig( FOTAConfig_t )")]] String checkURL = ""; [[deprecated("Use cfg.use_device_id with setConfig( FOTAConfig_t )")]] bool useDeviceID = false; + private: + HTTPClient _http; + WiFiClientSecure _client; + Stream *_stream; + fs::File _file; + + bool mode_z = F_hasZlib(); + + FOTAStreamType_t _stream_type = FOTA_HTTP_STREAM; // defaults to HTTP + + void setupStream(); + void stopStream(); + FOTAConfig_t _cfg; SemverClass _payload_sem = SemverClass(0,0,0); + String _manifestUrl; String _firmwareUrl; String _flashFileSystemUrl; @@ -219,6 +316,9 @@ class esp32FOTA UpdateEnd_cb onUpdateEnd; // after Update.end() and before validate_sig() UpdateCheckFail_cb onUpdateCheckFail; // validate_sig() error handling, mixed situations UpdateFinished_cb onUpdateFinished; // update successful + getStream_cb getStream; // optional stream getter, defaults to http.getStreamPtr() + endStream_cb endStream; // optional stream closer, defaults to http.end() + isConnected_cb isConnected; // optional connection checker, defaults to WiFi.status()==WL_CONNECTED std::map extraHTTPHeaders; // this holds the extra http headers defined by the user @@ -227,6 +327,8 @@ class esp32FOTA void debugSemVer( const char* label, semver_t* version ); void getPartition( int update_partition ); + bool validate_sig( const esp_partition_t* partition, unsigned char *signature, uint32_t firmware_size ); + // temporary partition holder for signature check operations const esp_partition_t* _target_partition = nullptr;