createApiAuditService()->startRequestContext(); self::setCorsHeaders(); if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { http_response_code(204); die(); } self::enforceRateLimit(); $authenticated = ApiAuth::authenticate(); if (!$authenticated) { ApiResponse::unauthorized(); } } private static function setCorsHeaders(): void { $rawOrigin = trim((string) ($_SERVER['HTTP_ORIGIN'] ?? '')); if ($rawOrigin === '') { return; } $origin = self::normalizeOrigin($rawOrigin); if ($origin === null) { return; } $allowedOrigins = array_fill_keys(self::settings()->getApiCorsAllowedOrigins(), true); if (!isset($allowedOrigins[$origin])) { return; } header('Vary: Origin'); header('Access-Control-Allow-Origin: ' . $origin); header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Authorization, Content-Type, Accept'); header('Access-Control-Max-Age: 86400'); } private static function registerShutdownHandler(): void { if (self::$shutdownRegistered) { return; } self::$shutdownRegistered = true; register_shutdown_function(static function (): void { $error = error_get_last(); if (!is_array($error)) { $statusCode = (int) http_response_code(); if ($statusCode <= 0) { $statusCode = 200; } \auditServicesFactory()->createApiAuditService()->finish($statusCode); return; } $type = (int) $error['type']; $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; if (!in_array($type, $fatalTypes, true)) { $statusCode = (int) http_response_code(); if ($statusCode <= 0) { $statusCode = 200; } \auditServicesFactory()->createApiAuditService()->finish($statusCode); return; } $statusCode = (int) http_response_code(); if ($statusCode < 400) { $statusCode = 500; } \auditServicesFactory()->createApiAuditService()->finish($statusCode, 'fatal_error'); }); } private static function normalizeOrigin(string $origin): ?string { $origin = rtrim(trim($origin), '/'); if ($origin === '') { return null; } $parsed = parse_url($origin); if (!is_array($parsed)) { return null; } $scheme = strtolower((string) ($parsed['scheme'] ?? '')); $host = strtolower((string) ($parsed['host'] ?? '')); $port = isset($parsed['port']) ? (int) $parsed['port'] : null; if (!in_array($scheme, ['http', 'https'], true) || $host === '') { return null; } if ( isset($parsed['path']) || isset($parsed['query']) || isset($parsed['fragment']) || isset($parsed['user']) || isset($parsed['pass']) ) { return null; } $defaultPort = ($scheme === 'https') ? 443 : 80; $portSuffix = ($port !== null && $port !== $defaultPort) ? ':' . $port : ''; return $scheme . '://' . $host . $portSuffix; } private static function enforceRateLimit(): void { $ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? '')); if ($ip === '') { $ip = 'unknown'; } $ipResult = self::rateLimiter()->hit( self::API_RATE_SCOPE_IP, $ip, self::API_RATE_LIMIT_IP, self::API_RATE_WINDOW_SECONDS, self::API_RATE_BLOCK_SECONDS ); if (!($ipResult['allowed'] ?? true)) { ApiResponse::tooManyRequests(max(1, (int) ($ipResult['retry_after'] ?? self::API_RATE_BLOCK_SECONDS))); } $selector = self::extractBearerSelector(); if ($selector === '') { return; } $tokenResult = self::rateLimiter()->hit( self::API_RATE_SCOPE_TOKEN, strtolower($selector) . '|' . $ip, self::API_RATE_LIMIT_TOKEN, self::API_RATE_WINDOW_SECONDS, self::API_RATE_BLOCK_SECONDS ); if (!($tokenResult['allowed'] ?? true)) { ApiResponse::tooManyRequests(max(1, (int) ($tokenResult['retry_after'] ?? self::API_RATE_BLOCK_SECONDS))); } } private static function extractBearerSelector(): string { $token = ApiAuth::extractBearerToken(); if ($token === '') { return ''; } $parts = explode(':', $token, 2); $selector = trim((string) ($parts[0] ?? '')); if ($selector === '' || !preg_match('/^[a-f0-9]{24}$/i', $selector)) { return ''; } return $selector; } private static function settings(): SettingGateway { if (self::$settingGateway instanceof SettingGateway) { return self::$settingGateway; } if (!(self::$settingServicesFactory instanceof SettingServicesFactory)) { self::$settingServicesFactory = new SettingServicesFactory(); } self::$settingGateway = self::$settingServicesFactory->createSettingGateway(); return self::$settingGateway; } private static function rateLimiter(): RateLimiterService { if (self::$rateLimiterService instanceof RateLimiterService) { return self::$rateLimiterService; } if (!(self::$securityServicesFactory instanceof SecurityServicesFactory)) { self::$securityServicesFactory = new SecurityServicesFactory(); } self::$rateLimiterService = self::$securityServicesFactory->createRateLimiterService(); return self::$rateLimiterService; } }