Skip to content

Commit

Permalink
Merge pull request #67 from Oliboy50/feat/forced_allow_origin_value
Browse files Browse the repository at this point in the history
Add an option to force Access-Control-Allow-Origin value
  • Loading branch information
Seldaek committed Dec 30, 2016
2 parents 309bb74 + d7c3ed2 commit 0f73575
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 12 deletions.
10 changes: 10 additions & 0 deletions DependencyInjection/Configuration.php
Expand Up @@ -42,6 +42,7 @@ public function getConfigTreeBuilder()
->append($this->getMaxAge())
->append($this->getHosts())
->append($this->getOriginRegex())
->append($this->getForcedAllowOriginValue())
->end()

->arrayNode('paths')
Expand All @@ -56,6 +57,7 @@ public function getConfigTreeBuilder()
->append($this->getMaxAge())
->append($this->getHosts())
->append($this->getOriginRegex())
->append($this->getForcedAllowOriginValue())
->end()
->end()
;
Expand Down Expand Up @@ -161,4 +163,12 @@ private function getOriginRegex()

return $node;
}

private function getForcedAllowOriginValue()
{
$node = new ScalarNodeDefinition('forced_allow_origin_value');
$node->defaultNull();

return $node;
}
}
29 changes: 18 additions & 11 deletions EventListener/CorsListener.php
Expand Up @@ -37,7 +37,6 @@ class CorsListener
);

protected $dispatcher;
protected $options;

/** @var ResolverInterface */
protected $configurationResolver;
Expand All @@ -61,9 +60,7 @@ public function onKernelRequest(GetResponseEvent $event)
return;
}

$options = $this->configurationResolver->getOptions($request);

if (!$options) {
if (!$options = $this->configurationResolver->getOptions($request)) {
return;
}

Expand All @@ -79,7 +76,6 @@ public function onKernelRequest(GetResponseEvent $event)
}

$this->dispatcher->addListener('kernel.response', array($this, 'onKernelResponse'));
$this->options = $options;
}

public function onKernelResponse(FilterResponseEvent $event)
Expand All @@ -88,15 +84,22 @@ public function onKernelResponse(FilterResponseEvent $event)
return;
}

if (!$options = $this->configurationResolver->getOptions($request = $event->getRequest())) {
return;
}

$response = $event->getResponse();
$request = $event->getRequest();
// add CORS response headers
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
if ($this->options['allow_credentials']) {
$response->headers->set('Access-Control-Allow-Origin',
!empty($options['forced_allow_origin_value'])
? $options['forced_allow_origin_value']
: $request->headers->get('Origin')
);
if ($options['allow_credentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
if ($this->options['expose_headers']) {
$response->headers->set('Access-Control-Expose-Headers', strtolower(implode(', ', $this->options['expose_headers'])));
if ($options['expose_headers']) {
$response->headers->set('Access-Control-Expose-Headers', strtolower(implode(', ', $options['expose_headers'])));
}
}

Expand Down Expand Up @@ -129,7 +132,11 @@ protected function getPreflightResponse(Request $request, array $options)
return $response;
}

$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
$response->headers->set('Access-Control-Allow-Origin',
!empty($options['forced_allow_origin_value'])
? $options['forced_allow_origin_value']
: $request->headers->get('Origin')
);

// check request method
if (!in_array(strtoupper($request->headers->get('Access-Control-Request-Method')), $options['allow_methods'], true)) {
Expand Down
11 changes: 10 additions & 1 deletion README.md
Expand Up @@ -31,7 +31,7 @@ Add the NelmioCorsBundle to your application's kernel:
);
// ...
}
````
```

## Configuration

Expand All @@ -56,6 +56,7 @@ seconds.
max_age: 0
hosts: []
origin_regex: false
forced_allow_origin_value: ~
paths:
'^/api/':
allow_origin: ['*']
Expand All @@ -77,6 +78,14 @@ allowed methods however have to be explicitly listed. `paths` must contain at le
If `origin_regex` is set, `allow_origin` must be a list of regular expressions matching
allowed origins. Remember to use `^` and `$` to clearly define the boundaries of the regex.

By default, the `Access-Control-Allow-Origin` response header value is
the `Origin` request header value (if it matches the rules you've defined with `allow_origin`),
so it should be fine for most of use cases. If it's not, you can override this behavior
by setting the exact value you want using `forced_allow_origin_value`.

Be aware that even if you set `forced_allow_origin_value` to `*`, if you also set `allow_origin` to `http://example.com`,
only this specific domain will be allowed to access your resources.

> **Note:** If you allow POST methods and have
> [HTTP method overriding](http://symfony.com/doc/current/reference/configuration/framework.html#http-method-override)
> enabled in the framework, it will enable the API users to perform PUT and DELETE
Expand Down
85 changes: 85 additions & 0 deletions Tests/CorsListenerTest.php
Expand Up @@ -38,6 +38,7 @@ public function getListener($dispatcher, array $options = array())
'max_age' => 0,
'hosts' => array(),
'origin_regex' => false,
'forced_allow_origin_value' => null,
),
$options
);
Expand Down Expand Up @@ -120,6 +121,51 @@ public function testPreflightedRequestLinkFirefox()
$this->assertEquals('LINK, PUT, Link', $resp->headers->get('Access-Control-Allow-Methods'));
}

public function testPreflightedRequestWithForcedAllowOriginValue()
{
// allow_origin matches origin header
// => 'Access-Control-Allow-Origin' should be equal to "forced_allow_origin_value" (i.e. '*')
$options = array(
'allow_origin' => array(true),
'allow_methods' => array('GET'),
'forced_allow_origin_value' => '*',
);

$req = Request::create('/foo', 'OPTIONS');
$req->headers->set('Origin', 'http://example.com');
$req->headers->set('Access-Control-Request-Method', 'GET');

$dispatcher = m::mock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$event = new GetResponseEvent(m::mock('Symfony\Component\HttpKernel\HttpKernelInterface'), $req, HttpKernelInterface::MASTER_REQUEST);
$this->getListener($dispatcher, $options)->onKernelRequest($event);
$resp = $event->getResponse();
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $resp);
$this->assertEquals(200, $resp->getStatusCode());
$this->assertEquals('*', $resp->headers->get('Access-Control-Allow-Origin'));
$this->assertEquals('GET', $resp->headers->get('Access-Control-Allow-Methods'));

// allow_origin does not match origin header
// => 'Access-Control-Allow-Origin' should be 'null'
$options = array(
'allow_origin' => array(),
'allow_methods' => array('GET'),
'forced_allow_origin_value' => '*',
);

$req = Request::create('/foo', 'OPTIONS');
$req->headers->set('Origin', 'http://example.com');
$req->headers->set('Access-Control-Request-Method', 'GET');

$dispatcher = m::mock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$event = new GetResponseEvent(m::mock('Symfony\Component\HttpKernel\HttpKernelInterface'), $req, HttpKernelInterface::MASTER_REQUEST);
$this->getListener($dispatcher, $options)->onKernelRequest($event);
$resp = $event->getResponse();
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $resp);
$this->assertEquals(200, $resp->getStatusCode());
$this->assertEquals('null', $resp->headers->get('Access-Control-Allow-Origin'));
$this->assertEquals('GET', $resp->headers->get('Access-Control-Allow-Methods'));
}

public function testSameHostRequest()
{
// Request with same host as origin
Expand Down Expand Up @@ -160,4 +206,43 @@ public function testRequestWithOriginButNo()

$this->assertNull($event->getResponse());
}

public function testRequestWithForcedAllowOriginValue()
{
// allow_origin matches origin header
// => 'Access-Control-Allow-Origin' should be equal to "forced_allow_origin_value" (i.e. 'http://example.com http://huh-lala.foobar')
$options = array(
'allow_origin' => array('http://example.com'),
'allow_methods' => array('GET'),
'forced_allow_origin_value' => 'http://example.com http://huh-lala.foobar',
);

$req = Request::create('/foo', 'GET');
$req->headers->set('Origin', 'http://example.com');

$dispatcher = m::mock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$dispatcher->shouldReceive('addListener')->once()->with('kernel.response', m::type('callable'));

$event = new GetResponseEvent(m::mock('Symfony\Component\HttpKernel\HttpKernelInterface'), $req, HttpKernelInterface::MASTER_REQUEST);
$this->getListener($dispatcher, $options)->onKernelRequest($event);
$event = new FilterResponseEvent(m::mock('Symfony\Component\HttpKernel\HttpKernelInterface'), $req, HttpKernelInterface::MASTER_REQUEST, new Response());
$this->getListener($dispatcher, $options)->onKernelResponse($event);
$resp = $event->getResponse();
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $resp);
$this->assertEquals(200, $resp->getStatusCode());
$this->assertEquals('http://example.com http://huh-lala.foobar', $resp->headers->get('Access-Control-Allow-Origin'));

// allow_origin does not match origin header
// => CorsListener should not interfere with the response
$req = Request::create('/foo', 'GET');
$req->headers->set('Origin', 'http://evil.com');

$dispatcher = m::mock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$dispatcher->shouldReceive('addListener')->times(0);

$event = new GetResponseEvent(m::mock('Symfony\Component\HttpKernel\HttpKernelInterface'), $req, HttpKernelInterface::MASTER_REQUEST);
$this->getListener($dispatcher, $options)->onKernelRequest($event);

$this->assertNull($event->getResponse());
}
}

0 comments on commit 0f73575

Please sign in to comment.