This commit is contained in:
Heuideog Yi @ PC RnD1 2026-04-16 08:04:46 +09:00
parent fd5298b691
commit f9745ed01c
2 changed files with 110 additions and 84 deletions

View File

@ -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";

View File

@ -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 // Reconstruct version as numeric for direct float comparison (e.g. 1.0 -> 10)
if (strcasecmp($fProjectRaw, $targetProject) === 0 && strcasecmp($fChipsetRaw, 'ESP32') === 0) { $VersionRaw = preg_replace("/[^0-9]/", "", $parts[2]) .
if ((float)$fVersion > (float)$serverNewestVersion) $serverNewestVersion = $fVersion; preg_replace("/[^0-9]/", "", $parts[3]);
// If file version > client version, add to potential candidates // Stepped Logic: Collect all versions strictly higher than the current device version
if ((float)$fVersion > (float)$targetVersion) { // strcmp is used for binary-safe string comparison of numeric strings.
$availableUpdates[$fVersion] = $firmwareBaseDir . $file; // 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();
// =========================================================
} }
?> ?>