Skip to content

Commit

Permalink
Added support for CORS
Browse files Browse the repository at this point in the history
  • Loading branch information
maryo committed May 14, 2018
1 parent e03a3b3 commit 68c8492
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 13 deletions.
40 changes: 40 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion DependencyInjection/VanioApiExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Expand Down
199 changes: 199 additions & 0 deletions Request/CorsListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<?php
namespace Vanio\ApiBundle\Request;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Vanio\Stdlib\Strings;
use Vanio\Stdlib\Uri;

class CorsListener implements EventSubscriberInterface
{
private const SIMPLE_HEADERS = ['accept', 'accept-language', 'content-language', 'origin'];
private const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];

/** @var string[]|bool */
private $allowedOrigins;

/** @var string[]|bool */
private $allowedMethods;

/** @var string[]|bool */
private $allowedHeaders;

/** @var string[]|bool */
private $exposedHeaders;

/** @var bool */
private $areCredentialsAllowed;

/** @var int|null */
private $maximumAge;

/**
* @param string[]|bool $allowedOrigins
* @param string[]|bool $allowedMethods
* @param string[]|bool $allowedHeaders
* @param string[]|bool $exposedHeaders
* @param bool $areCredentialsAllowed
*/
public function __construct(
$allowedOrigins = [],
$allowedMethods = true,
$allowedHeaders = true,
$exposedHeaders = true,
bool $areCredentialsAllowed = true,
?int $maximumAge = null
) {
$this->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;
}
}
9 changes: 9 additions & 0 deletions Resources/config/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<parameter key="vanio_api.form.entity_type_extension.class">Vanio\ApiBundle\Form\EntityTypeExtension</parameter>
<parameter key="vanio_api.request.format_listener.class">Vanio\ApiBundle\Request\FormatListener</parameter>
<parameter key="vanio_api.request.request_body_listener.class">Vanio\ApiBundle\Request\RequestBodyListener</parameter>
<parameter key="vanio_api.request.cors_listener.class">Vanio\ApiBundle\Request\CorsListener</parameter>
<parameter key="vanio_api.request.limit_param_converter.class">Vanio\ApiBundle\Request\LimitParamConverter</parameter>
<parameter key="vanio_api.request.properties_param_converter.class">Vanio\ApiBundle\Request\PropertiesParamConverter</parameter>
<parameter key="vanio_api.request.filter_param_converter.class">Vanio\ApiBundle\Request\FilterParamConverter</parameter>
Expand Down Expand Up @@ -76,6 +77,14 @@
<argument>%vanio_api.formats%</argument>
</service>

<service id="vanio_api.request.cors_listener" class="%vanio_api.request.cors_listener.class%" abstract="true">
<argument>%vanio_api.cors.allow_origins%</argument>
<argument>%vanio_api.cors.allow_methods%</argument>
<argument>%vanio_api.cors.allow_headers%</argument>
<argument>%vanio_api.cors.expose_headers%</argument>
<argument>%vanio_api.cors.allow_credentials%</argument>
</service>

<service id="vanio_api.request.limit_param_converter" class="%vanio_api.request.limit_param_converter.class%">
<argument>%vanio_api.limit_default_options%</argument>
<tag name="request.param_converter" converter="limit"/>
Expand Down
9 changes: 9 additions & 0 deletions Tests/DependencyInjection/VanioApiExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 68c8492

Please sign in to comment.