This commit is contained in:
Heuideog Yi @ PC RnD1 2026-04-09 03:43:16 +09:00
parent 9e2873829c
commit 9d2fb8bc50
4 changed files with 1739 additions and 0 deletions

291
HCUpdate.h.backup Normal file
View File

@ -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 <Arduino.h>
#include <MD5Builder.h>
#include <functional>
#include <WiFiClientSecure.h>
#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<void(size_t, size_t)> THandlerFunction_Progress;
UpdateClass();
UpdateClass(int httpClientTimeout);
int update(WiFiClientSecure& client, String &url, uint16_t port, String& uri,
String &currentVersion, 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<typename T> 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)

787
HCUpdater.cpp.backup Normal file
View File

@ -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 <WiFi.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#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)

491
UPnPClient.cpp.backup Normal file
View File

@ -0,0 +1,491 @@
#include <HTTPClient.h>
#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("<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>");
if (servicePos == -1) {
ESP_LOGI(TAG_UPNP,"WANIPConnection service not found in XML.");
return "";
}
// Find the start of the controlURL tag within the <service> block
int controlStart = xml.indexOf("<controlURL>", servicePos);
if (controlStart == -1) {
ESP_LOGI(TAG_UPNP,"controlURL not found in service block.");
return "";
}
controlStart += 12; // Move past "<controlURL>"
// Find the closing tag
int controlEnd = xml.indexOf("</controlURL>", controlStart);
if (controlEnd == -1) {
ESP_LOGI(TAG_UPNP,"Malformed XML: Missing </controlURL>.");
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),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body>"
"<u:GetSpecificPortMappingEntry xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
"<NewRemoteHost></NewRemoteHost>"
"<NewExternalPort>%d</NewExternalPort>"
"<NewProtocol>TCP</NewProtocol>"
"<NewInternalClient>%s</NewInternalClient>"
"<NewPortMappingDescription>%s</NewPortMappingDescription>"
"</u:GetSpecificPortMappingEntry>"
"</s:Body>"
"</s:Envelope>",
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),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body><u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
"<NewRemoteHost></NewRemoteHost>"
"<NewExternalPort>%d</NewExternalPort>"
"<NewProtocol>TCP</NewProtocol>"
"<NewInternalPort>%d</NewInternalPort>"
"<NewInternalClient>%s</NewInternalClient>"
"<NewEnabled>1</NewEnabled>"
"<NewPortMappingDescription>HC_%04X</NewPortMappingDescription>"
"<NewLeaseDuration>0</NewLeaseDuration>"
"</u:AddPortMapping></s:Body></s:Envelope>",
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),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body>"
"<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>"
"</s:Body>"
"</s:Envelope>");
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, "<NewExternalIPAddress>");
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),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body>"
"<u:GetExternalPort xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>"
"</s:Body>"
"</s:Envelope>");
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, "<NewExternalPort>");
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;
};

170
src/firmware_download.php Normal file
View File

@ -0,0 +1,170 @@
<?php
/**
* [firmware_download.php] - CLEAN PRODUCTION VERSION
* --------------------------------------------------------------------------------------------
* PURPOSE:
* - Serves firmware binaries to ESP32 clients using a "Stepped" update logic.
* - Validates client identity via User-Agent and specific hardware headers.
* - Scans the './firmware' directory for files matching: {Project}.{Chip}.{Version}.{SubVer}.bin
* - Selects the next available version (lowest version that is higher than the client's current).
* * WORKFLOW:
* 1. Identity Check: Rejects any request not matching 'ESP32-http-Update' UA.
* 2. Header Check: Validates MAC and system health headers from HCUpdate.cpp.
* 3. Matching: Filters local files by Project Name and 'ESP32' chipset.
* 4. Streaming: Delivers the binary with MD5 and Content-Length for ESP32 verification.
* --------------------------------------------------------------------------------------------
* Revision History:
* 2026.04.08 - [RnD16] Stripped logging for production performance.
* 2026.04.08 - [RnD17] Final production locking with detailed inline commentary.
*/
header('Content-type: text/plain; charset=utf8', true);
// --- 1. CORE FUNCTIONS ---
/**
* Validates existence and value of HTTP headers passed by the web server
* Note: PHP converts 'X-Header-Name' to 'HTTP_X_HEADER_NAME'
*/
function check_header($name, $value = false) {
if(!isset($_SERVER[$name])) return false;
if($value && $_SERVER[$name] != $value) return false;
return true;
}
/**
* Handles the actual binary delivery to the MCU
* Sends both standard HTTP headers and custom X-headers for HCUpdate logic
*/
function sendFile($path, $newVersion) {
// =========================================================
// MANDATORY HEADERS FOR CLIENT-SIDE HCUpdate.cpp DECISION
// =========================================================
header("X-New-Version: ".$newVersion);
header("X-Update-Required: 1");
header("version: ". $newVersion); // Legacy support header
header("update: 1"); // Legacy support header
// =========================================================
header($_SERVER["SERVER_PROTOCOL"].' 200 OK', true, 200);
header('Content-Type: application/octet-stream', true);
// basename() ensures we don't leak the internal server folder structure
header('Content-Disposition: attachment; filename='.basename($path));
// Critical for ESP32 Update.begin(size)
header('Content-Length: '.filesize($path), true);
// Used by ESP32 to verify file integrity after download
header('x-MD5: '.md5_file($path), true);
// Stream the actual bytes
readfile($path);
exit();
}
/**
* Standardized exit for cases where no update is found or access is denied
*/
function stop_and_exit($headerCode = 200, $headerMsg = "") {
// =========================================================
// FORCE UPDATE STATE TO FALSE ON EXIT
// =========================================================
header("X-Update-Required: 0");
header("update: 0");
// =========================================================
if ($headerCode != 200) {
header($_SERVER["SERVER_PROTOCOL"]." $headerCode $headerMsg", true, $headerCode);
}
exit();
}
// --- 2. CONFIGURATION ---
// Local directory where .bin files are stored
$firmwareBaseDir = realpath(__DIR__ . '/../firmware') . DIRECTORY_SEPARATOR;
// --- 3. STRICT IDENTITY & HEADER VALIDATION ---
// 1. User-Agent must be exactly 'ESP32-http-Update'
if(!check_header('HTTP_USER_AGENT', 'ESP32-http-Update')) {
header($_SERVER["SERVER_PROTOCOL"].' 403 Forbidden agent check', true, 403);
echo "UserAgent: only for ESP32 updater!\n";
exit();
}
// 2. Mandatory system headers must be present (MAC, Size, MD5, ChipInfo)
if( !check_header('HTTP_X_ESP32_STA_MAC') ||
!check_header('HTTP_X_ESP32_SKETCH_SIZE') ||
!check_header('HTTP_X_ESP32_SKETCH_MD5') ||
!check_header('HTTP_X_ESP32_CHIP_SIZE') )
{
header($_SERVER["SERVER_PROTOCOL"].' 403 Forbidden headercheck', true, 403);
echo "Header: only for ESP32 updater!";
exit();
}
// --- 4. PROJECT & VERSION EXTRACTION ---
if (!isset($_SERVER['HTTP_X_ESP_PROJECT']) || !isset($_SERVER['HTTP_X_ESP_VERSION'])) {
stop_and_exit(400, "Bad Request: Missing Project/Version");
}
$rawProj = trim($_SERVER['HTTP_X_ESP_PROJECT']);
$rawVer = trim($_SERVER['HTTP_X_ESP_VERSION']);
// Sanitization: Ensure paths/logic aren't broken by special characters
$targetProject = preg_replace("/[^a-zA-Z0-9]/", "-", $rawProj);
$targetVersion = preg_replace("/[^0-9]/", "", $rawVer);
// --- 5. SCANNING LOGIC ---
$availableUpdates = [];
$serverNewestVersion = "0";
if (is_dir($firmwareBaseDir)) {
// Filter out linux directory navigation dots
$files = array_diff(scandir($firmwareBaseDir), array('.', '..'));
foreach ($files as $file) {
// Expected format: ProjectName.Chipset.Version.Subversion.bin
$parts = explode('.', $file);
if (count($parts) >= 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();
// =========================================================
}
?>