[FIX] Uge session of debugging and code coverage.
This commit is contained in:
+50
-68
@@ -17,10 +17,6 @@ if (file_exists(__DIR__ . '/.env')) {
|
||||
}
|
||||
}
|
||||
|
||||
// Increase upload limits for this script
|
||||
ini_set('upload_max_filesize', '64M');
|
||||
ini_set('post_max_size', '64M');
|
||||
|
||||
// Security headers
|
||||
header('Content-Type: application/json');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
@@ -29,17 +25,14 @@ header('X-XSS-Protection: 1; mode=block');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||
|
||||
// CORS - Restrict to allowed origins
|
||||
// CORS - only allowlisted origins receive the header (no wildcard fallback)
|
||||
$allowedOrigins = ['http://localhost:5173', 'http://localhost:3000', 'http://localhost:4173', 'https://ohmj2.free.fr', 'https://partitions.c.nadal-fr.com', 'https://ohmj-api.c.nadal-fr.com'];
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
|
||||
// Always send CORS headers for preflight requests
|
||||
if (in_array($origin, $allowedOrigins)) {
|
||||
if (in_array($origin, $allowedOrigins, true)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
} else {
|
||||
// For preflight without matching origin, still allow (for debugging)
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header('Vary: Origin');
|
||||
}
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Authorization, Content-Type');
|
||||
@@ -49,31 +42,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rate limiting function
|
||||
// Normalize the path once (strip an optional /api prefix)
|
||||
$path = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
|
||||
$path = preg_replace('#^api/#', '', $path);
|
||||
|
||||
// Rate limiting (flock-protected to avoid read-modify-write races)
|
||||
function checkRateLimit(string $key, int $maxRequests = 100, int $windowSeconds = 60): bool {
|
||||
$rateFile = sys_get_temp_dir() . '/rate_' . md5($key) . '.json';
|
||||
$now = time();
|
||||
|
||||
$data = ['count' => 0, 'reset' => $now + $windowSeconds];
|
||||
if (file_exists($rateFile)) {
|
||||
$content = file_get_contents($rateFile);
|
||||
$data = json_decode($content, true) ?? $data;
|
||||
|
||||
$fp = @fopen($rateFile, 'c+');
|
||||
if ($fp === false) {
|
||||
return true; // fail open if the temp dir is not writable
|
||||
}
|
||||
|
||||
// Reset if window expired
|
||||
if ($now > $data['reset']) {
|
||||
if (!flock($fp, LOCK_EX)) {
|
||||
fclose($fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
$content = stream_get_contents($fp);
|
||||
$data = json_decode($content, true);
|
||||
if (!is_array($data) || !isset($data['count'], $data['reset']) || $now > $data['reset']) {
|
||||
$data = ['count' => 0, 'reset' => $now + $windowSeconds];
|
||||
}
|
||||
|
||||
|
||||
$data['count']++;
|
||||
file_put_contents($rateFile, json_encode($data));
|
||||
|
||||
rewind($fp);
|
||||
ftruncate($fp, 0);
|
||||
fwrite($fp, json_encode($data));
|
||||
fflush($fp);
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
|
||||
return $data['count'] <= $maxRequests;
|
||||
}
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
|
||||
// Check rate limit for login endpoint (5 attempts per minute)
|
||||
if (preg_match('#^login$#', trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/')) && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
if ($path === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!checkRateLimit('login_' . $ip, 5, 60)) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['error' => 'Too many requests. Please try again later.']);
|
||||
@@ -82,7 +89,6 @@ if (preg_match('#^login$#', trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH
|
||||
}
|
||||
|
||||
// Check general rate limit (500 requests per minute per IP - increased for testing)
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
if (!checkRateLimit('general_' . $ip, 500, 60)) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['error' => 'Rate limit exceeded']);
|
||||
@@ -99,30 +105,19 @@ if ($scoresPath === false || $scoresPath === '') {
|
||||
}
|
||||
$scanner = new ScoreScanner($scoresPath);
|
||||
|
||||
// Get Authorization header
|
||||
// Get Authorization header (PHP exposes it as HTTP_AUTHORIZATION)
|
||||
$token = null;
|
||||
$headers = getallheaders();
|
||||
if (isset($headers['Authorization'])) {
|
||||
$authHeader = $headers['Authorization'];
|
||||
if (preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) {
|
||||
$token = $matches[1];
|
||||
}
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
if (preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) {
|
||||
$token = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Parse request
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$path = trim($uri, '/');
|
||||
|
||||
// Remove 'api/' prefix if present
|
||||
$path = preg_replace('#^api/#', '', $path);
|
||||
|
||||
// Debug: log the path
|
||||
// file_put_contents('/tmp/api_debug.log', date('Y-m-d H:i:s') . " PATH: $path METHOD: $method\n", FILE_APPEND);
|
||||
|
||||
// GET /download/:path - Download PDF (BEFORE auth check)
|
||||
if (preg_match('#^download/([^?]+)#', $path, $matches) && $method === 'GET') {
|
||||
$filePath = urldecode($matches[1]);
|
||||
$filePath = rawurldecode($matches[1]);
|
||||
|
||||
// Security: Check for null bytes (must use double quotes for escape sequence)
|
||||
if (strpos($filePath, "\x00") !== false) {
|
||||
@@ -134,11 +129,10 @@ if (preg_match('#^download/([^?]+)#', $path, $matches) && $method === 'GET') {
|
||||
// Security: Prevent directory traversal
|
||||
$filePath = str_replace(['../', '..\\', '..'], '', $filePath);
|
||||
|
||||
// Check token from header or query parameter
|
||||
$downloadToken = $_GET['token'] ?? $token;
|
||||
$downloadToken = urldecode($downloadToken ?? '');
|
||||
// Security: token is read from the Authorization header only
|
||||
$downloadToken = $token ?? '';
|
||||
|
||||
if ($downloadToken === null || $downloadToken === '') {
|
||||
if ($downloadToken === '') {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Authorization required']);
|
||||
exit;
|
||||
@@ -158,7 +152,12 @@ if (preg_match('#^download/([^?]+)#', $path, $matches) && $method === 'GET') {
|
||||
$realBasePath = realpath($basePath);
|
||||
$realFilePath = realpath($fullPath);
|
||||
|
||||
if ($realFilePath === false || strpos($realFilePath, $realBasePath) !== 0) {
|
||||
if ($realFilePath === false) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'File not found']);
|
||||
exit;
|
||||
}
|
||||
if ($realBasePath === false || strpos($realFilePath, $realBasePath . DIRECTORY_SEPARATOR) !== 0) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Access denied']);
|
||||
exit;
|
||||
@@ -171,12 +170,6 @@ if (preg_match('#^download/([^?]+)#', $path, $matches) && $method === 'GET') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!file_exists($realFilePath) || !is_file($realFilePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'File not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Length: ' . filesize($realFilePath));
|
||||
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
||||
@@ -294,16 +287,9 @@ if ($path === 'admin/scores' && $method === 'POST') {
|
||||
if ($num > $maxId) $maxId = $num;
|
||||
}
|
||||
|
||||
// Skip existing IDs
|
||||
$scoresPath = getenv('SCORES_PATH') ?: __DIR__ . '/../legacy/Scores/';
|
||||
$id = strval($maxId + 1);
|
||||
while (is_dir($scoresPath . $id)) {
|
||||
$id = strval(intval($id) + 1);
|
||||
}
|
||||
// Pad with zeros to 3 digits if needed
|
||||
if (strlen($id) < 3) {
|
||||
$id = str_pad($id, 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
// Next score id, zero-padded to 3 digits.
|
||||
// A concurrent collision is rejected by createScore (already exists).
|
||||
$id = str_pad((string) ($maxId + 1), 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
@@ -330,7 +316,7 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'PUT')
|
||||
|
||||
$name = $input['name'] ?? null;
|
||||
$compositor = $input['compositor'] ?? null;
|
||||
$ressource = $input['ressource'] ?? '';
|
||||
$ressource = $input['ressource'] ?? null;
|
||||
|
||||
$result = $scanner->updateScore($scoreId, $name, $compositor, $ressource);
|
||||
|
||||
@@ -370,11 +356,7 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE
|
||||
if ($result['success']) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
if (strpos($result['error'], 'not found') !== false) {
|
||||
http_response_code(404);
|
||||
} else {
|
||||
http_response_code(400);
|
||||
}
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => $result['error']]);
|
||||
}
|
||||
exit;
|
||||
|
||||
Reference in New Issue
Block a user