*/ public static function requiredAllowedOrigins(): array { return self::REQUIRED_ALLOWED_ORIGINS; } /** * @return array */ public static function traefikHeadersMiddlewareLabels(string $middlewareName, string $corsConfig = ''): array { $middlewareName = trim($middlewareName); if ($middlewareName === '' || preg_match('/^[a-zA-Z0-9-]+$/', $middlewareName) !== 1) { return []; } $allowedHeaders = array_values(array_filter( array_map('trim', explode(',', self::ALLOWED_HEADERS)), static fn(string $header): bool => $header !== '' && $header !== '*' )); $allowedMethods = array_values(array_filter(array_map('trim', explode(',', self::ALLOWED_METHODS)))); $exposedHeaders = array_values(array_filter(array_map('trim', explode(',', self::EXPOSED_HEADERS)))); $prefix = "traefik.http.middlewares.{$middlewareName}.headers"; $allowedOrigins = self::allowedOrigins($corsConfig); $originLabel = $allowedOrigins === ['*'] ? "{$prefix}.accesscontrolalloworiginlistregex=^(https?://[^/]+|capacitor://[^/]+)$" : "{$prefix}.accesscontrolalloworiginlist=" . implode(',', $allowedOrigins); return [ "{$prefix}.accesscontrolallowcredentials=true", "{$prefix}.accesscontrolallowheaders=" . implode(',', $allowedHeaders), "{$prefix}.accesscontrolallowmethods=" . implode(',', $allowedMethods), $originLabel, "{$prefix}.accesscontrolexposeheaders=" . implode(',', $exposedHeaders), "{$prefix}.accesscontrolmaxage=" . self::MAX_AGE_SECONDS, "{$prefix}.addvaryheader=true", ]; } /** * @return array */ public static function allowedOrigins(string $corsConfig): array { $origins = []; foreach (self::splitOrigins($corsConfig) as $configuredOrigin) { if ($configuredOrigin === '*') { return ['*']; } $origin = self::normalizeOrigin($configuredOrigin); if ($origin !== '') { $origins[$origin] = true; } } foreach (self::REQUIRED_ALLOWED_ORIGINS as $requiredOrigin) { $origin = self::normalizeOrigin($requiredOrigin); if ($origin !== '') { $origins[$origin] = true; } } return array_keys($origins); } public static function withRequiredOrigins(string $corsConfig): string { $allowedOrigins = self::allowedOrigins($corsConfig); if ($allowedOrigins === ['*']) { return '*'; } return implode(',', $allowedOrigins); } public static function isOriginAllowed(?string $origin, string $corsConfig): bool { $origin = self::normalizeRequestOrigin($origin); if ($origin === '') { return false; } $allowedOrigins = self::allowedOrigins($corsConfig); return in_array('*', $allowedOrigins, true) || in_array($origin, $allowedOrigins, true); } /** * @return array */ public static function responseHeaders(?string $origin, string $corsConfig): array { $origin = self::normalizeRequestOrigin($origin); if ($origin === '' || !self::isOriginAllowed($origin, $corsConfig)) { return []; } return [ 'Access-Control-Allow-Origin' => $origin, 'Access-Control-Allow-Credentials' => 'true', 'Access-Control-Allow-Headers' => self::ALLOWED_HEADERS, 'Access-Control-Allow-Methods' => self::ALLOWED_METHODS, 'Access-Control-Expose-Headers' => self::EXPOSED_HEADERS, 'Access-Control-Max-Age' => self::MAX_AGE_SECONDS, 'Timing-Allow-Origin' => $origin, 'Vary' => 'Origin', ]; } /** * @return array{allowed:bool,status:int,headers:array,body:string} */ public static function preflightResponse(?string $origin, string $corsConfig): array { $headers = self::responseHeaders($origin, $corsConfig); if ($headers === []) { return [ 'allowed' => false, 'status' => 403, 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode(['success' => false, 'message' => 'CORS origin not allowed']) ?: '', ]; } $headers['Content-Type'] = 'application/json'; return [ 'allowed' => true, 'status' => 200, 'headers' => $headers, 'body' => '', ]; } public static function applyResponseHeaders(string $corsConfig, ?string $origin = null): bool { $headers = self::responseHeaders($origin ?? ($_SERVER['HTTP_ORIGIN'] ?? ''), $corsConfig); if ($headers === []) { return false; } self::emitHeaders($headers); return true; } /** * @param array $headers */ public static function emitHeaders(array $headers): void { foreach ($headers as $name => $value) { header($name . ': ' . $value, strtolower((string)$name) !== 'vary'); } } /** * @return array */ private static function splitOrigins(string $corsConfig): array { return array_values(array_filter( array_map('trim', explode(',', $corsConfig)), static fn(string $origin): bool => $origin !== '' )); } }