diff --git a/composer.json b/composer.json index 3c932fb..b4cd5c9 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": "^7.1", + "symfony/polyfill-php80": "^1.16", "topthink/framework": "^6.0" }, "require-dev": { @@ -27,7 +28,6 @@ "HZEX\\Think\\Cors\\": "src" }, "files": [ - "src/helper.php" ] }, "autoload-dev": { diff --git a/src/CorsCore.php b/src/CorsCore.php index 99b0143..fb8af26 100644 --- a/src/CorsCore.php +++ b/src/CorsCore.php @@ -5,6 +5,7 @@ use think\Request; use think\Response; +use function str_starts_with; class CorsCore { @@ -137,6 +138,19 @@ private function getHost(Request $request): string return "{$request->scheme()}://{$request->host()}"; } + /** + * @param Response $response + * @param string $name + * @param string|int $value + */ + private function setHeader(Response $response, string $name, $value): void + { + (function (string $name, string $value) { + /** @noinspection PhpUndefinedFieldInspection */ + $this->header[$name] = $value; + })->call($response, $name, $value); + } + /** * 是否一个Cors请求 * @param Request $request @@ -148,195 +162,175 @@ public function isCorsRequest(Request $request) } /** - * 检查Host是否一致 + * 是否预检请求 * @param Request $request * @return bool */ - public function isSameHost(Request $request): bool + public function isPreflightRequest(Request $request): bool { - return $this->getOrigin($request) === $this->getHost($request); + // Sec-Fetch-Mode: cors + return $request->method(true) === 'OPTIONS' && $request->header('Access-Control-Request-Method'); } - /** - * 检查请求源 - * @param Request $request - * @return bool - */ - public function checkOrigin(Request $request): bool + public function handlePreflightRequest(Request $request): Response { - if ($this->allowedOrigins === true) { - return true; - } + $response = Response::create('', 'html', 204); - $origin = $this->getOrigin($request); + return $this->addPreflightRequestHeaders($response, $request); + } - if (in_array($origin, $this->allowedOrigins)) { - return true; - } + public function addPreflightRequestHeaders(Response $response, Request $request): Response + { + $this->configureAllowedOrigin($response, $request); - foreach ($this->allowedOriginsPatterns as $pattern) { - if (preg_match($pattern, $origin)) { - return true; - } - } + if ($response->getHeader('Access-Control-Allow-Origin')) { + $this->configureAllowCredentials($response, $request); - return false; - } + $this->configureAllowedMethods($response, $request); - /** - * 检查请求方法 - * @param Request $request - * @return bool - */ - public function checkMethod(Request $request): bool - { - if ($this->allowedMethods === true) { - return true; - } + $this->configureAllowedHeaders($response, $request); - $method = strtoupper($request->header('Access-Control-Request-Method', '')); + $this->configureMaxAge($response, $request); + } - return in_array($method, $this->allowedMethods); + return $response; } - /** - * 是否允许来源请求 - * @param Request $request - * @return bool - */ - public function isRequestAllowed(Request $request) + public function addActualRequestHeaders(Response $response, Request $request): Response { - return $this->checkOrigin($request); - } + $this->configureAllowedOrigin($response, $request); - /** - * 是否预检请求 - * @param Request $request - * @return bool - */ - public function isPreflightRequest(Request $request): bool - { - // Sec-Fetch-Mode: cors - return $this->isCorsRequest($request) - && $request->method(true) === 'OPTIONS' - && $request->header('Access-Control-Request-Method'); - } + if ($response->getHeader('Access-Control-Allow-Origin')) { + $this->configureAllowCredentials($response, $request); - /** - * 处理预检请求 - * @param Request $request - * @return Response - */ - public function handlePreflightRequest(Request $request): Response - { - if ($check = $this->checkPreflightRequestConditions($request)) { - return $check; + $this->configureExposedHeaders($response, $request); } - return $this->buildPreflightResponse($request); + return $response; } - /** - * 构建预检响应 - * @param Request $request - * @return Response - */ - public function buildPreflightResponse(Request $request): Response + public function isOriginAllowed(Request $request): bool { - $response = Response::create('', 'html', 204); - - $headers = [ - 'Access-Control-Allow-Origin' => $this->getOrigin($request), - ]; - - if ($this->supportsCredentials) { - $headers['Access-Control-Allow-Credentials'] = 'true'; + if ($this->allowedOrigins === true) { + return true; } - if ($this->maxAge) { - $headers['Access-Control-Max-Age'] = $this->maxAge; + if (!$this->getOrigin($request)) { + return false; } - $headers['Access-Control-Allow-Methods'] = $this->allowedMethods === true - ? strtoupper($request->header('Access-Control-Request-Method', '')) - : implode(', ', $this->allowedMethods); + $origin = $this->getOrigin($request); - $headers['Access-Control-Allow-Headers'] = $this->allowedHeaders === true - ? strtoupper($request->header('Access-Control-Request-Headers')) - : implode(', ', $this->allowedHeaders); + if (in_array($origin, $this->allowedOrigins)) { + return true; + } - $response->header($headers); + foreach ($this->allowedOriginsPatterns as $pattern) { + if (preg_match($pattern, $origin)) { + return true; + } + } - return $response; + return false; } - /** - * 检查预检请求 - * @param Request $request - * @return Response|null - */ - public function checkPreflightRequestConditions(Request $request): ?Response + private function configureAllowedOrigin(Response $response, Request $request) { - if (!$this->checkOrigin($request)) { - return $this->createBadRequestResponse(403, 'Origin not allowed'); + if ($this->allowedOrigins === true && !$this->supportsCredentials) { + // Safe+cacheable, allow everything + $this->setHeader($response, 'Access-Control-Allow-Origin', '*'); + } elseif ($this->isSingleOriginAllowed()) { + // Single origins can be safely set + $this->setHeader($response, 'Access-Control-Allow-Origin', array_values($this->allowedOrigins)[0]); + } else { + // For dynamic headers, check the origin first + if ($this->isOriginAllowed($request)) { + $this->setHeader($response, 'Access-Control-Allow-Origin', $this->getOrigin($request)); + } + + $this->varyHeader($response, 'Origin'); } + } - if (!$this->checkMethod($request)) { - return $this->createBadRequestResponse(405, 'Method not allowed'); + private function isSingleOriginAllowed(): bool + { + if ($this->allowedOrigins === true || !empty($this->allowedOriginsPatterns)) { + return false; } - if ($this->allowedHeaders !== true && $headers = $request->header('Access-Control-Request-Headers')) { - $headers = array_filter(explode(',', strtolower($headers))); + return count($this->allowedOrigins) === 1; + } - foreach ($headers as $header) { - if (!in_array(trim($header), $this->allowedHeaders)) { - return $this->createBadRequestResponse(403, 'Header not allowed'); - } + private function configureAllowedMethods(Response $response, Request $request) + { + if ($this->allowedMethods === true) { + if ($this->supportsCredentials) { + $allowMethods = strtoupper($request->header('Access-Control-Request-Method')); + $this->varyHeader($response, 'Access-Control-Request-Method'); + } else { + $allowMethods = '*'; } + } else { + $allowMethods = implode(', ', $this->allowedMethods); } - return null; + $this->setHeader($response, 'Access-Control-Allow-Methods', $allowMethods); } - /** - * @param Response $response - * @param Request $request - * @return Response - */ - public function addRequestHeaders(Response $response, Request $request): Response + private function configureAllowedHeaders(Response $response, Request $request) { - $headers = [ - 'Access-Control-Allow-Origin' => $this->getOrigin($request), - ]; - - if ($vary = $response->getHeader('Vary')) { - $headers['Vary'] = "{$vary}, Origin"; + if ($this->allowedHeaders === true) { + if ($this->supportsCredentials) { + $allowHeaders = $request->header('Access-Control-Request-Headers'); + $this->varyHeader($response, 'Access-Control-Request-Headers'); + } else { + $allowHeaders = '*'; + } } else { - $headers['Vary'] = 'Origin'; + $allowHeaders = implode(', ', $this->allowedHeaders); } + $this->setHeader($response, 'Access-Control-Allow-Headers', $allowHeaders); + } + private function configureAllowCredentials(Response $response, Request $request) + { if ($this->supportsCredentials) { - $headers['Access-Control-Allow-Credentials'] = 'true'; + $this->setHeader($response, 'Access-Control-Allow-Credentials', 'true'); } + } + private function configureExposedHeaders(Response $response, Request $request) + { if ($this->exposedHeaders) { - $exposedHeaders = array_uintersect( - $this->exposedHeaders, - array_keys($response->getHeader()), - '\strcasecmp' - ); - if ($exposedHeaders) { - $headers['Access-Control-Expose-Headers'] = implode(', ', $exposedHeaders); - } + $this->setHeader($response, 'Access-Control-Expose-Headers', implode(', ', $this->exposedHeaders)); } + } - $response->header($headers); + private function configureMaxAge(Response $response, Request $request) + { + if ($this->maxAge !== null) { + $this->setHeader($response, 'Access-Control-Max-Age', $this->maxAge); + } + } + + public function varyHeader(Response $response, $header): Response + { + if (!$response->getHeader('Vary')) { + $this->setHeader($response, 'Vary', $header); + } elseif (!in_array($header, explode(', ', $response->getHeader('Vary')))) { + $this->setHeader($response, 'Vary', "{$response->getHeader('Vary')}, $header"); + } return $response; } - private function createBadRequestResponse(int $code, string $reason = ''): Response + /** + * 检查Host是否一致 + * @param Request $request + * @return bool + */ + public function isSameHost(Request $request): bool { - return Response::create($reason, 'html', $code); + return $this->getOrigin($request) === $this->getHost($request); } } diff --git a/src/CorsMiddleware.php b/src/CorsMiddleware.php index de587b0..b89f7c8 100644 --- a/src/CorsMiddleware.php +++ b/src/CorsMiddleware.php @@ -40,20 +40,17 @@ public function __construct(Config $config) public function handle(Request $request, Closure $next): Response { if ($this->cors->isPreflightRequest($request)) { - return $this->cors->handlePreflightRequest($request); - } - - if (!$this->cors->isRequestAllowed($request)) { - return Response::create('Not allowed in CORS policy.', 'html', 403); + $response = $this->cors->handlePreflightRequest($request); + return $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } /** @var Response $response */ $response = $next($request); - if ($this->cors->isCorsRequest($request)) { - $this->cors->addRequestHeaders($response, $request); + if ($request->method(true) === 'OPTIONS') { + $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } - return $response; + return $this->cors->addActualRequestHeaders($response, $request); } } diff --git a/src/helper.php b/src/helper.php deleted file mode 100644 index 18b5995..0000000 --- a/src/helper.php +++ /dev/null @@ -1,26 +0,0 @@ -