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>
This commit is contained in:
9
server/builds/.htaccess
Normal file
9
server/builds/.htaccess
Normal file
@@ -0,0 +1,9 @@
|
||||
# Tout accès direct à un .zip passe d'abord par le gardien gate.php qui vérifie
|
||||
# la signature HMAC + l'expiration. Si OK, le gardien sert le fichier (avec
|
||||
# support Range pour la reprise). Sinon, 403.
|
||||
RewriteEngine On
|
||||
RewriteBase /PS_Launcher/builds/
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{REQUEST_URI} \.zip$
|
||||
RewriteRule ^(.*)$ gate.php?file=$1 [QSA,L]
|
||||
93
server/builds/gate.php
Normal file
93
server/builds/gate.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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);
|
||||
Reference in New Issue
Block a user