tt
This commit is contained in:
parent
fd5298b691
commit
f9745ed01c
2
OTA.cpp
2
OTA.cpp
|
|
@ -17,7 +17,7 @@
|
|||
// OTA
|
||||
//
|
||||
// ==============================================================
|
||||
const char *HC__VERSION = "20250415001";
|
||||
const char *HC__VERSION = "20260415001";
|
||||
#define UPDATE_PORT ((uint16_t) 443)
|
||||
const char *url = "visionsoft.kr";
|
||||
const char *uri = "/sc/pages/firmware_download.php";
|
||||
|
|
|
|||
|
|
@ -1,30 +1,41 @@
|
|||
<?php
|
||||
/**
|
||||
* [firmware_download.php] - CLEAN PRODUCTION VERSION
|
||||
* [firmware_download.php] - UNIFIED STEPPED FIRMWARE SERVER (ESP32 & ESP8266)
|
||||
* --------------------------------------------------------------------------------------------
|
||||
* 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.
|
||||
* - Serves firmware binaries to both ESP32 and ESP8266 using "Stepped" upgrade logic.
|
||||
* - Enforces chipset-specific header validation (MAC, MD5, Sketch/Chip Size).
|
||||
* - Scans './firmware' for naming convention: {Project}.{Chipset}.{Version}.{SubVer}.bin
|
||||
* - Implements security gates: Rejects non-ESP User-Agents and missing hardware headers.
|
||||
* --------------------------------------------------------------------------------------------
|
||||
* Revision History:
|
||||
* 2026.04.08 - [RnD16] Stripped logging for production performance.
|
||||
* 2026.04.08 - [RnD17] Final production locking with detailed inline commentary.
|
||||
* REVISION HISTORY:
|
||||
* 2026.04.08 - [RnD16] Initial production version; stripped logging for speed.
|
||||
* 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);
|
||||
|
||||
// --- 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
|
||||
* Note: PHP converts 'X-Header-Name' to 'HTTP_X_HEADER_NAME'
|
||||
* Validates existence and value of HTTP headers.
|
||||
* Note: PHP converts 'X-Header-Name' to 'HTTP_X_HEADER_NAME'.
|
||||
*/
|
||||
function check_header($name, $value = 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
|
||||
* Sends both standard HTTP headers and custom X-headers for HCUpdate logic
|
||||
* Executes the binary transfer to the MCU with all required metadata headers.
|
||||
*/
|
||||
function sendFile($path, $newVersion) {
|
||||
// =========================================================
|
||||
// MANDATORY HEADERS FOR CLIENT-SIDE HCUpdate.cpp DECISION
|
||||
// =========================================================
|
||||
log_ota(">>> SENDING UPDATE: File: " . basename($path) . " | New Version: $newVersion");
|
||||
|
||||
// Standard protocol headers for UpdateClass decision logic
|
||||
header("X-New-Version: ".$newVersion);
|
||||
header("X-Update-Required: 1");
|
||||
header("version: ". $newVersion); // Legacy support header
|
||||
header("update: 1"); // Legacy support header
|
||||
// =========================================================
|
||||
header("version: ". $newVersion); // Compatibility/Legacy header
|
||||
header("update: 1"); // Compatibility/Legacy 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)
|
||||
// Size is critical for Update.begin() on both ESP32 and ESP8266
|
||||
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);
|
||||
|
||||
// Stream the actual bytes
|
||||
// Clear output buffer and stream file
|
||||
if (ob_get_level()) ob_end_clean();
|
||||
readfile($path);
|
||||
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 = "") {
|
||||
// =========================================================
|
||||
// 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);
|
||||
}
|
||||
echo $headerMsg;
|
||||
exit();
|
||||
}
|
||||
|
||||
// --- 2. CONFIGURATION ---
|
||||
// Local directory where .bin files are stored
|
||||
// =============================================================
|
||||
// 2. CONFIGURATION & IDENTITY VALIDATION
|
||||
// =============================================================
|
||||
|
||||
$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();
|
||||
// --- A. CHIPSET DETECTION VIA USER-AGENT ---
|
||||
$clientChipSet = "";
|
||||
if (check_header('HTTP_USER_AGENT', 'ESP32-http-Update')) {
|
||||
$clientChipSet = "ESP32";
|
||||
} else if (check_header('HTTP_USER_AGENT', 'ESP8266-http-Update')) {
|
||||
$clientChipSet = "ESP8266";
|
||||
} 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 ---
|
||||
// This ensures we only talk to clients using our specific UpdateClass implementation
|
||||
if ($clientChipSet === "ESP32") {
|
||||
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();
|
||||
!check_header('HTTP_X_ESP32_CHIP_SIZE') ) {
|
||||
stop_and_exit(403, "Forbidden: Missing ESP32 System Headers");
|
||||
}
|
||||
} 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'])) {
|
||||
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']);
|
||||
$rawVer = trim($_SERVER['HTTP_X_ESP_VERSION']);
|
||||
$clientProj = trim($_SERVER['HTTP_X_ESP_PROJECT']);
|
||||
$clientVer = 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);
|
||||
// Sanitization for safe filesystem lookups and numeric comparisons
|
||||
$targetProject = preg_replace("/[^a-zA-Z0-9]/", "-", $clientProj);
|
||||
$targetVersion = preg_replace("/[^0-9]/", "", $clientVer);
|
||||
|
||||
log_ota("New Request: " . $targetProject . "." . $clientChipSet . "." . $targetVersion);
|
||||
|
||||
// =============================================================
|
||||
// 4. STEPPED UPGRADE SCANNING ENGINE
|
||||
// =============================================================
|
||||
|
||||
// --- 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
|
||||
// Expected structure: [Project].[Chipset].[Version].[SubVer].bin
|
||||
$parts = explode('.', $file);
|
||||
if (count($parts) >= 5) {
|
||||
$fProjectRaw = trim($parts[0]);
|
||||
$fChipsetRaw = trim($parts[1]);
|
||||
if (count($parts) >= 5 && strcasecmp(trim($parts[4]), "bin") === 0) {
|
||||
$ProjectRaw = trim($parts[0]);
|
||||
$ChipsetRaw = 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]);
|
||||
// Logic Gate: Chipset and Project must match exactly
|
||||
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;
|
||||
// Reconstruct version as numeric for direct float comparison (e.g. 1.0 -> 10)
|
||||
$VersionRaw = preg_replace("/[^0-9]/", "", $parts[2]) .
|
||||
preg_replace("/[^0-9]/", "", $parts[3]);
|
||||
|
||||
// If file version > client version, add to potential candidates
|
||||
if ((float)$fVersion > (float)$targetVersion) {
|
||||
$availableUpdates[$fVersion] = $firmwareBaseDir . $file;
|
||||
// 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)) {
|
||||
// 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);
|
||||
$nextVer = array_key_first($availableUpdates);
|
||||
$targetFile = $availableUpdates[$nextVer];
|
||||
|
||||
sendFile($targetFile, $nextVer);
|
||||
} else {
|
||||
// =========================================================
|
||||
// NO UPDATE FOUND: RETURN 304 NOT MODIFIED
|
||||
// =========================================================
|
||||
// Client is up to date or no newer version exists for this specific chipset/project
|
||||
header("X-Update-Required: 0");
|
||||
header("update: 0");
|
||||
header("X-New-Version: " . $rawVer);
|
||||
header("version: 000000000"); // Reset display for client UI if needed
|
||||
header("X-New-Version: " . $clientVer);
|
||||
header("version: " . $clientVer);
|
||||
header($_SERVER["SERVER_PROTOCOL"]." 304 Not Modified", true, 304);
|
||||
exit();
|
||||
// =========================================================
|
||||
}
|
||||
?>
|
||||
Loading…
Reference in New Issue
Block a user