From c501115612bf753497617a035cf5584b39b5551a Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 1 May 2026 09:07:22 +0200 Subject: [PATCH] Server: portable rewrite + JSON-only error handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous .htaccess used PATH_INFO (api/index.php/$1) which OVH mutualisé does not always allow, producing an Apache 500 HTML page that masked our own JSON error reporting. Switch to query-string routing (?route=...): same effect, works everywhere. Add a global exception handler in index.php that emits JSON errors only — no more opaque Apache 500. Add a /api/debug endpoint that reports PHP version, sodium availability, pdo_mysql, and whether versions.json is found. Useful for diagnosing shared-hosting setup before adding license logic. Co-Authored-By: Claude Opus 4.7 (1M context) --- server/.htaccess | 25 +++-------- server/api/index.php | 99 +++++++++++++++++++++++++++----------------- 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/server/.htaccess b/server/.htaccess index 9a66488..403e897 100644 --- a/server/.htaccess +++ b/server/.htaccess @@ -1,28 +1,15 @@ RewriteEngine On RewriteBase /PS_Launcher/ -# HTTPS forcé +# HTTPS forcé (si le mutualisé OVH ne le force pas déjà) RewriteCond %{HTTPS} off RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] # Pas de listing de répertoires Options -Indexes -# /PS_Launcher/api/* → api/index.php (le PATH_INFO est exposé via QUERY_STRING) -RewriteRule ^api/?$ api/index.php [QSA,L] -RewriteRule ^api/(.*)$ api/index.php/$1 [QSA,L] - -# Bloquer l'accès direct aux fichiers sensibles - - Require all denied - - -# Headers de sécurité - - Header set X-Content-Type-Options "nosniff" - Header set X-Frame-Options "DENY" - Header set Referrer-Policy "no-referrer" - - -# Type MIME pour les ZIPs (assure la pris en compte de Range:) -AddType application/zip .zip +# /PS_Launcher/api -> api/index.php +# /PS_Launcher/api/foo -> api/index.php?route=foo +# (passage par query string : pas de PATH_INFO, marche partout) +RewriteRule ^api/?$ api/index.php [QSA,L] +RewriteRule ^api/([^?]+)/?$ api/index.php?route=$1 [QSA,L] diff --git a/server/api/index.php b/server/api/index.php index 16b40ec..2b62eb1 100644 --- a/server/api/index.php +++ b/server/api/index.php @@ -1,52 +1,77 @@ getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); + echo json_encode([ + 'error' => 'server_error', + 'message' => $e->getMessage(), + 'file' => basename($e->getFile()), + 'line' => $e->getLine(), + ]); + exit; +}); + require __DIR__ . '/lib/Response.php'; use PSLauncher\Response; -// Charge la config (hors git ; copier config.example.php → config.php au déploiement) +// Charge la config (config.example.php est le template ; config.php est gitignored) $configPath = __DIR__ . '/config.php'; if (!is_file($configPath)) { - http_response_code(500); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode(['error' => 'config_missing', 'message' => 'config.php absent — copier config.example.php']); - exit; + Response::error('config_missing', 'config.php absent — copier config.example.php', 500); } $config = require $configPath; -// Le path est passé via REQUEST_URI parce que le RewriteRule utilise PATH_INFO -$uri = $_SERVER['REQUEST_URI'] ?? '/'; -$path = parse_url($uri, PHP_URL_PATH) ?? '/'; - -// Strip prefix /PS_Launcher/api -if (preg_match('#^.*?/PS_Launcher/api(.*)$#', $path, $m)) { - $route = trim($m[1], '/'); -} else { - $route = trim($path, '/'); -} - +// La route est passée via ?route=... par le .htaccess +$route = trim((string)($_GET['route'] ?? ''), '/'); $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; -try { - if ($method === 'GET' && ($route === '' || $route === 'manifest')) { - require __DIR__ . '/routes/Manifest.php'; - \PSLauncher\Routes\Manifest::handle($config); - return; - } - - if ($method === 'GET' && preg_match('#^releasenotes/([0-9]+\.[0-9]+\.[0-9]+)$#', $route, $m)) { - require __DIR__ . '/routes/Releasenotes.php'; - \PSLauncher\Routes\Releasenotes::handle($m[1]); - return; - } - - if ($method === 'GET' && $route === 'health') { - Response::json(['status' => 'ok', 'serverTime' => gmdate('c')]); - } - - Response::error('not_found', "Route not found: {$route}", 404); -} catch (\Throwable $e) { - error_log('[PSLauncher] ' . $e->getMessage() . "\n" . $e->getTraceAsString()); - Response::error('server_error', 'Internal server error', 500); +if ($method === 'GET' && ($route === '' || $route === 'manifest')) { + require __DIR__ . '/routes/Manifest.php'; + \PSLauncher\Routes\Manifest::handle($config); + return; } + +if ($method === 'GET' && preg_match('#^releasenotes/([0-9]+\.[0-9]+\.[0-9]+)$#', $route, $m)) { + require __DIR__ . '/routes/Releasenotes.php'; + \PSLauncher\Routes\Releasenotes::handle($m[1]); + return; +} + +if ($method === 'GET' && $route === 'health') { + Response::json([ + 'status' => 'ok', + 'serverTime' => gmdate('c'), + 'phpVersion' => PHP_VERSION, + 'configLoaded' => is_array($config), + ]); +} + +if ($method === 'GET' && $route === 'debug') { + Response::json([ + 'status' => 'ok', + 'route' => $route, + 'request_uri' => $_SERVER['REQUEST_URI'] ?? '', + 'script_name' => $_SERVER['SCRIPT_NAME'] ?? '', + 'php' => PHP_VERSION, + 'sodium' => function_exists('sodium_crypto_sign_detached'), + 'pdo_mysql' => extension_loaded('pdo_mysql'), + 'manifest_file' => is_file(dirname(__DIR__) . '/manifest/versions.json'), + ]); +} + +Response::error('not_found', "Route not found: '{$route}'", 404);