Files
PS_Launcher/server/builds/gate.php
j.foucher 962f5a8ce0 Server: faster releases (cache hashes), per-version skip, longer signed-URL TTL
- 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>
2026-05-02 19:14:32 +02:00

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);