8889841cREADME.md000066600000006326150443533630006044 0ustar00# Stack/Cors Library and middleware enabling cross-origin resource sharing for your http-{foundation,kernel} using application. It attempts to implement the [W3C Recommendation] for cross-origin resource sharing. [W3C Recommendation]: http://www.w3.org/TR/cors/ Build status: ![.github/workflows/run-tests.yml](https://github.com/asm89/stack-cors/workflows/.github/workflows/run-tests.yml/badge.svg) ## Installation Require `asm89/stack-cors` using composer. ## Usage This package can be used as a library or as [stack middleware]. [stack middleware]: http://stackphp.com/ ### Options | Option | Description | Default value | |------------------------|------------------------------------------------------------|---------------| | allowedMethods | Matches the request method. | `[]` | | allowedOrigins | Matches the request origin. | `[]` | | allowedOriginsPatterns | Matches the request origin with `preg_match`. | `[]` | | allowedHeaders | Sets the Access-Control-Allow-Headers response header. | `[]` | | exposedHeaders | Sets the Access-Control-Expose-Headers response header. | `false` | | maxAge | Sets the Access-Control-Max-Age response header. | `false` | | supportsCredentials | Sets the Access-Control-Allow-Credentials header. | `false` | The _allowedMethods_ and _allowedHeaders_ options are case-insensitive. You don't need to provide both _allowedOrigins_ and _allowedOriginsPatterns_. If one of the strings passed matches, it is considered a valid origin. If `['*']` is provided to _allowedMethods_, _allowedOrigins_ or _allowedHeaders_ all methods / origins / headers are allowed. ### Example: using the library ```php ['x-allowed-header', 'x-other-allowed-header'], 'allowedMethods' => ['DELETE', 'GET', 'POST', 'PUT'], 'allowedOrigins' => ['http://localhost'], 'allowedOriginsPatterns' => ['/localhost:\d/'], 'exposedHeaders' => false, 'maxAge' => false, 'supportsCredentials' => false, ]); $cors->addActualRequestHeaders(Response $response, $origin); $cors->handlePreflightRequest(Request $request); $cors->isActualRequestAllowed(Request $request); $cors->isCorsRequest(Request $request); $cors->isPreflightRequest(Request $request); ``` ## Example: using the stack middleware ```php ['x-allowed-header', 'x-other-allowed-header'], // you can use ['*'] to allow any methods 'allowedMethods' => ['DELETE', 'GET', 'POST', 'PUT'], // you can use ['*'] to allow requests from any origin 'allowedOrigins' => ['localhost'], // you can enter regexes that are matched to the origin request header 'allowedOriginsPatterns' => ['/localhost:\d/'], 'exposedHeaders' => false, 'maxAge' => false, 'supportsCredentials' => false, ]); ``` src/Cors.php000066600000003365150443533630006773 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Asm89\Stack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; class Cors implements HttpKernelInterface { /** * @var \Symfony\Component\HttpKernel\HttpKernelInterface */ private $app; /** * @var \Asm89\Stack\CorsService */ private $cors; private $defaultOptions = [ 'allowedHeaders' => [], 'allowedMethods' => [], 'allowedOrigins' => [], 'allowedOriginsPatterns' => [], 'exposedHeaders' => [], 'maxAge' => 0, 'supportsCredentials' => false, ]; public function __construct(HttpKernelInterface $app, array $options = []) { $this->app = $app; $this->cors = new CorsService(array_merge($this->defaultOptions, $options)); } public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true): Response { if ($this->cors->isPreflightRequest($request)) { $response = $this->cors->handlePreflightRequest($request); return $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } $response = $this->app->handle($request, $type, $catch); if ($request->getMethod() === 'OPTIONS') { $this->cors->varyHeader($response, 'Access-Control-Request-Method'); } return $this->cors->addActualRequestHeaders($response, $request); } } src/CorsService.php000066600000016177150443533630010321 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Asm89\Stack; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class CorsService { private $options; public function __construct(array $options = []) { $this->options = $this->normalizeOptions($options); } private function normalizeOptions(array $options = []): array { $options += [ 'allowedOrigins' => [], 'allowedOriginsPatterns' => [], 'supportsCredentials' => false, 'allowedHeaders' => [], 'exposedHeaders' => [], 'allowedMethods' => [], 'maxAge' => 0, ]; // normalize array('*') to true if (in_array('*', $options['allowedOrigins'])) { $options['allowedOrigins'] = true; } if (in_array('*', $options['allowedHeaders'])) { $options['allowedHeaders'] = true; } else { $options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']); } if (in_array('*', $options['allowedMethods'])) { $options['allowedMethods'] = true; } else { $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']); } return $options; } /** * @deprecated use isOriginAllowed */ public function isActualRequestAllowed(Request $request): bool { return $this->isOriginAllowed($request); } public function isCorsRequest(Request $request): bool { return $request->headers->has('Origin'); } public function isPreflightRequest(Request $request): bool { return $request->getMethod() === 'OPTIONS' && $request->headers->has('Access-Control-Request-Method'); } public function handlePreflightRequest(Request $request): Response { $response = new Response(); $response->setStatusCode(204); return $this->addPreflightRequestHeaders($response, $request); } public function addPreflightRequestHeaders(Response $response, Request $request): Response { $this->configureAllowedOrigin($response, $request); if ($response->headers->has('Access-Control-Allow-Origin')) { $this->configureAllowCredentials($response, $request); $this->configureAllowedMethods($response, $request); $this->configureAllowedHeaders($response, $request); $this->configureMaxAge($response, $request); } return $response; } public function isOriginAllowed(Request $request): bool { if ($this->options['allowedOrigins'] === true) { return true; } if (!$request->headers->has('Origin')) { return false; } $origin = $request->headers->get('Origin'); if (in_array($origin, $this->options['allowedOrigins'])) { return true; } foreach ($this->options['allowedOriginsPatterns'] as $pattern) { if (preg_match($pattern, $origin)) { return true; } } return false; } public function addActualRequestHeaders(Response $response, Request $request): Response { $this->configureAllowedOrigin($response, $request); if ($response->headers->has('Access-Control-Allow-Origin')) { $this->configureAllowCredentials($response, $request); $this->configureExposedHeaders($response, $request); } return $response; } private function configureAllowedOrigin(Response $response, Request $request) { if ($this->options['allowedOrigins'] === true && !$this->options['supportsCredentials']) { // Safe+cacheable, allow everything $response->headers->set('Access-Control-Allow-Origin', '*'); } elseif ($this->isSingleOriginAllowed()) { // Single origins can be safely set $response->headers->set('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]); } else { // For dynamic headers, set the requested Origin header when set and allowed if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) { $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); } $this->varyHeader($response, 'Origin'); } } private function isSingleOriginAllowed(): bool { if ($this->options['allowedOrigins'] === true || !empty($this->options['allowedOriginsPatterns'])) { return false; } return count($this->options['allowedOrigins']) === 1; } private function configureAllowedMethods(Response $response, Request $request) { if ($this->options['allowedMethods'] === true) { $allowMethods = strtoupper($request->headers->get('Access-Control-Request-Method')); $this->varyHeader($response, 'Access-Control-Request-Method'); } else { $allowMethods = implode(', ', $this->options['allowedMethods']); } $response->headers->set('Access-Control-Allow-Methods', $allowMethods); } private function configureAllowedHeaders(Response $response, Request $request) { if ($this->options['allowedHeaders'] === true) { $allowHeaders = $request->headers->get('Access-Control-Request-Headers'); $this->varyHeader($response, 'Access-Control-Request-Headers'); } else { $allowHeaders = implode(', ', $this->options['allowedHeaders']); } $response->headers->set('Access-Control-Allow-Headers', $allowHeaders); } private function configureAllowCredentials(Response $response, Request $request) { if ($this->options['supportsCredentials']) { $response->headers->set('Access-Control-Allow-Credentials', 'true'); } } private function configureExposedHeaders(Response $response, Request $request) { if ($this->options['exposedHeaders']) { $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders'])); } } private function configureMaxAge(Response $response, Request $request) { if ($this->options['maxAge'] !== null) { $response->headers->set('Access-Control-Max-Age', (int) $this->options['maxAge']); } } public function varyHeader(Response $response, $header): Response { if (!$response->headers->has('Vary')) { $response->headers->set('Vary', $header); } elseif (!in_array($header, explode(', ', $response->headers->get('Vary')))) { $response->headers->set('Vary', $response->headers->get('Vary') . ', ' . $header); } return $response; } private function isSameHost(Request $request): bool { return $request->headers->get('Origin') === $request->getSchemeAndHttpHost(); } } composer.json000066600000002420150443533630007276 0ustar00{ "name": "asm89/stack-cors", "description": "Cross-origin resource sharing library and stack middleware", "keywords": ["stack", "cors"], "homepage": "https://github.com/asm89/stack-cors", "type": "library", "license": "MIT", "authors": [ { "name": "Alexander", "email": "iam.asm89@gmail.com" } ], "require": { "php": "^7.2|^8.0", "symfony/http-foundation": "^4|^5|^6", "symfony/http-kernel": "^4|^5|^6" }, "require-dev": { "phpunit/phpunit": "^7|^9", "squizlabs/php_codesniffer": "^3.5" }, "autoload": { "psr-4": { "Asm89\\Stack\\": "src/" } }, "autoload-dev": { "psr-4": { "Asm89\\Stack\\Tests\\": "tests/" } }, "scripts": { "test": "phpunit", "check-style": "phpcs -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src", "fix-style": "phpcbf -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src" }, "extra": { "branch-alias": { "dev-master": "2.1-dev" } } } LICENSE000066600000002070150443533630005562 0ustar00Copyright (c) 2013-2017 Alexander Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.