Skip to content

Commit

Permalink
Merge pull request #2 from NHZEX/v2
Browse files Browse the repository at this point in the history
sync asm89/stack-cors v2.0.0
  • Loading branch information
NHZEX authored May 12, 2020
2 parents 4b8014f + 00053c9 commit 69c171b
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 166 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
],
"require": {
"php": "^7.1",
"symfony/polyfill-php80": "^1.16",
"topthink/framework": "^6.0"
},
"require-dev": {
Expand All @@ -27,7 +28,6 @@
"HZEX\\Think\\Cors\\": "src"
},
"files": [
"src/helper.php"
]
},
"autoload-dev": {
Expand Down
256 changes: 125 additions & 131 deletions src/CorsCore.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use think\Request;
use think\Response;
use function str_starts_with;

class CorsCore
{
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
13 changes: 5 additions & 8 deletions src/CorsMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading

0 comments on commit 69c171b

Please sign in to comment.