diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 8695190..4dc1dad 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -26,6 +26,46 @@ public function getConfigTreeBuilder(): TreeBuilder
})
->end()
->end()
+ ->arrayNode('cors')
+ ->addDefaultsIfNotSet()
+ ->canBeEnabled()
+ ->children()
+ ->arrayNode('allow_origins')
+ ->scalarPrototype()->end()
+ ->beforeNormalization()
+ ->always(function ($value) {
+ return (array) $value;
+ })
+ ->end()
+ ->end()
+ ->variableNode('allow_methods')
+ ->defaultTrue()
+ ->beforeNormalization()
+ ->always(function ($value) {
+ return $value === true ? true : (array) $value;
+ })
+ ->end()
+ ->end()
+ ->variableNode('allow_headers')
+ ->defaultTrue()
+ ->beforeNormalization()
+ ->always(function ($value) {
+ return $value === true ? true : (array) $value;
+ })
+ ->end()
+ ->end()
+ ->variableNode('expose_headers')
+ ->defaultTrue()
+ ->beforeNormalization()
+ ->always(function ($value) {
+ return $value === true ? true : (array) $value;
+ })
+ ->end()
+ ->end()
+ ->booleanNode('allow_credentials')->defaultTrue()->end()
+ ->integerNode('maximum_age')->defaultNull()->end()
+ ->end()
+ ->end()
->arrayNode('limit_default_options')
->children()
->integerNode('default_limit')->end()
diff --git a/DependencyInjection/VanioApiExtension.php b/DependencyInjection/VanioApiExtension.php
index 066e589..3a6e9f3 100644
--- a/DependencyInjection/VanioApiExtension.php
+++ b/DependencyInjection/VanioApiExtension.php
@@ -22,11 +22,12 @@ public function load(array $configs, ContainerBuilder $container): void
$subscribers = [
'format_listener' => 'vanio_api.request.format_listener',
'request_body_listener' => 'vanio_api.request.request_body_listener',
+ 'cors' => 'vanio_api.request.cors_listener',
'access_denied_listener' => 'vanio_api.security.access_denied_listener',
];
foreach ($subscribers as $name => $id) {
- if ($config[$name]) {
+ if ($config[$name] && !isset($config[$name]['enabled']) || $config[$name]['enabled']) {
$container->getDefinition($id)->setAbstract(false)->addTag('kernel.event_subscriber');
}
}
diff --git a/Request/CorsListener.php b/Request/CorsListener.php
new file mode 100644
index 0000000..0da9075
--- /dev/null
+++ b/Request/CorsListener.php
@@ -0,0 +1,199 @@
+allowedOrigins = $allowedOrigins;
+ $this->allowedMethods = $this->allowedMethods === true
+ ? array_map('strtoupper', $allowedMethods)
+ : self::METHODS;
+ $this->allowedHeaders = $allowedHeaders;
+ $this->areCredentialsAllowed = $areCredentialsAllowed;
+ $this->maximumAge = $maximumAge;
+ }
+
+ /**
+ * @return mixed[]
+ */
+ public static function getSubscribedEvents(): array
+ {
+ return [KernelEvents::REQUEST => ['onRequest', 1024]];
+ }
+
+ /**
+ * @internal
+ */
+ public function onRequest(
+ GetResponseEvent $event,
+ string $eventName,
+ EventDispatcherInterface $eventDispatcher
+ ): void {
+ if (!$event->isMasterRequest()) {
+ return;
+ }
+
+ $request = $event->getRequest();
+ $origin = $request->headers->get('Origin');
+
+ if ($origin === null || $origin === $request->getSchemeAndHttpHost()) {
+ return;
+ } elseif ($request->isMethod('OPTIONS')) {
+ $event->setResponse($this->createPreflightResponse($request));
+
+ return;
+ } elseif ($this->checkOrigin($request)) {
+ $eventDispatcher->addListener(KernelEvents::RESPONSE, [$this, 'onResponse']);
+ }
+ }
+
+ /**
+ * @internal
+ */
+ public function onResponse(
+ FilterResponseEvent $event,
+ string $eventName,
+ EventDispatcherInterface $eventDispatcher
+ ): void {
+ if (!$event->isMasterRequest()) {
+ return;
+ }
+
+ $eventDispatcher->removeListener($eventName, [$this, 'onResponse']);
+ $response = $event->getResponse();
+ $response->headers->set('Access-Control-Allow-Origin', $event->getRequest()->headers->get('Origin'));
+
+ if ($this->exposedHeaders) {
+ $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->exposedHeaders));
+ }
+
+ if ($this->areCredentialsAllowed) {
+ $response->headers->set('Access-Control-Allow-Credentials', 'true');
+ }
+ }
+
+ private function createPreflightResponse(Request $request): Response
+ {
+ $response = new Response;
+ $response->headers->set('Content-Type', 'text/plain');
+ $headers = $request->headers->get('Access-Control-Request-Headers');
+
+ if ($allowedMethods = $this->allowedMethods) {
+ $response->headers->set('Access-Control-Allow-Methods', implode(', ', $allowedMethods));
+ }
+
+ if ($this->allowedHeaders) {
+ if ($allowedHeaders = $this->allowedHeaders === true ? $headers : implode(', ', $this->allowedHeaders)) {
+ $response->headers->set('Access-Control-Allow-Headers', $allowedHeaders);
+ }
+ }
+
+ if ($this->areCredentialsAllowed) {
+ $response->headers->set('Access-Control-Allow-Credentials', 'true');
+ }
+
+ if ($this->maximumAge) {
+ $response->headers->set('Access-Control-Max-Age', $this->maximumAge);
+ }
+
+ if (!$this->checkOrigin($request)) {
+ $response->headers->set('Access-Control-Allow-Origin', 'null');
+
+ return $response;
+ }
+
+ $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
+ $method = strtoupper($request->headers->get('Access-Control-Request-Method'));
+
+ if (!in_array($method, $allowedMethods, true)) {
+ return $response->setStatusCode(405);
+ } elseif (!in_array($method, $allowedMethods, true)) {
+ $allowedMethods[] = $method;
+ $response->headers->set('Access-Control-Allow-Methods', implode(', ', $allowedMethods));
+ }
+
+ if ($this->allowedHeaders !== true && $headers) {
+ foreach (preg_split('~, *~', trim(strtolower($headers))) as $header) {
+ if (!in_array($header, self::SIMPLE_HEADERS, true)) {
+ continue;
+ } elseif (!in_array($header, $this->allowedHeaders, true)) {
+ $response
+ ->setStatusCode(400)
+ ->setContent(sprintf('Unauthorized header "%s".', $header));
+ }
+ }
+ }
+
+ return $response;
+ }
+
+ private function checkOrigin(Request $request): bool
+ {
+ if ($this->allowedOrigins === true) {
+ return true;
+ }
+
+ $origin = new Uri($request->headers->get('Origin'));
+
+ foreach ((array) $this->allowedOrigins as $allowedOrigin) {
+ $allowedOrigin = Strings::contains($allowedOrigin, '//')
+ ? $allowedOrigin
+ : sprintf('//%s', $allowedOrigin);
+ $allowedOrigin = new Uri($allowedOrigin);
+
+ if (
+ $origin->host() === $allowedOrigin->host()
+ && (!$allowedOrigin->scheme() || $allowedOrigin->scheme() === $origin->scheme())
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/Resources/config/config.xml b/Resources/config/config.xml
index a3b853b..5e366a7 100644
--- a/Resources/config/config.xml
+++ b/Resources/config/config.xml
@@ -9,6 +9,7 @@
Vanio\ApiBundle\Form\EntityTypeExtension
Vanio\ApiBundle\Request\FormatListener
Vanio\ApiBundle\Request\RequestBodyListener
+ Vanio\ApiBundle\Request\CorsListener
Vanio\ApiBundle\Request\LimitParamConverter
Vanio\ApiBundle\Request\PropertiesParamConverter
Vanio\ApiBundle\Request\FilterParamConverter
@@ -76,6 +77,14 @@
%vanio_api.formats%
+
+ %vanio_api.cors.allow_origins%
+ %vanio_api.cors.allow_methods%
+ %vanio_api.cors.allow_headers%
+ %vanio_api.cors.expose_headers%
+ %vanio_api.cors.allow_credentials%
+
+
%vanio_api.limit_default_options%
diff --git a/Tests/DependencyInjection/VanioApiExtensionTest.php b/Tests/DependencyInjection/VanioApiExtensionTest.php
index 20bb80c..de7d704 100644
--- a/Tests/DependencyInjection/VanioApiExtensionTest.php
+++ b/Tests/DependencyInjection/VanioApiExtensionTest.php
@@ -18,6 +18,15 @@ function test_default_configuration()
'serializer_doctrine_type_mapping' => [],
'api_doc_type_mapping' => [],
'api_doc_request_with_credentials' => false,
+ 'cors' => [
+ 'enabled' => false,
+ 'allow_origins' => [],
+ 'allow_methods' => true,
+ 'allow_headers' => true,
+ 'expose_headers' => true,
+ 'allow_credentials' => true,
+ 'maximum_age' => null,
+ ],
], $config);
}
}
diff --git a/composer.json b/composer.json
index cc3562f..48fac6f 100644
--- a/composer.json
+++ b/composer.json
@@ -12,11 +12,12 @@
"php": "^7.2",
"jms/serializer-bundle": "^2.3.1",
"symfony/framework-bundle": "^3.3.16",
- "symfony/security-bundle": "^3.3.16"
+ "symfony/security-bundle": "^3.3.16",
+ "vanio/stdlib": "^0.1.0"
},
"require-dev": {
- "phpunit/phpunit": "^7.0",
- "vanio/coding-standards": "^0.3"
+ "phpunit/phpunit": "^7.0.0",
+ "vanio/coding-standards": "^0.3.0"
},
"suggest": {
"vanio/vanio-web-bundle": "For API documentation",
diff --git a/composer.lock b/composer.lock
index e6e30c9..b6a0b02 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "dab33ab39fb705a0e3cfa76301edc540",
+ "content-hash": "eab17e0c0211745b59926ca366e16677",
"packages": [
{
"name": "doctrine/annotations",
@@ -1972,6 +1972,67 @@
"description": "Symfony SecurityBundle",
"homepage": "https://symfony.com",
"time": "2018-04-06T07:35:25+00:00"
+ },
+ {
+ "name": "vanio/stdlib",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/vaniocz/stdlib.git",
+ "reference": "1eba532786307b892e2814db32d9e8b2c7cbb9e1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/vaniocz/stdlib/zipball/1eba532786307b892e2814db32d9e8b2c7cbb9e1",
+ "reference": "1eba532786307b892e2814db32d9e8b2c7cbb9e1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.5",
+ "vanio/coding-standards": "^0.1@dev"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Vanio\\Stdlib\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "/tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marek Štípek",
+ "email": "marek.stipek@vanio.cz"
+ },
+ {
+ "name": "Adam Hojka"
+ }
+ ],
+ "description": "General purpose classes extending the PHP language.",
+ "homepage": "https://github.com/vaniocz/stdlib",
+ "keywords": [
+ "enum",
+ "object utility",
+ "objects",
+ "standard library",
+ "string utility",
+ "strings",
+ "uri value object"
+ ],
+ "time": "2017-11-19T13:46:43+00:00"
}
],
"packages-dev": [
@@ -3379,12 +3440,12 @@
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "53a28408d345044c0360c2c1b4a2aaebf4a3b8c9"
+ "reference": "b88026ff826ccbe7b77e8fcd6b9e0ffb77a7a39b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/53a28408d345044c0360c2c1b4a2aaebf4a3b8c9",
- "reference": "53a28408d345044c0360c2c1b4a2aaebf4a3b8c9",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b88026ff826ccbe7b77e8fcd6b9e0ffb77a7a39b",
+ "reference": "b88026ff826ccbe7b77e8fcd6b9e0ffb77a7a39b",
"shasum": ""
},
"require": {
@@ -3422,7 +3483,7 @@
"phpcs",
"standards"
],
- "time": "2018-05-02T05:47:50+00:00"
+ "time": "2018-05-09T04:44:56+00:00"
},
{
"name": "theseer/tokenizer",
@@ -3470,12 +3531,12 @@
"source": {
"type": "git",
"url": "https://github.com/vaniocz/coding-standards.git",
- "reference": "6030ea1ad8b08f3c59cd942deed1810a55fc99c3"
+ "reference": "82db8e7b53e1db57085b858a868c81da98271d43"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vaniocz/coding-standards/zipball/6030ea1ad8b08f3c59cd942deed1810a55fc99c3",
- "reference": "6030ea1ad8b08f3c59cd942deed1810a55fc99c3",
+ "url": "https://api.github.com/repos/vaniocz/coding-standards/zipball/82db8e7b53e1db57085b858a868c81da98271d43",
+ "reference": "82db8e7b53e1db57085b858a868c81da98271d43",
"shasum": ""
},
"require": {
@@ -3512,7 +3573,7 @@
"coding standards",
"conventions"
],
- "time": "2018-05-02T02:17:32+00:00"
+ "time": "2018-05-14T00:56:37+00:00"
},
{
"name": "webmozart/assert",