diff --git a/OTA.cpp b/OTA.cpp index 646ec6d..6fa3fa3 100644 --- a/OTA.cpp +++ b/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"; diff --git a/src/firmware_download.php b/src/firmware_download.php index 4be30f5..20a1a1b 100644 --- a/src/firmware_download.php +++ b/src/firmware_download.php @@ -1,30 +1,41 @@ >> 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]); - - // 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; + // Logic Gate: Chipset and Project must match exactly + if (strcasecmp($ProjectRaw, $targetProject) === 0 && + strcasecmp($ChipsetRaw, $clientChipSet) === 0) { - // If file version > client version, add to potential candidates - if ((float)$fVersion > (float)$targetVersion) { - $availableUpdates[$fVersion] = $firmwareBaseDir . $file; + // 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]); + + // 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(); - // ========================================================= } ?> \ No newline at end of file