- gate.php : ETag now derived from size+mtime instead of md5_file($zip). Previously the gate hashed the entire 14 GB ZIP on every HEAD/GET call (including each of 8 parallel segments) → 30s-5min preparation lag for clients before the first byte. Now <1ms per request. - SignManifest : disk-backed cache for SHA-256 keyed by (path,size,mtime). Re-signing 5×14 GB versions used to take ~25 min, now ~1s when nothing changed. New "Force re-hash" toggle in admin to ignore the cache. - versions.php : per-row "🔁 Hash" button to sign a single version, plus a "⚙ Hash" dropdown to toggle hashAlgorithm:none for builds where the user accepts skipping client-side verification (manifest stays signed Ed25519, only the per-ZIP SHA-256 verification is bypassed). - DownloadUrl.php : signed-URL TTL bumped 1h → 6h to cover slow ADSL users who need >1h to finish a 14 GB download. - .gitignore : track server/builds/.htaccess + gate.php (still ignore the actual ZIP/exe binaries). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.2 KiB
PHP
94 lines
3.2 KiB
PHP
<?php
|
|
/**
|
|
* Gardien des téléchargements de builds. Sert un .zip seulement si l'URL est
|
|
* accompagnée d'une signature HMAC valide non expirée, signée par la même
|
|
* clé que /api/download-url/{version}. Supporte les Range: requests pour la
|
|
* reprise (dégradé : on utilise readfile + fseek manuellement).
|
|
*
|
|
* Pour les hébergements où on veut désactiver la protection (tests internes), il
|
|
* suffit de retirer le RewriteRule du .htaccess voisin — Apache servira alors
|
|
* directement le fichier statique avec son support natif des Range.
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
ini_set('display_errors', '0');
|
|
set_time_limit(0);
|
|
ignore_user_abort(true);
|
|
|
|
require __DIR__ . '/../api/lib/Crypto.php';
|
|
$config = require __DIR__ . '/../api/config.php';
|
|
$secret = $config['hmac_secret'] ?? '';
|
|
|
|
function deny(string $reason, int $code = 403): never
|
|
{
|
|
http_response_code($code);
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
echo "Forbidden: $reason\n";
|
|
exit;
|
|
}
|
|
|
|
$file = trim((string)($_GET['file'] ?? ''), '/');
|
|
if ($file === '' || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $file)) deny('invalid file');
|
|
|
|
$path = __DIR__ . '/' . $file;
|
|
if (!is_file($path)) deny('not found', 404);
|
|
|
|
$exp = (int)($_GET['exp'] ?? 0);
|
|
$lic = (string)($_GET['lic'] ?? '');
|
|
$sig = (string)($_GET['sig'] ?? '');
|
|
|
|
if ($exp <= 0 || $lic === '' || $sig === '') deny('missing params');
|
|
if ($exp < time()) deny('expired');
|
|
if ($secret === '') deny('server config error', 500);
|
|
|
|
$relPath = '/builds/' . $file;
|
|
$expected = \PSLauncher\Crypto::hmacHex($relPath . '|' . $exp . '|' . $lic, $secret);
|
|
if (!hash_equals($expected, $sig)) deny('bad signature');
|
|
|
|
// Sert le fichier avec support Range
|
|
$size = filesize($path);
|
|
$mtime = filemtime($path) ?: 0;
|
|
$start = 0;
|
|
$end = $size - 1;
|
|
|
|
header('Content-Type: application/zip');
|
|
header('Accept-Ranges: bytes');
|
|
// ⚠️ NE PAS utiliser md5_file($path) ici : sur un fichier 14 Go, ça relit
|
|
// l'intégralité du ZIP à chaque requête (~30 s-5 min selon disque). Avec
|
|
// 8 segments parallèles + HEAD probe, ça multipliait par 9 le temps avant
|
|
// le premier byte. On utilise un ETag dérivé size+mtime, équivalent à ce
|
|
// qu'Apache fait nativement pour les fichiers statiques. Change quand on
|
|
// upload un nouveau ZIP, donc cache invalidation correcte.
|
|
header('ETag: "' . dechex($size) . '-' . dechex($mtime) . '"');
|
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
|
|
header('Cache-Control: private, max-age=3600');
|
|
|
|
if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) {
|
|
$start = (int)$m[1];
|
|
if ($m[2] !== '') $end = min((int)$m[2], $size - 1);
|
|
if ($start > $end || $start >= $size) {
|
|
http_response_code(416);
|
|
header("Content-Range: bytes */$size");
|
|
exit;
|
|
}
|
|
http_response_code(206);
|
|
header("Content-Range: bytes $start-$end/$size");
|
|
}
|
|
|
|
$length = $end - $start + 1;
|
|
header('Content-Length: ' . $length);
|
|
|
|
$fp = fopen($path, 'rb');
|
|
fseek($fp, $start);
|
|
$bufSize = 1 << 20; // 1 MiB
|
|
$remaining = $length;
|
|
while ($remaining > 0 && !feof($fp) && !connection_aborted()) {
|
|
$chunk = fread($fp, (int)min($bufSize, $remaining));
|
|
if ($chunk === false) break;
|
|
echo $chunk;
|
|
@ob_flush();
|
|
@flush();
|
|
$remaining -= strlen($chunk);
|
|
}
|
|
fclose($fp);
|