From 9d2fb8bc5030cdcd27dbfd06b599241f191ef48b Mon Sep 17 00:00:00 2001 From: RnD1 Date: Thu, 9 Apr 2026 03:43:16 +0900 Subject: [PATCH] tt --- HCUpdate.h.backup | 291 ++++++++++++++ HCUpdater.cpp.backup | 787 ++++++++++++++++++++++++++++++++++++++ UPnPClient.cpp.backup | 491 ++++++++++++++++++++++++ src/firmware_download.php | 170 ++++++++ 4 files changed, 1739 insertions(+) create mode 100644 HCUpdate.h.backup create mode 100644 HCUpdater.cpp.backup create mode 100644 UPnPClient.cpp.backup create mode 100644 src/firmware_download.php diff --git a/HCUpdate.h.backup b/HCUpdate.h.backup new file mode 100644 index 0000000..16c8c8c --- /dev/null +++ b/HCUpdate.h.backup @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#if defined(ESP32) +#ifndef ESP32UPDATER_H +#define ESP32UPDATER_H + +#include +#include +#include +#include +#include "esp_partition.h" + +#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) +#define UPDATE_ERROR_DECRYPT (13) + +#define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF + +#define U_FLASH 0 +#define U_SPIFFS 100 +#define U_AUTH 200 + +#define ENCRYPTED_BLOCK_SIZE 16 +#define ENCRYPTED_TWEAK_BLOCK_SIZE 32 +#define ENCRYPTED_KEY_SIZE 32 + +#define U_AES_DECRYPT_NONE 0 +#define U_AES_DECRYPT_AUTO 1 +#define U_AES_DECRYPT_ON 2 +#define U_AES_DECRYPT_MODE_MASK 3 +#define U_AES_IMAGE_DECRYPTING_BIT 4 + +#define SPI_SECTORS_PER_BLOCK 16 // usually large erase block is 32k/64k +#define SPI_FLASH_BLOCK_SIZE (SPI_SECTORS_PER_BLOCK * SPI_FLASH_SEC_SIZE) + +enum HTTPUpdateResult { + HTTP_UPDATE_FAILED, + HTTP_UPDATE_NO_UPDATES, + HTTP_UPDATE_OK +}; + +class UpdateClass { +public: + typedef std::function THandlerFunction_Progress; + + UpdateClass(); + UpdateClass(int httpClientTimeout); + + int update(WiFiClientSecure& client, String &url, uint16_t port, String& uri, + String ¤tVersion, short nDeviceType, bool bForceUpdate); + int handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType); + int http_downloadUpdate(HTTPClient &httpClient); + + + /* + This callback will be called when Update is receiving data + */ + UpdateClass &onProgress(THandlerFunction_Progress fn); + + /* + Call this to check the space needed for the update + Will return false if there is not enough space + */ + bool begin(size_t size = UPDATE_SIZE_UNKNOWN, int command = U_FLASH, int ledPin = -1, uint8_t ledOn = LOW, const char *label = NULL); + + /* + Setup decryption configuration + Crypt Key is 32bytes(256bits) block of data, use the same key as used to encrypt image file + Crypt Address, use the same value as used to encrypt image file + Crypt Config, use the same value as used to encrypt image file + Crypt Mode, used to select if image files should be decrypted or not + */ + bool setupCrypt(const uint8_t *cryptKey = 0, size_t cryptAddress = 0, uint8_t cryptConfig = 0xf, int cryptMode = U_AES_DECRYPT_AUTO); + + /* + Writes a buffer to the flash and increments the address + Returns the amount written + */ + size_t write(uint8_t *data, size_t len); + + /* + Writes the remaining bytes from the Stream to the flash + Uses readBytes() and sets UPDATE_ERROR_STREAM on timeout + Returns the bytes written + Should be equal to the remaining bytes when called + Usable for slow streams like Serial + */ + size_t writeStream(Stream &data); + + /* + If all bytes are written + this call will write the config to eboot + and return true + If there is already an update running but is not finished and !evenIfRemaining + or there is an error + this will clear everything and return false + the last error is available through getError() + evenIfRemaining is helpful when you update without knowing the final size first + */ + bool end(bool evenIfRemaining = false); + + /* + sets AES256 key(32 bytes) used for decrypting image file + */ + bool setCryptKey(const uint8_t *cryptKey); + + /* + sets crypt mode used on image files + */ + bool setCryptMode(const int cryptMode); + + /* + sets address used for decrypting image file + */ + void setCryptAddress(const size_t cryptAddress) { + _cryptAddress = cryptAddress & 0x00fffff0; + } + + /* + sets crypt config used for decrypting image file + */ + void setCryptConfig(const uint8_t cryptConfig) { + _cryptCfg = cryptConfig & 0x0f; + } + + /* + Aborts the running update + */ + void abort(); + + /* + Prints the last error to an output stream + */ + void printError(Print &out); + + const char *errorString(); + + /* + sets the expected MD5 for the firmware (hexString) + */ + bool setMD5(const char *expected_md5); + + /* + returns the MD5 String of the successfully ended firmware + */ + String md5String(void) { + return _md5.toString(); + } + + /* + populated the result with the md5 bytes of the successfully ended firmware + */ + void md5(uint8_t *result) { + return _md5.getBytes(result); + } + + //Helpers + uint8_t getError() { + return _error; + } + void clearError() { + _error = UPDATE_ERROR_OK; + } + bool hasError() { + return _error != UPDATE_ERROR_OK; + } + bool isRunning() { + return _size > 0; + } + bool isFinished() { + return _progress == _size; + } + size_t size() { + return _size; + } + size_t progress() { + return _progress; + } + size_t remaining() { + return _size - _progress; + } + + /* + Template to write from objects that expose + available() and read(uint8_t*, size_t) methods + faster than the writeStream method + writes only what is available + */ + template size_t write(T &data) { + size_t written = 0; + if (hasError() || !isRunning()) { + return 0; + } + + size_t available = data.available(); + while (available) { + if (_bufferLen + available > remaining()) { + available = remaining() - _bufferLen; + } + if (_bufferLen + available > 4096) { + size_t toBuff = 4096 - _bufferLen; + data.read(_buffer + _bufferLen, toBuff); + _bufferLen += toBuff; + if (!_writeBuffer()) { + return written; + } + written += toBuff; + } else { + data.read(_buffer + _bufferLen, available); + _bufferLen += available; + written += available; + if (_bufferLen == remaining()) { + if (!_writeBuffer()) { + return written; + } + } + } + if (remaining() == 0) { + return written; + } + available = data.available(); + } + return written; + } + + /* + check if there is a firmware on the other OTA partition that you can bootinto + */ + bool canRollBack(); + /* + set the other OTA partition as bootable (reboot to enable) + */ + bool rollBack(); + +private: + void _reset(); + void _abort(uint8_t err); + void _cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key); + bool _decryptBuffer(); + bool _writeBuffer(); + bool _verifyHeader(uint8_t data); + bool _verifyEnd(); + bool _enablePartition(const esp_partition_t *partition); + bool _chkDataInBlock(const uint8_t *data, size_t len) const; // check if block contains any data or is empty + + int _httpClientTimeout; + followRedirects_t _followRedirects = HTTPC_DISABLE_FOLLOW_REDIRECTS; + uint8_t _error; + uint8_t *_cryptKey; + uint8_t *_cryptBuffer; + uint8_t *_buffer; + uint8_t *_skipBuffer; + size_t _bufferLen; + size_t _size; + THandlerFunction_Progress _progress_callback; + uint32_t _progress; + uint32_t _paroffset; + uint32_t _command; + const esp_partition_t *_partition; + + String _target_md5; + MD5Builder _md5; + + int _ledPin; + uint8_t _ledOn; + + uint8_t _cryptMode; + size_t _cryptAddress; + uint8_t _cryptCfg; +}; + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE) +extern UpdateClass Update; +#endif + +#endif // defined(ESP32UPDATER_H) +#endif // defined(ESP32) diff --git a/HCUpdater.cpp.backup b/HCUpdater.cpp.backup new file mode 100644 index 0000000..3ebbb21 --- /dev/null +++ b/HCUpdater.cpp.backup @@ -0,0 +1,787 @@ +#if defined(ESP32) +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "HermitCrab.h" + +#include "Arduino.h" +#include "spi_flash_mmap.h" +#include "esp_ota_ops.h" +#include "esp_image_format.h" +#include "mbedtls/aes.h" +#include +#include +#include +#include "HCUpdate.h" + +#define TAG_FW "FW Upate" + +static const char *_err2str(uint8_t _error) { + if (_error == UPDATE_ERROR_OK) { + return ("No Error"); + } else if (_error == UPDATE_ERROR_WRITE) { + return ("Flash Write Failed"); + } else if (_error == UPDATE_ERROR_ERASE) { + return ("Flash Erase Failed"); + } else if (_error == UPDATE_ERROR_READ) { + return ("Flash Read Failed"); + } else if (_error == UPDATE_ERROR_SPACE) { + return ("Not Enough Space"); + } else if (_error == UPDATE_ERROR_SIZE) { + return ("Bad Size Given"); + } else if (_error == UPDATE_ERROR_STREAM) { + return ("Stream Read Timeout"); + } else if (_error == UPDATE_ERROR_MD5) { + return ("MD5 Check Failed"); + } else if (_error == UPDATE_ERROR_MAGIC_BYTE) { + return ("Wrong Magic Byte"); + } else if (_error == UPDATE_ERROR_ACTIVATE) { + return ("Could Not Activate The Firmware"); + } else if (_error == UPDATE_ERROR_NO_PARTITION) { + return ("Partition Could Not be Found"); + } else if (_error == UPDATE_ERROR_BAD_ARGUMENT) { + return ("Bad Argument"); + } else if (_error == UPDATE_ERROR_ABORT) { + return ("Aborted"); + } else if (_error == UPDATE_ERROR_DECRYPT) { + return ("Decryption error"); + } + return ("UNKNOWN"); +} + +int UpdateClass::update(WiFiClientSecure& client, String& host, uint16_t port, String&uri, String& currentVersion, short nDeviceType, bool bForceUpdate) +{ + HTTPClient http; + if (!http.begin(client, host, port, uri)) { + ESP_LOGI(TAG_FW,"OTA - httpClient begin failed\n"); + return HTTP_UPDATE_FAILED; + } + int size = handleUpdate(http, currentVersion, nDeviceType); + + ESP_LOGI(TAG_FW,"OTA - size(%d) Server Version: %s Mode: %s\n", size, http.header("version").c_str(), + http.header("update") && http.header("update").toInt() == 1 ? "Download" : "Check Only" ); + + //is there an image to download + if (size >= 0) { + if (!http.header("update") || http.header("update").toInt() == 0) { + ESP_LOGI(TAG_FW,"OTA - No Firmware available"); + } else if (!http.header("version") || strcmp(http.header("version").c_str(), HC__VERSION) <= 0) { + ESP_LOGI(TAG_FW,"OTA - Firmware is upto Date"); + } else { + //image avaliabe to download & update + if (!bForceUpdate) { + ESP_LOGI(TAG_FW,"OTA - Found V%s Firmware\n", http.header("version").c_str()); + } else { + ESP_LOGI(TAG_FW,"OTA - Downloading & Installing V%s Firmware\n", http.header("version").c_str()); + } + + if (bForceUpdate) { + if (http_downloadUpdate(http) == 0) { + http.end(); //end connection + ESP_LOGI(TAG_FW,"OTA - Firmware Update Successful, rebooting"); + ESP.restart(); + } + } + } + } + + http.end(); //end connection + return 0; +} + +int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType) +{ + HTTPUpdateResult ret = HTTP_UPDATE_FAILED; + + // use HTTP/1.0 for update since the update handler not support any transfer Encoding + http.useHTTP10(true); + http.setTimeout(_httpClientTimeout); + http.setFollowRedirects(_followRedirects); + http.setUserAgent("ESP32-http-Update"); + http.addHeader("Cache-Control", "no-cache"); + http.addHeader("X-ESP32-DEVICE-TYPE", String(nDeviceType)); + http.addHeader("X-ESP32-VERSION", HC__VERSION); + http.addHeader("X-ESP32-STA-MAC", WiFi.macAddress()); + + unsigned long nChipId = 0; + for (int i = 0; i < 17; i = i + 8) { + nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + http.addHeader(F("X-ESP32-Chip-ID"), String(nChipId)); + http.addHeader(F("x-ESP32-free-space"), String(ESP.getFreeSketchSpace())); + http.addHeader(F("x-ESP32-sketch-size"), String(ESP.getSketchSize())); + http.addHeader(F("x-ESP32-sketch-md5"), String(ESP.getSketchMD5())); + //http.addHeader(F("x-ESP32-chip-size"), String(ESP.getFlashChipRealSize())); + http.addHeader(F("x-ESP32-sdk-version"), ESP.getSdkVersion()); + + //set headers to look for to get returned values in servers http response to our http request + const char *headerkeys[] = {"update", "version"}; //server returns update 0=no update found, 1=update found, version=version of update found + size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *); + http.collectHeaders(headerkeys, headerkeyssize); + + int code = http.GET(); + int len = http.getSize(); + + if (code == HTTP_CODE_OK) { + return (len > 0 ? len : 0); //return 0 or length of image to download + } else if (code < 0) { + ESP_LOGI(TAG_FW,"Error: %s\n", http.errorToString(code).c_str()); + ESP_LOGI(TAG_FW, "%s", http.getString()); + return code; //error code should be minus between -1 to -11 + } else { + ESP_LOGI(TAG_FW,"Error: HTTP Server response code %i\n", code); + ESP_LOGI(TAG_FW, "%s", http.getString()); + return -code; //return code should be minus between -100 to -511 + } + + return ret; +} + +int UpdateClass::http_downloadUpdate(HTTPClient &httpClient) { + WiFiClient *stream = httpClient.getStreamPtr(); + int written = 0; + + // Check content length and begin update + int fileSize = httpClient.getSize(); + if (fileSize <= 0) { + ESP_LOGI(TAG_FW,"Invalid content length"); + return 1; + } + if (!Update.begin(fileSize, U_FLASH)) { + ESP_LOGI(TAG_FW,"Update begin failed!"); + return 1; + } + + // Download and write the update + while (httpClient.connected() && written < fileSize) { + size_t availableBytes = stream->available(); + if (availableBytes) { + uint8_t buffer[128]; // Buffer to hold incoming data + int bytesRead = stream->readBytes(buffer, min(availableBytes, sizeof(buffer))); + int bytesWritten = Update.write(buffer, bytesRead); + if (bytesWritten != bytesRead) { + ESP_LOGI(TAG_FW,"Update write failed!"); + Update.end(); + return 1; + } + written += bytesWritten; + } + } + + // Finalize update + if (Update.end()) { + if (Update.isFinished()) { + ESP_LOGI(TAG_FW,"Update successful!"); + return 0; + } else { + ESP_LOGI(TAG_FW,"Update not finished!"); + return 1; + } + } else { + ESP_LOGI(TAG_FW,"Update end failed!"); + return 1; + } +} + + +static bool _partitionIsBootable(const esp_partition_t *partition) { + uint8_t buf[ENCRYPTED_BLOCK_SIZE]; + if (!partition) { + return false; + } + if (!ESP.partitionRead(partition, 0, (uint32_t *)buf, ENCRYPTED_BLOCK_SIZE)) { + return false; + } + + if (buf[0] != ESP_IMAGE_HEADER_MAGIC) { + return false; + } + return true; +} + +bool UpdateClass::_enablePartition(const esp_partition_t *partition) { + if (!partition) { + return false; + } + return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE); +} + +UpdateClass::UpdateClass() + : _error(0) + , _cryptKey(0) + , _cryptBuffer(0) + , _buffer(0) + , _skipBuffer(0) + , _bufferLen(0) + , _size(0) + , _progress_callback(NULL) + , _progress(0) + , _paroffset(0) + , _command(U_FLASH) + , _partition(NULL) + , _cryptMode(U_AES_DECRYPT_AUTO) + , _cryptAddress(0) + , _cryptCfg(0xf) + , _httpClientTimeout(8000) + {} + +UpdateClass::UpdateClass(int httpClientTimeout) + : _error(0) + , _cryptKey(0) + , _cryptBuffer(0) + , _buffer(0) + , _skipBuffer(0) + , _bufferLen(0) + , _size(0) + , _progress_callback(NULL) + , _progress(0) + , _paroffset(0) + , _command(U_FLASH) + , _partition(NULL) + , _cryptMode(U_AES_DECRYPT_AUTO) + , _cryptAddress(0) + , _cryptCfg(0xf) + , _httpClientTimeout(httpClientTimeout) + {} + +UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress fn) { + _progress_callback = fn; + return *this; +} + +void UpdateClass::_reset() { + if (_buffer) { + delete[] _buffer; + } + if (_skipBuffer) { + delete[] _skipBuffer; + } + + _cryptBuffer = nullptr; + _buffer = nullptr; + _skipBuffer = nullptr; + _bufferLen = 0; + _progress = 0; + _size = 0; + _command = U_FLASH; + + if (_ledPin != -1) { + digitalWrite(_ledPin, !_ledOn); // off + } +} + +bool UpdateClass::canRollBack() { + if (_buffer) { //Update is running + return false; + } + const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL); + return _partitionIsBootable(partition); +} + +bool UpdateClass::rollBack() { + if (_buffer) { //Update is running + return false; + } + const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL); + return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition); +} + +bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) { + if (_size > 0) { + log_w("already running"); + return false; + } + + _ledPin = ledPin; + _ledOn = !!ledOn; // 0(LOW) or 1(HIGH) + + _reset(); + _error = 0; + _target_md5 = emptyString; + _md5 = MD5Builder(); + + if (size == 0) { + _error = UPDATE_ERROR_SIZE; + return false; + } + + if (command == U_FLASH) { + _partition = esp_ota_get_next_update_partition(NULL); + if (!_partition) { + _error = UPDATE_ERROR_NO_PARTITION; + return false; + } + log_d("OTA Partition: %s", _partition->label); + } else if (command == U_SPIFFS) { + _partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, label); + _paroffset = 0; + if (!_partition) { + _partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL); + _paroffset = 0x1000; //Offset for ffat, assuming size is already corrected + if (!_partition) { + _error = UPDATE_ERROR_NO_PARTITION; + return false; + } + } + } else { + _error = UPDATE_ERROR_BAD_ARGUMENT; + log_e("bad command %u", command); + return false; + } + + if (size == UPDATE_SIZE_UNKNOWN) { + size = _partition->size; + } else if (size > _partition->size) { + _error = UPDATE_ERROR_SIZE; + log_e("too large %u > %u", size, _partition->size); + return false; + } + + //initialize + _buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE]; + if (!_buffer) { + log_e("_buffer allocation failed"); + return false; + } + _size = size; + _command = command; + _md5.begin(); + return true; +} + +bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) { + if (setCryptKey(cryptKey)) { + if (setCryptMode(cryptMode)) { + setCryptAddress(cryptAddress); + setCryptConfig(cryptConfig); + return true; + } + } + return false; +} + +bool UpdateClass::setCryptKey(const uint8_t *cryptKey) { + if (!cryptKey) { + if (_cryptKey) { + delete[] _cryptKey; + _cryptKey = 0; + log_d("AES key unset"); + } + return false; //key cleared, no key to decrypt with + } + //initialize + if (!_cryptKey) { + _cryptKey = new (std::nothrow) uint8_t[ENCRYPTED_KEY_SIZE]; + } + if (!_cryptKey) { + log_e("new failed"); + return false; + } + memcpy(_cryptKey, cryptKey, ENCRYPTED_KEY_SIZE); + return true; +} + +bool UpdateClass::setCryptMode(const int cryptMode) { + if (cryptMode >= U_AES_DECRYPT_NONE && cryptMode <= U_AES_DECRYPT_ON) { + _cryptMode = cryptMode; + } else { + log_e("bad crypt mode argument %i", cryptMode); + return false; + } + return true; +} + +void UpdateClass::_abort(uint8_t err) { + _reset(); + _error = err; +} + +void UpdateClass::abort() { + _abort(UPDATE_ERROR_ABORT); +} + +void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) { + memcpy(tweaked_key, _cryptKey, ENCRYPTED_KEY_SIZE); + if (_cryptCfg == 0) { + return; //no tweaking needed, use crypt key as-is + } + + const uint8_t pattern[] = {23, 23, 23, 14, 23, 23, 23, 12, 23, 23, 23, 10, 23, 23, 23, 8}; + int pattern_idx = 0; + int key_idx = 0; + int bit_len = 0; + uint32_t tweak = 0; + cryptAddress &= 0x00ffffe0; //bit 23-5 + cryptAddress <<= 8; //bit23 shifted to bit31(MSB) + while (pattern_idx < sizeof(pattern)) { + tweak = cryptAddress << (23 - pattern[pattern_idx]); //bit shift for small patterns + // alternative to: tweak = rotl32(tweak,8 - bit_len); + tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + bit_len)); //rotate to line up with end of previous tweak bits + bit_len += pattern[pattern_idx++] - 4; //add number of bits in next pattern(23-4 = 19bits = 23bit to 5bit) + while (bit_len > 7) { + tweaked_key[key_idx++] ^= tweak; //XOR byte + // alternative to: tweak = rotl32(tweak, 8); + tweak = (tweak << 8) | (tweak >> 24); //compiler should optimize to use rotate(fast) + bit_len -= 8; + } + tweaked_key[key_idx] ^= tweak; //XOR remaining bits, will XOR zeros if no remaining bits + } + if (_cryptCfg == 0xf) { + return; //return with fully tweaked key + } + + //some of tweaked key bits need to be restore back to crypt key bits + const uint8_t cfg_bits[] = {67, 65, 63, 61}; + key_idx = 0; + pattern_idx = 0; + while (key_idx < ENCRYPTED_KEY_SIZE) { + bit_len += cfg_bits[pattern_idx]; + if ((_cryptCfg & (1 << pattern_idx)) == 0) { //restore crypt key bits + while (bit_len > 0) { + if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) { //restore a crypt key byte + tweaked_key[key_idx] = _cryptKey[key_idx]; + } else { //MSBits restore crypt key bits, LSBits keep as tweaked bits + tweaked_key[key_idx] &= (0xff >> bit_len); + tweaked_key[key_idx] |= (_cryptKey[key_idx] & (~(0xff >> bit_len))); + } + key_idx++; + bit_len -= 8; + } + } else { //keep tweaked key bits + while (bit_len > 0) { + if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) { //MSBits keep as tweaked bits, LSBits restore crypt key bits + tweaked_key[key_idx] &= (~(0xff >> bit_len)); + tweaked_key[key_idx] |= (_cryptKey[key_idx] & (0xff >> bit_len)); + } + key_idx++; + bit_len -= 8; + } + } + pattern_idx++; + } +} + +bool UpdateClass::_decryptBuffer() { + if (!_cryptKey) { + log_w("AES key not set"); + return false; + } + if (_bufferLen % ENCRYPTED_BLOCK_SIZE != 0) { + log_e("buffer size error"); + return false; + } + if (!_cryptBuffer) { + _cryptBuffer = new (std::nothrow) uint8_t[ENCRYPTED_BLOCK_SIZE]; + } + if (!_cryptBuffer) { + log_e("new failed"); + return false; + } + uint8_t tweaked_key[ENCRYPTED_KEY_SIZE]; //tweaked crypt key + int done = 0; + + /* + Mbedtls functions will be replaced with esp_aes functions when hardware acceleration is available + + To Do: + Replace mbedtls for the cases where there's no hardware acceleration + */ + + mbedtls_aes_context ctx; //initialize AES + mbedtls_aes_init(&ctx); + while ((_bufferLen - done) >= ENCRYPTED_BLOCK_SIZE) { + for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) { + _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i] = _buffer[i + done]; //reverse order 16 bytes to decrypt + } + if (((_cryptAddress + _progress + done) % ENCRYPTED_TWEAK_BLOCK_SIZE) == 0 || done == 0) { + _cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key); //update tweaked crypt key + if (mbedtls_aes_setkey_enc(&ctx, tweaked_key, 256)) { + return false; + } + if (mbedtls_aes_setkey_dec(&ctx, tweaked_key, 256)) { + return false; + } + } + if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) { //use MBEDTLS_AES_ENCRYPT to decrypt flash code + return false; + } + for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) { + _buffer[i + done] = _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i]; //reverse order 16 bytes from decrypt + } + done += ENCRYPTED_BLOCK_SIZE; + } + return true; +} + +bool UpdateClass::_writeBuffer() { + //first bytes of loading image, check to see if loading image needs decrypting + if (!_progress) { + _cryptMode &= U_AES_DECRYPT_MODE_MASK; + if ((_cryptMode == U_AES_DECRYPT_ON) || ((_command == U_FLASH) && (_cryptMode & U_AES_DECRYPT_AUTO) && (_buffer[0] != ESP_IMAGE_HEADER_MAGIC))) { + _cryptMode |= U_AES_IMAGE_DECRYPTING_BIT; //set to decrypt the loading image + log_d("Decrypting OTA Image"); + } + } + //check if data in buffer needs decrypting + if (_cryptMode & U_AES_IMAGE_DECRYPTING_BIT) { + if (!_decryptBuffer()) { + _abort(UPDATE_ERROR_DECRYPT); + return false; + } + } + //first bytes of new firmware + uint8_t skip = 0; + if (!_progress && _command == U_FLASH) { + //check magic + if (_buffer[0] != ESP_IMAGE_HEADER_MAGIC) { + _abort(UPDATE_ERROR_MAGIC_BYTE); + return false; + } + + //Stash the first 16 bytes of data and set the offset so they are + //not written at this point so that partially written firmware + //will not be bootable + skip = ENCRYPTED_BLOCK_SIZE; + _skipBuffer = new (std::nothrow) uint8_t[skip]; + if (!_skipBuffer) { + log_e("_skipBuffer allocation failed"); + return false; + } + memcpy(_skipBuffer, _buffer, skip); + } + if (!_progress && _progress_callback) { + _progress_callback(0, _size); + } + size_t offset = _partition->address + _progress; + bool block_erase = + (_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0); // if it's the block boundary, than erase the whole block from here + bool part_head_sectors = + _partition->address % SPI_FLASH_BLOCK_SIZE + && offset < (_partition->address / SPI_FLASH_BLOCK_SIZE + 1) * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition heading block + bool part_tail_sectors = + offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition tailing block + if (block_erase || part_head_sectors || part_tail_sectors) { + if (!ESP.partitionEraseRange(_partition, _progress, block_erase ? SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE)) { + _abort(UPDATE_ERROR_ERASE); + return false; + } + } + + // try to skip empty blocks on unencrypted partitions + if ((_partition->encrypted || _chkDataInBlock(_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) + && !ESP.partitionWrite(_partition, _progress + skip, (uint32_t *)_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) { + _abort(UPDATE_ERROR_WRITE); + return false; + } + + //restore magic or md5 will fail + if (!_progress && _command == U_FLASH) { + _buffer[0] = ESP_IMAGE_HEADER_MAGIC; + } + _md5.add(_buffer, _bufferLen); + _progress += _bufferLen; + _bufferLen = 0; + if (_progress_callback) { + _progress_callback(_progress, _size); + } + return true; +} + +bool UpdateClass::_verifyHeader(uint8_t data) { + if (_command == U_FLASH) { + if (data != ESP_IMAGE_HEADER_MAGIC) { + _abort(UPDATE_ERROR_MAGIC_BYTE); + return false; + } + return true; + } else if (_command == U_SPIFFS) { + return true; + } + return false; +} + +bool UpdateClass::_verifyEnd() { + if (_command == U_FLASH) { + if (!_enablePartition(_partition) || !_partitionIsBootable(_partition)) { + _abort(UPDATE_ERROR_READ); + return false; + } + + if (esp_ota_set_boot_partition(_partition)) { + _abort(UPDATE_ERROR_ACTIVATE); + return false; + } + _reset(); + return true; + } else if (_command == U_SPIFFS) { + _reset(); + return true; + } + return false; +} + +bool UpdateClass::setMD5(const char *expected_md5) { + if (strlen(expected_md5) != 32) { + return false; + } + _target_md5 = expected_md5; + _target_md5.toLowerCase(); + return true; +} + +bool UpdateClass::end(bool evenIfRemaining) { + if (hasError() || _size == 0) { + return false; + } + + if (!isFinished() && !evenIfRemaining) { + log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size); + _abort(UPDATE_ERROR_ABORT); + return false; + } + + if (evenIfRemaining) { + if (_bufferLen > 0) { + _writeBuffer(); + } + _size = progress(); + } + + _md5.calculate(); + if (_target_md5.length()) { + if (_target_md5 != _md5.toString()) { + _abort(UPDATE_ERROR_MD5); + return false; + } + } + + return _verifyEnd(); +} + +size_t UpdateClass::write(uint8_t *data, size_t len) { + if (hasError() || !isRunning()) { + return 0; + } + + if (len > remaining()) { + _abort(UPDATE_ERROR_SPACE); + return 0; + } + + size_t left = len; + + while ((_bufferLen + left) > SPI_FLASH_SEC_SIZE) { + size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen; + memcpy(_buffer + _bufferLen, data + (len - left), toBuff); + _bufferLen += toBuff; + if (!_writeBuffer()) { + return len - left; + } + left -= toBuff; + } + memcpy(_buffer + _bufferLen, data + (len - left), left); + _bufferLen += left; + if (_bufferLen == remaining()) { + if (!_writeBuffer()) { + return len - left; + } + } + return len; +} + +size_t UpdateClass::writeStream(Stream &data) { + size_t written = 0; + size_t toRead = 0; + int timeout_failures = 0; + + if (hasError() || !isRunning()) { + return 0; + } + + if (_command == U_FLASH && !_cryptMode) { + if (!_verifyHeader(data.peek())) { + _reset(); + return 0; + } + } + + if (_ledPin != -1) { + pinMode(_ledPin, OUTPUT); + } + + while (remaining()) { + if (_ledPin != -1) { + digitalWrite(_ledPin, _ledOn); // Switch LED on + } + size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen; + if (bytesToRead > remaining()) { + bytesToRead = remaining(); + } + + /* + Init read&timeout counters and try to read, if read failed, increase counter, + wait 100ms and try to read again. If counter > 300 (30 sec), give up/abort + */ + toRead = 0; + timeout_failures = 0; + while (!toRead) { + toRead = data.readBytes(_buffer + _bufferLen, bytesToRead); + if (toRead == 0) { + timeout_failures++; + if (timeout_failures >= 300) { + _abort(UPDATE_ERROR_STREAM); + return written; + } + delay(100); + } + } + + if (_ledPin != -1) { + digitalWrite(_ledPin, !_ledOn); // Switch LED off + } + _bufferLen += toRead; + if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) { + return written; + } + written += toRead; + +#if CONFIG_FREERTOS_UNICORE + delay(1); // Fix solo WDT +#endif + } + return written; +} + +void UpdateClass::printError(Print &out) { + out.println(_err2str(_error)); +} + +const char *UpdateClass::errorString() { + return _err2str(_error); +} + +bool UpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const { + // check 32-bit aligned blocks only + if (!len || len % sizeof(uint32_t)) { + return true; + } + + size_t dwl = len / sizeof(uint32_t); + + do { + if (*(uint32_t *)data ^ 0xffffffff) { // for SPI NOR flash empty blocks are all one's, i.e. filled with 0xff byte + return true; + } + + data += sizeof(uint32_t); + } while (--dwl); + return false; +} + +#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE) +UpdateClass Update; +#endif + +#endif // defined(ESP32) \ No newline at end of file diff --git a/UPnPClient.cpp.backup b/UPnPClient.cpp.backup new file mode 100644 index 0000000..ef39e39 --- /dev/null +++ b/UPnPClient.cpp.backup @@ -0,0 +1,491 @@ +#include +#include "HermitCrab.h" +#include "UPnpClient.h" +#include "Config.h" +#include "WiFiHost.h" +#define TAG_UPNP "UPnP" + + +bool CUpnpClient::registerUPnP(uint32_t *pip, uint16_t *pport) { + routerIP = *pip; // Gateway IP address + publicPort = *pport; // host.m_nPublicPort = config.m_nPublicPort + publicIP = 0UL; + bool bSuccess = false; + ESP_LOGI(TAG_UPNP,"UPnP %s(%D)\n", routerIP.toString().c_str(), publicPort); + + // Step 1 - Discover UPnP service. + if (discoverUPnP()) { + // Step 2 - Check if mapping already exists + if (!(bSuccess = requestPortMappingEntry())) { + // Step 3 - Request new mapping + bSuccess = requestPortForwarding(); + } + } else { + ESP_LOGI(TAG_UPNP," UPnP discovery failed."); + } + + if (bSuccess) { + // Extract external IP + requestExternalIP(); + ESP_LOGI(TAG_UPNP," Public IP(Port): %s(%d)\n", publicIP.toString().c_str(), publicPort); + + + // Extract external port assigned by UPnP + // requestExternalPort(); + // ESP_LOGI(TAG_UPNP,"Assigned External Port: %d\n", publicPort); + + *pport = publicPort; // External server port + *pip = publicIP; // External IP address + return true; + } else { + ESP_LOGI(TAG_UPNP," UPnP PortForwarding failed."); + } + return bSuccess; +} + +bool CUpnpClient::discoverUPnP() { + //ESP_LOGI(TAG_UPNP,"\n\n** Sending SSDP discovery request..."); + WiFiUDP udp; + bool ret = false; + + // SSDP M-SEARCH Request + const char *ssdpRequest = + "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 3\r\n" + "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" + "\r\n"; + + udp.beginMulticast(SSDP_MULTICAST_IP, SSDP_PORT); + udp.beginPacket(SSDP_MULTICAST_IP, SSDP_PORT); + udp.write((const uint8_t *)ssdpRequest, strlen(ssdpRequest)); // Fix length + udp.endPacket(); + + unsigned long startTime = millis(); + while (millis() - startTime < SEARCH_TIMEOUT) { + int packetSize = udp.parsePacket(); + if (packetSize > 0) { + udp.read(buffer, sizeof(buffer) - 1); + buffer[packetSize] = '\0'; + + //ESP_LOGI(TAG_UPNP,"SSDP response received:"); + //ESP_LOGI(TAG_UPNP,buffer); + + // Check if the response contains "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1" + char *stField = strstr(buffer, "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1"); + if (stField) { + // Extract router's UPnP service URL + char *location = strstr(buffer, "LOCATION: "); + if (location) { + location += 9; // Move past "LOCATION: " + while (*location == ' ') location++; // skip blanks + char *end = strchr(location, '\r'); + if (end) { + *end = '\0'; + //ESP_LOGI(TAG_UPNP,"Router UPnP URL: %s\n", location); + routerLocation = String(location); + + // Extract IP and Port from the Location URL + int firstColonPos = routerLocation.indexOf(':'); // Find the first colon (for the protocol) + int secondColonPos = routerLocation.indexOf(':', firstColonPos + 1); // Find the second colon (IP:PORT) + + if (secondColonPos != -1) { + routerIPString = routerLocation.substring(routerLocation.indexOf("://") + 3, secondColonPos); // Extract IP + routerPort = routerLocation.substring(secondColonPos + 1, routerLocation.indexOf('/', secondColonPos)).toInt(); // Extract Port + } + + routerIP.fromString(routerIPString); + //ESP_LOGI(TAG_UPNP,"Router UPnP IP(Port): %s(%d)\n", routerIP.toString().c_str(), routerPort); + + String xml = fetchUPnPDescription(routerLocation); + if (!xml.isEmpty()) { + // ESP_LOGI(TAG_UPNP,xml); + // ESP_LOGI(TAG_UPNP,"\n--- End Of XML ----\n"); + + // Parse the XML and get the controlURL + // String parseXML(const String &xml); + controlURL = parseXML(xml); + if (!controlURL.isEmpty()) { + ret = true; + break; + } + } + } + } + } + } + } + udp.stop(); + + if (!ret) { + DPRINTLN("No SSDP response from UPnP device."); + } + return ret; +} + +String CUpnpClient::fetchUPnPDescription(const String &location) { + HTTPClient http; + WiFiClient client; + + //ESP_LOGI(TAG_UPNP,"Fetching UPnP XML from: \"%s\"\n", location.c_str()); + + http.begin(client, location); + int httpCode = http.GET(); + + if (httpCode > 0) { + //ESP_LOGI(TAG_UPNP,"HTTP Response Code: %d\n", httpCode); + if (httpCode == HTTP_CODE_OK) { + String xmlContent = http.getString(); + http.end(); + return xmlContent; + } + } + else { + //ESP_LOGI(TAG_UPNP,"HTTP Request failed, error: %s\n", http.errorToString(httpCode).c_str()); + } + + http.end(); + return ""; +} + +String CUpnpClient::parseXML(const String &xml) { + // Locate the WANIPConnection service + int servicePos = xml.indexOf("urn:schemas-upnp-org:service:WANIPConnection:1"); + if (servicePos == -1) { + ESP_LOGI(TAG_UPNP,"WANIPConnection service not found in XML."); + return ""; + } + + // Find the start of the controlURL tag within the block + int controlStart = xml.indexOf("", servicePos); + if (controlStart == -1) { + ESP_LOGI(TAG_UPNP,"controlURL not found in service block."); + return ""; + } + controlStart += 12; // Move past "" + + // Find the closing tag + int controlEnd = xml.indexOf("", controlStart); + if (controlEnd == -1) { + ESP_LOGI(TAG_UPNP,"Malformed XML: Missing ."); + return ""; + } + + // Extract and clean the controlURL + String controlURL = xml.substring(controlStart, controlEnd); + controlURL.trim(); // Remove any spaces or newlines + + //ESP_LOGI(TAG_UPNP,"Extracted controlURL: %s\n", controlURL.c_str()); + return controlURL; +} + +int CUpnpClient::sendSoapRequest(const char *request, char *response, size_t responseSize) { + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { // UPnP uses port 1900 for SOAP requests + ESP_LOGI(TAG_UPNP,"Failed to connect to router: %s\n", routerIP.toString().c_str()); + return -1; + } + + client.print("POST /control?WANIPConn1 HTTP/1.1\r\n"); + client.print("Host: "); + client.print(routerIP); + client.print("\r\n"); + client.print("Content-Type: text/xml; charset=\"utf-8\"\r\n"); + client.print("SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetSpecificPortMappingEntry\"\r\n"); + client.print("Content-Length: "); + client.print(strlen(request)); + client.print("\r\n\r\n"); + client.print(request); + + unsigned long startMillis = millis(); + while (!client.available() && millis() - startMillis < 5000) { + delay(10); // Wait for response + } + + int index = 0; + while (client.available() && index < responseSize - 1) { + response[index++] = client.read(); + } + response[index] = '\0'; + + client.stop(); + return index; +} + +bool CUpnpClient::requestPortMappingEntry() { + ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Mapping Entry..."); + char soapRequest[512]; + char mappingName[32]; + sprintf(mappingName, "HC_%04X", publicPort); + + snprintf(soapRequest, sizeof(soapRequest), + "" + "" + "" + "" + "" + "%d" + "TCP" + "%s" + "%s" + "" + "" + "", + publicPort, + WiFi.localIP().toString().c_str(), + mappingName); + + char response[1024]; + int responseLen = sendSoapRequest(soapRequest, response, sizeof(response)); + + if (responseLen > 0) { + if (strstr(response, mappingName)) { + //ESP_LOGI(TAG_UPNP," Port mapping already exists."); + return true; + } + } + ESP_LOGI(TAG_UPNP," Port mapping not found."); + return false; +} + +bool CUpnpClient::requestPortForwarding() { + // Sending port-forwarding request to the router's external IP or gateway + ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Forwarding..."); + + if (controlURL.isEmpty()) { + ESP_LOGI(TAG_UPNP,"Error: controlURL is empty."); + return false; + } + + // Ensure controlURL is correctly formatted (handle absolute/relative cases) + String postURL = controlURL; + if (postURL.startsWith("http://") || postURL.startsWith("https://")) { + int pathStart = postURL.indexOf('/', 8); // Skip "http://" + if (pathStart != -1) { + postURL = postURL.substring(pathStart); // Extract path only + ESP_LOGI(TAG_UPNP,"PostURL: \"%s\"\n", postURL.c_str()); + } else { + ESP_LOGI(TAG_UPNP,"Invalid controlURL format."); + return false; + } + } + + // XML body + char xmlBody[800]; + snprintf(xmlBody, sizeof(xmlBody), + "" + "" + "" + "" + "%d" + "TCP" + "%d" + "%s" + "1" + "HC_%04X" + "0" + "", + publicPort, publicPort, + WiFi.localIP().toString().c_str(), + publicPort); + //ESP_LOGI(TAG_UPNP,"XML Body: "); + //ESP_LOGI(TAG_UPNP,xmlBody); + + + // Calculate the correct content length + int contentLength = strlen(xmlBody); + + snprintf(buffer, sizeof(buffer), + "POST %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: text/xml; charset=\"utf-8\"\r\n" + "SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n" + "%s", // Append XML body after headers + controlURL.c_str(), + routerIP.toString().c_str(), routerPort, + contentLength, + xmlBody); + + //ESP_LOGI(TAG_UPNP,"==== HTTP Request ===\n\n%s\n\n--- End Of HTTP ---\n", buffer); + + // Connect and send + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { + ESP_LOGI(TAG_UPNP,"Failed to connect to the router. %s:%d\n", routerIP.toString().c_str(), routerPort); + return false; + } + + client.print(buffer); + + // Read response + String response; + uint32_t timeout = millis() + 5000; + while (client.available() == 0) { + if (millis() > timeout) { + ESP_LOGI(TAG_UPNP,"Router did not respond to port mapping request."); + client.stop(); + return false; + } + } + + while (client.available()) { + response += client.readString(); + } + client.stop(); + return true; +}; + +bool CUpnpClient::requestExternalIP() { + //ESP_LOGI(TAG_UPNP,"\n\nRequesting External IP via UPnP..."); + + // XML Request Body + char xmlBody[300]; + snprintf(xmlBody, sizeof(xmlBody), + "" + "" + "" + "" + "" + ""); + + int contentLength = strlen(xmlBody); + + // HTTP Request + snprintf(buffer, sizeof(buffer), + "POST %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: text/xml; charset=\"utf-8\"\r\n" + "SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress\"\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n" + "%s", + controlURL.c_str(), + routerIP.toString().c_str(), routerPort, + contentLength, + xmlBody); + + // Connect and Send + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { + DPRINTLN("Failed to connect to router for External IP request."); + return false; + } + + client.print(buffer); + + // Read Response + String response; + uint32_t timeout = millis() + 5000; + while (client.available() == 0) { + if (millis() > timeout) { + DPRINTLN("Router did not respond to External IP request."); + client.stop(); + return false; + } + } + + while (client.available()) { + response += client.readString(); + } + client.stop(); + + //ESP_LOGI(TAG_UPNP,"External IP Response:"); + //ESP_LOGI(TAG_UPNP,response); + + // Extract External IP from XML + char *sz = (char *)(response.c_str()); + char *extIP = strstr(sz, ""); + if (extIP) { + extIP += 22; + char *end = strchr(extIP, '<'); + if (end) *end = '\0'; + + publicIP = IPAddress(extIP); + DPRINTF("UPnP - External IP: %s\n", extIP); + return true; + } + + DPRINTLN("Failed to extract External IP."); + return false; +} + +bool CUpnpClient::requestExternalPort() { + //ESP_LOGI(TAG_UPNP,"\n\nRequesting External Port via UPnP..."); + + // XML Request Body for getting External Port + char xmlBody[300]; + snprintf(xmlBody, sizeof(xmlBody), + "" + "" + "" + "" + "" + ""); + + int contentLength = strlen(xmlBody); + + // HTTP Request to get External Port + snprintf(buffer, sizeof(buffer), + "POST %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: text/xml; charset=\"utf-8\"\r\n" + "SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalPort\"\r\n" + "Content-Length: %d\r\n" + "Connection: Close\r\n" + "\r\n" + "%s", + controlURL.c_str(), + routerIP.toString().c_str(), routerPort, + contentLength, + xmlBody); + + // Connect and Send + WiFiClient client; + if (!client.connect(routerIP, routerPort)) { + ESP_LOGI(TAG_UPNP,"Failed to connect to router for External Port request."); + return false; + } + + client.print(buffer); + + // Read Response + String response; + uint32_t timeout = millis() + 5000; + while (client.available() == 0) { + if (millis() > timeout) { + ESP_LOGI(TAG_UPNP,"Router did not respond to External Port request."); + client.stop(); + return false; + } + } + + while (client.available()) { + response += client.readString(); + } + client.stop(); + + //ESP_LOGI(TAG_UPNP,"External Port Response:"); + //ESP_LOGI(TAG_UPNP,response); + + // Extract External Port from XML + char *sz = (char *)(response.c_str()); + char *extPort = strstr(sz, ""); + if (extPort) { + extPort += 17; + char *end = strchr(extPort, '<'); + if (end) *end = '\0'; + + publicPort = atoi(extPort); + ESP_LOGI(TAG_UPNP,"External Port: %d\n", publicPort); + return true; + } + + ESP_LOGI(TAG_UPNP,"Failed to extract External Port."); + return false; +}; diff --git a/src/firmware_download.php b/src/firmware_download.php new file mode 100644 index 0000000..4be30f5 --- /dev/null +++ b/src/firmware_download.php @@ -0,0 +1,170 @@ += 5) { + $fProjectRaw = trim($parts[0]); + $fChipsetRaw = trim($parts[1]); + + // Reconstruct version as numeric string for comparison (e.g., 20260408001) + $fVersion = preg_replace("/[^0-9]/", "", $parts[2]) . preg_replace("/[^0-9]/", "", $parts[3]); + + // Only consider files belonging to this project and the ESP32 chipset + if (strcasecmp($fProjectRaw, $targetProject) === 0 && strcasecmp($fChipsetRaw, 'ESP32') === 0) { + if ((float)$fVersion > (float)$serverNewestVersion) $serverNewestVersion = $fVersion; + + // If file version > client version, add to potential candidates + if ((float)$fVersion > (float)$targetVersion) { + $availableUpdates[$fVersion] = $firmwareBaseDir . $file; + } + } + } + } +} + +// --- 6. FINAL DECISION --- +if (!empty($availableUpdates)) { + // Sort ascending to find the NEAREST (next) version for a stepped upgrade + ksort($availableUpdates); + $nextVer = array_key_first($availableUpdates); + $targetFile = $availableUpdates[$nextVer]; + + sendFile($targetFile, $nextVer); +} else { + // ========================================================= + // NO UPDATE FOUND: RETURN 304 NOT MODIFIED + // ========================================================= + header("X-Update-Required: 0"); + header("update: 0"); + header("X-New-Version: " . $rawVer); + header("version: 000000000"); // Reset display for client UI if needed + header($_SERVER["SERVER_PROTOCOL"]." 304 Not Modified", true, 304); + exit(); + // ========================================================= +} +?> \ No newline at end of file