tt
This commit is contained in:
parent
fd5298b691
commit
f9745ed01c
2
OTA.cpp
2
OTA.cpp
|
|
@ -17,7 +17,7 @@
|
||||||
// OTA
|
// OTA
|
||||||
//
|
//
|
||||||
// ==============================================================
|
// ==============================================================
|
||||||
const char *HC__VERSION = "20250415001";
|
const char *HC__VERSION = "20260415001";
|
||||||
#define UPDATE_PORT ((uint16_t) 443)
|
#define UPDATE_PORT ((uint16_t) 443)
|
||||||
const char *url = "visionsoft.kr";
|
const char *url = "visionsoft.kr";
|
||||||
const char *uri = "/sc/pages/firmware_download.php";
|
const char *uri = "/sc/pages/firmware_download.php";
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,41 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* [firmware_download.php] - CLEAN PRODUCTION VERSION
|
* [firmware_download.php] - UNIFIED STEPPED FIRMWARE SERVER (ESP32 & ESP8266)
|
||||||
* --------------------------------------------------------------------------------------------
|
* --------------------------------------------------------------------------------------------
|
||||||
* PURPOSE:
|
* PURPOSE:
|
||||||
* - Serves firmware binaries to ESP32 clients using a "Stepped" update logic.
|
* - Serves firmware binaries to both ESP32 and ESP8266 using "Stepped" upgrade logic.
|
||||||
* - Validates client identity via User-Agent and specific hardware headers.
|
* - Enforces chipset-specific header validation (MAC, MD5, Sketch/Chip Size).
|
||||||
* - Scans the './firmware' directory for files matching: {Project}.{Chip}.{Version}.{SubVer}.bin
|
* - Scans './firmware' for naming convention: {Project}.{Chipset}.{Version}.{SubVer}.bin
|
||||||
* - Selects the next available version (lowest version that is higher than the client's current).
|
* - Implements security gates: Rejects non-ESP User-Agents and missing hardware headers.
|
||||||
* * 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:
|
* REVISION HISTORY:
|
||||||
* 2026.04.08 - [RnD16] Stripped logging for production performance.
|
* 2026.04.08 - [RnD16] Initial production version; stripped logging for speed.
|
||||||
* 2026.04.08 - [RnD17] Final production locking with detailed inline commentary.
|
* 2026.04.10 - [RnD18] Added Dual-Chipset support (ESP32/ESP8266) with specific header gates.
|
||||||
|
* 2026.04.10 - [RnD19] Finalized Stepped logic (ksort) and finalized header synchronization.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Set default content type to text for error/info messages; binary will override this.
|
||||||
header('Content-type: text/plain; charset=utf8', true);
|
header('Content-type: text/plain; charset=utf8', true);
|
||||||
|
|
||||||
// --- 1. CORE FUNCTIONS ---
|
// =============================================================
|
||||||
|
// 0. LOGGING UTILITY
|
||||||
|
// =============================================================
|
||||||
|
function log_ota($message) {
|
||||||
|
$logFile = realpath(__DIR__ . '/../log') . '/ota_update.log';
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$clientIP = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
|
// Append to file: [Date] [IP] Message
|
||||||
|
file_put_contents($logFile, "[$timestamp] [$clientIP] $message" . PHP_EOL, FILE_APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 1. CORE UTILITY FUNCTIONS
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates existence and value of HTTP headers passed by the web server
|
* Validates existence and value of HTTP headers.
|
||||||
* Note: PHP converts 'X-Header-Name' to 'HTTP_X_HEADER_NAME'
|
* Note: PHP converts 'X-Header-Name' to 'HTTP_X_HEADER_NAME'.
|
||||||
*/
|
*/
|
||||||
function check_header($name, $value = false) {
|
function check_header($name, $value = false) {
|
||||||
if(!isset($_SERVER[$name])) return false;
|
if(!isset($_SERVER[$name])) return false;
|
||||||
|
|
@ -33,138 +44,153 @@ function check_header($name, $value = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the actual binary delivery to the MCU
|
* Executes the binary transfer to the MCU with all required metadata headers.
|
||||||
* Sends both standard HTTP headers and custom X-headers for HCUpdate logic
|
|
||||||
*/
|
*/
|
||||||
function sendFile($path, $newVersion) {
|
function sendFile($path, $newVersion) {
|
||||||
// =========================================================
|
log_ota(">>> SENDING UPDATE: File: " . basename($path) . " | New Version: $newVersion");
|
||||||
// MANDATORY HEADERS FOR CLIENT-SIDE HCUpdate.cpp DECISION
|
|
||||||
// =========================================================
|
// Standard protocol headers for UpdateClass decision logic
|
||||||
header("X-New-Version: ".$newVersion);
|
header("X-New-Version: ".$newVersion);
|
||||||
header("X-Update-Required: 1");
|
header("X-Update-Required: 1");
|
||||||
header("version: ". $newVersion); // Legacy support header
|
header("version: ". $newVersion); // Compatibility/Legacy header
|
||||||
header("update: 1"); // Legacy support header
|
header("update: 1"); // Compatibility/Legacy header
|
||||||
// =========================================================
|
|
||||||
|
|
||||||
header($_SERVER["SERVER_PROTOCOL"].' 200 OK', true, 200);
|
header($_SERVER["SERVER_PROTOCOL"].' 200 OK', true, 200);
|
||||||
header('Content-Type: application/octet-stream', true);
|
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));
|
header('Content-Disposition: attachment; filename='.basename($path));
|
||||||
|
|
||||||
// Critical for ESP32 Update.begin(size)
|
// Size is critical for Update.begin() on both ESP32 and ESP8266
|
||||||
header('Content-Length: '.filesize($path), true);
|
header('Content-Length: '.filesize($path), true);
|
||||||
|
|
||||||
// Used by ESP32 to verify file integrity after download
|
// MD5 allows the MCU to verify the flash integrity post-write
|
||||||
header('x-MD5: '.md5_file($path), true);
|
header('x-MD5: '.md5_file($path), true);
|
||||||
|
|
||||||
// Stream the actual bytes
|
// Clear output buffer and stream file
|
||||||
|
if (ob_get_level()) ob_end_clean();
|
||||||
readfile($path);
|
readfile($path);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standardized exit for cases where no update is found or access is denied
|
* Standard exit routine for 'No Update' or 'Access Denied' scenarios.
|
||||||
*/
|
*/
|
||||||
function stop_and_exit($headerCode = 200, $headerMsg = "") {
|
function stop_and_exit($headerCode = 200, $headerMsg = "") {
|
||||||
// =========================================================
|
|
||||||
// FORCE UPDATE STATE TO FALSE ON EXIT
|
|
||||||
// =========================================================
|
|
||||||
header("X-Update-Required: 0");
|
header("X-Update-Required: 0");
|
||||||
header("update: 0");
|
header("update: 0");
|
||||||
// =========================================================
|
|
||||||
|
|
||||||
if ($headerCode != 200) {
|
if ($headerCode != 200) {
|
||||||
header($_SERVER["SERVER_PROTOCOL"]." $headerCode $headerMsg", true, $headerCode);
|
header($_SERVER["SERVER_PROTOCOL"]." $headerCode $headerMsg", true, $headerCode);
|
||||||
}
|
}
|
||||||
|
echo $headerMsg;
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. CONFIGURATION ---
|
// =============================================================
|
||||||
// Local directory where .bin files are stored
|
// 2. CONFIGURATION & IDENTITY VALIDATION
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
$firmwareBaseDir = realpath(__DIR__ . '/../firmware') . DIRECTORY_SEPARATOR;
|
$firmwareBaseDir = realpath(__DIR__ . '/../firmware') . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
// --- 3. STRICT IDENTITY & HEADER VALIDATION ---
|
// --- A. CHIPSET DETECTION VIA USER-AGENT ---
|
||||||
|
$clientChipSet = "";
|
||||||
// 1. User-Agent must be exactly 'ESP32-http-Update'
|
if (check_header('HTTP_USER_AGENT', 'ESP32-http-Update')) {
|
||||||
if(!check_header('HTTP_USER_AGENT', 'ESP32-http-Update')) {
|
$clientChipSet = "ESP32";
|
||||||
header($_SERVER["SERVER_PROTOCOL"].' 403 Forbidden agent check', true, 403);
|
} else if (check_header('HTTP_USER_AGENT', 'ESP8266-http-Update')) {
|
||||||
echo "UserAgent: only for ESP32 updater!\n";
|
$clientChipSet = "ESP8266";
|
||||||
exit();
|
} else {
|
||||||
|
stop_and_exit(403, "Forbidden: Only for ESP32/ESP8266 UpdateClass");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Mandatory system headers must be present (MAC, Size, MD5, ChipInfo)
|
// --- B. CHIPSET-SPECIFIC HEADER GATES ---
|
||||||
if( !check_header('HTTP_X_ESP32_STA_MAC') ||
|
// This ensures we only talk to clients using our specific UpdateClass implementation
|
||||||
!check_header('HTTP_X_ESP32_SKETCH_SIZE') ||
|
if ($clientChipSet === "ESP32") {
|
||||||
!check_header('HTTP_X_ESP32_SKETCH_MD5') ||
|
if( !check_header('HTTP_X_ESP32_STA_MAC') ||
|
||||||
!check_header('HTTP_X_ESP32_CHIP_SIZE') )
|
!check_header('HTTP_X_ESP32_SKETCH_SIZE') ||
|
||||||
{
|
!check_header('HTTP_X_ESP32_SKETCH_MD5') ||
|
||||||
header($_SERVER["SERVER_PROTOCOL"].' 403 Forbidden headercheck', true, 403);
|
!check_header('HTTP_X_ESP32_CHIP_SIZE') ) {
|
||||||
echo "Header: only for ESP32 updater!";
|
stop_and_exit(403, "Forbidden: Missing ESP32 System Headers");
|
||||||
exit();
|
}
|
||||||
|
} else if ($clientChipSet === "ESP8266") {
|
||||||
|
if( !check_header('HTTP_X_ESP8266_STA_MAC') ||
|
||||||
|
!check_header('HTTP_X_ESP8266_SKETCH_SIZE') ||
|
||||||
|
!check_header('HTTP_X_ESP8266_CHIP_SIZE') ) {
|
||||||
|
stop_and_exit(403, "Forbidden: Missing ESP8266 System Headers");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. PROJECT & VERSION EXTRACTION ---
|
// =============================================================
|
||||||
|
// 3. PROJECT & VERSION PROCESSING
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
if (!isset($_SERVER['HTTP_X_ESP_PROJECT']) || !isset($_SERVER['HTTP_X_ESP_VERSION'])) {
|
if (!isset($_SERVER['HTTP_X_ESP_PROJECT']) || !isset($_SERVER['HTTP_X_ESP_VERSION'])) {
|
||||||
stop_and_exit(400, "Bad Request: Missing Project/Version");
|
stop_and_exit(400, "Bad Request: Missing Project/Version metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
$rawProj = trim($_SERVER['HTTP_X_ESP_PROJECT']);
|
$clientProj = trim($_SERVER['HTTP_X_ESP_PROJECT']);
|
||||||
$rawVer = trim($_SERVER['HTTP_X_ESP_VERSION']);
|
$clientVer = trim($_SERVER['HTTP_X_ESP_VERSION']);
|
||||||
|
|
||||||
// Sanitization: Ensure paths/logic aren't broken by special characters
|
// Sanitization for safe filesystem lookups and numeric comparisons
|
||||||
$targetProject = preg_replace("/[^a-zA-Z0-9]/", "-", $rawProj);
|
$targetProject = preg_replace("/[^a-zA-Z0-9]/", "-", $clientProj);
|
||||||
$targetVersion = preg_replace("/[^0-9]/", "", $rawVer);
|
$targetVersion = preg_replace("/[^0-9]/", "", $clientVer);
|
||||||
|
|
||||||
|
log_ota("New Request: " . $targetProject . "." . $clientChipSet . "." . $targetVersion);
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 4. STEPPED UPGRADE SCANNING ENGINE
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
// --- 5. SCANNING LOGIC ---
|
|
||||||
$availableUpdates = [];
|
$availableUpdates = [];
|
||||||
$serverNewestVersion = "0";
|
|
||||||
|
|
||||||
if (is_dir($firmwareBaseDir)) {
|
if (is_dir($firmwareBaseDir)) {
|
||||||
// Filter out linux directory navigation dots
|
|
||||||
$files = array_diff(scandir($firmwareBaseDir), array('.', '..'));
|
$files = array_diff(scandir($firmwareBaseDir), array('.', '..'));
|
||||||
|
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
// Expected format: ProjectName.Chipset.Version.Subversion.bin
|
// Expected structure: [Project].[Chipset].[Version].[SubVer].bin
|
||||||
$parts = explode('.', $file);
|
$parts = explode('.', $file);
|
||||||
if (count($parts) >= 5) {
|
if (count($parts) >= 5 && strcasecmp(trim($parts[4]), "bin") === 0) {
|
||||||
$fProjectRaw = trim($parts[0]);
|
$ProjectRaw = trim($parts[0]);
|
||||||
$fChipsetRaw = trim($parts[1]);
|
$ChipsetRaw = trim($parts[1]);
|
||||||
|
|
||||||
// Reconstruct version as numeric string for comparison (e.g., 20260408001)
|
// Logic Gate: Chipset and Project must match exactly
|
||||||
$fVersion = preg_replace("/[^0-9]/", "", $parts[2]) . preg_replace("/[^0-9]/", "", $parts[3]);
|
if (strcasecmp($ProjectRaw, $targetProject) === 0 &&
|
||||||
|
strcasecmp($ChipsetRaw, $clientChipSet) === 0) {
|
||||||
// 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
|
// Reconstruct version as numeric for direct float comparison (e.g. 1.0 -> 10)
|
||||||
if ((float)$fVersion > (float)$targetVersion) {
|
$VersionRaw = preg_replace("/[^0-9]/", "", $parts[2]) .
|
||||||
$availableUpdates[$fVersion] = $firmwareBaseDir . $file;
|
preg_replace("/[^0-9]/", "", $parts[3]);
|
||||||
|
|
||||||
|
// Stepped Logic: Collect all versions strictly higher than the current device version
|
||||||
|
// strcmp is used for binary-safe string comparison of numeric strings.
|
||||||
|
// log_ota(" compare: '" . $VersionRaw . "' and '" . $targetVersion . "'");
|
||||||
|
|
||||||
|
if (floatval($VersionRaw) > floatval($targetVersion)) {
|
||||||
|
$availableUpdates[$VersionRaw] = $firmwareBaseDir . $file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 6. FINAL DECISION ---
|
|
||||||
|
// =============================================================
|
||||||
|
// 5. FINAL DELIVERY DECISION
|
||||||
|
// =============================================================
|
||||||
if (!empty($availableUpdates)) {
|
if (!empty($availableUpdates)) {
|
||||||
// Sort ascending to find the NEAREST (next) version for a stepped upgrade
|
log_ota(" Candidates: " . (empty($availableUpdates) ? "None" : implode(", ", array_keys($availableUpdates))));
|
||||||
|
// Sort Ascending (ksort): This ensures we serve the 'next' version in the chain,
|
||||||
|
// preventing the device from skipping critical intermediate updates.
|
||||||
ksort($availableUpdates);
|
ksort($availableUpdates);
|
||||||
$nextVer = array_key_first($availableUpdates);
|
$nextVer = array_key_first($availableUpdates);
|
||||||
$targetFile = $availableUpdates[$nextVer];
|
$targetFile = $availableUpdates[$nextVer];
|
||||||
|
|
||||||
sendFile($targetFile, $nextVer);
|
sendFile($targetFile, $nextVer);
|
||||||
} else {
|
} else {
|
||||||
// =========================================================
|
// Client is up to date or no newer version exists for this specific chipset/project
|
||||||
// NO UPDATE FOUND: RETURN 304 NOT MODIFIED
|
|
||||||
// =========================================================
|
|
||||||
header("X-Update-Required: 0");
|
header("X-Update-Required: 0");
|
||||||
header("update: 0");
|
header("update: 0");
|
||||||
header("X-New-Version: " . $rawVer);
|
header("X-New-Version: " . $clientVer);
|
||||||
header("version: 000000000"); // Reset display for client UI if needed
|
header("version: " . $clientVer);
|
||||||
header($_SERVER["SERVER_PROTOCOL"]." 304 Not Modified", true, 304);
|
header($_SERVER["SERVER_PROTOCOL"]." 304 Not Modified", true, 304);
|
||||||
exit();
|
exit();
|
||||||
// =========================================================
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
Loading…
Reference in New Issue
Block a user