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
//
// ==============================================================
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";

View File

@ -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)
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();
// --- 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') ) {
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();
// =========================================================
}
?>