From bc834169f0ffcd4a98038f957ff2d5c3392cf544 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Tue, 2 Aug 2016 11:56:38 +0100 Subject: [PATCH] Issue #1869548 by dawehner, jeqq, dmouse, slasher13, damiankloip, alexpott, nod_, AlxVallejo, chr.fritsch, yanniboi, effulgentsia, dixon_, -enzo-, Wim Leers, Crell, andypost, catch, Damien Tournoud, kim.pepper, linclark: Opt-in CORS support --- composer.lock | 43 ++++++++++ core/composer.json | 3 +- core/core.services.yml | 13 +++ core/lib/Drupal/Core/CoreServiceProvider.php | 3 + .../Compiler/CorsCompilerPass.php | 31 +++++++ .../Core/HttpKernel/CorsIntegrationTest.php | 84 +++++++++++++++++++ sites/default/default.services.yml | 19 +++++ 7 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php create mode 100644 core/tests/Drupal/KernelTests/Core/HttpKernel/CorsIntegrationTest.php diff --git a/composer.lock b/composer.lock index 132fba518826..8d2b3259d443 100644 --- a/composer.lock +++ b/composer.lock @@ -7,6 +7,49 @@ "hash": "7d101b08e5ae002d827cd42ae9a4e344", "content-hash": "60f7057617c6d995bf9946d0b12f0b5d", "packages": [ + { + "name": "asm89/stack-cors", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/asm89/stack-cors.git", + "reference": "3ae8ef219bb4c9a6caf857421719aa07fa7776cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asm89/stack-cors/zipball/3ae8ef219bb4c9a6caf857421719aa07fa7776cc", + "reference": "3ae8ef219bb4c9a6caf857421719aa07fa7776cc", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/http-foundation": "~2.1|~3.0", + "symfony/http-kernel": "~2.1|~3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Asm89\\Stack": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander", + "email": "iam.asm89@gmail.com" + } + ], + "description": "Cross-origin resource sharing library and stack middleware", + "homepage": "https://github.com/asm89/stack-cors", + "keywords": [ + "cors", + "stack" + ], + "time": "2016-08-01 12:05:04" + }, { "name": "composer/installers", "version": "v1.0.21", diff --git a/core/composer.json b/core/composer.json index 310964982c64..9ac5ab02d9ec 100644 --- a/core/composer.json +++ b/core/composer.json @@ -31,7 +31,8 @@ "symfony/psr-http-message-bridge": "v0.2", "zendframework/zend-diactoros": "~1.1", "composer/semver": "~1.0", - "paragonie/random_compat": "~1.0" + "paragonie/random_compat": "~1.0", + "asm89/stack-cors": "~1.0" }, "require-dev": { "behat/mink": "~1.7", diff --git a/core/core.services.yml b/core/core.services.yml index d90aebaa8b9e..6310c211b55e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -33,6 +33,14 @@ parameters: - sftp - webcal - rtsp + cors.config: + enabled: false + allowedHeaders: [] + allowedMethods: [] + allowedOrigins: ['*'] + exposedHeaders: false + maxAge: false + supportsCredentials: false services: # Simple cache contexts, directly derived from the request context. cache_context.ip: @@ -706,6 +714,11 @@ services: - { name: http_middleware, priority: 50 } calls: - [setContainer, ['@service_container']] + http_middleware.cors: + class: Asm89\Stack\Cors + arguments: ['%cors.config%'] + tags: + - { name: http_middleware } psr7.http_foundation_factory: class: Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory psr7.http_message_factory: diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 013dec14d9b2..8a22bc50d4b6 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -6,6 +6,7 @@ use Drupal\Core\Cache\ListCacheBinsPass; use Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass; use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass; +use Drupal\Core\DependencyInjection\Compiler\CorsCompilerPass; use Drupal\Core\DependencyInjection\Compiler\GuzzleMiddlewarePass; use Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass; use Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass; @@ -64,6 +65,8 @@ public function register(ContainerBuilder $container) { $container->addCompilerPass(new BackendCompilerPass()); + $container->addCompilerPass(new CorsCompilerPass()); + $container->addCompilerPass(new StackedKernelPass()); $container->addCompilerPass(new StackedSessionHandlerPass()); diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php new file mode 100644 index 000000000000..207e094094f4 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php @@ -0,0 +1,31 @@ +<?php + +namespace Drupal\Core\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Provides a compiler pass which disables the CORS middleware in case disabled. + * + * @see core.services.yml + */ +class CorsCompilerPass implements CompilerPassInterface { + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) { + $enabled = FALSE; + + if ($cors_config = $container->getParameter('cors.config')) { + $enabled = !empty($cors_config['enabled']); + } + + // Remove the CORS middleware completly in case it was not enabled. + if (!$enabled) { + $container->removeDefinition('http_middleware.cors'); + } + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/HttpKernel/CorsIntegrationTest.php b/core/tests/Drupal/KernelTests/Core/HttpKernel/CorsIntegrationTest.php new file mode 100644 index 000000000000..e73efa3b74f0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/HttpKernel/CorsIntegrationTest.php @@ -0,0 +1,84 @@ +<?php + +namespace Drupal\KernelTests\Core\HttpKernel; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DependencyInjection\ServiceModifierInterface; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Tests CORS provided by Drupal. + * + * @see sites/default/default.services.yml + * @see \Asm89\Stack\Cors + * @see \Asm89\Stack\CorsService + * + * @group Http + */ +class CorsIntegrationTest extends KernelTestBase implements ServiceModifierInterface { + + /** + * The cors container configuration. + * + * @var null|array + */ + protected $corsConfig = NULL; + + /** + * {@inheritdoc} + */ + public static $modules = ['system', 'test_page_test']; + + protected function setUp() { + parent::setUp(); + + $this->installSchema('system', 'router'); + \Drupal::service('router.builder')->rebuild(); + } + + public function testCrossSiteRequest() { + + // Test default parameters. + $cors_config = $this->container->getParameter('cors.config'); + $this->assertSame(FALSE, $cors_config['enabled']); + $this->assertSame([], $cors_config['allowedHeaders']); + $this->assertSame([], $cors_config['allowedMethods']); + $this->assertSame(['*'], $cors_config['allowedOrigins']); + + $this->assertSame(FALSE, $cors_config['exposedHeaders']); + $this->assertSame(FALSE, $cors_config['maxAge']); + $this->assertSame(FALSE, $cors_config['supportsCredentials']); + + // Configure the CORS stack to allow a specific set of origins, but don't + // specify an origin header. + $request = Request::create('/test-page'); + $request->headers->set('Origin', ''); + $cors_config['enabled'] = TRUE; + $cors_config['allowedOrigins'] = ['http://example.com']; + + $this->corsConfig = $cors_config; + $this->container->get('kernel')->rebuildContainer(); + + /** @var \Symfony\Component\HttpFoundation\Response $response */ + $response = $this->container->get('http_kernel')->handle($request); + $this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode()); + $this->assertEquals('Not allowed.', $response->getContent()); + + // Specify a valid origin. + $request->headers->set('Origin', 'http://example.com'); + $response = $this->container->get('http_kernel')->handle($request); + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + } + + /** + * {@inheritdoc} + */ + public function alter(ContainerBuilder $container) { + if (isset($this->corsConfig)) { + $container->setParameter('cors.config', $this->corsConfig); + } + } + +} diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml index 23f6483cc72f..e1bbbc7e21f0 100644 --- a/sites/default/default.services.yml +++ b/sites/default/default.services.yml @@ -153,3 +153,22 @@ parameters: - sftp - webcal - rtsp + + # Configure Cross-Site HTTP requests (CORS). + # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS + # for more information about the topic in general. + # Note: By default the configuration is disabled. + cors.config: + enabled: false + # Specify allowed headers, like 'x-allowed-header'. + allowedHeaders: [] + # Specify allowed request methods, specify ['*'] to allow all possible ones. + allowedMethods: [] + # Configure requests allowed from specific origins. + allowedOrigins: ['*'] + # Sets the Access-Control-Expose-Headers header. + exposedHeaders: false + # Sets the Access-Control-Max-Age header. + maxAge: false + # Sets the Access-Control-Allow-Credentials header. + supportsCredentials: false -- GitLab