tt
This commit is contained in:
parent
9e2873829c
commit
9d2fb8bc50
291
HCUpdate.h.backup
Normal file
291
HCUpdate.h.backup
Normal 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 ¤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<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
787
HCUpdater.cpp.backup
Normal 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
491
UPnPClient.cpp.backup
Normal 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
170
src/firmware_download.php
Normal 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();
|
||||||
|
// =========================================================
|
||||||
|
}
|
||||||
|
?>
|
||||||
Loading…
Reference in New Issue
Block a user