Skip to content

Commit

Permalink
Merge branch 'feature/43'
Browse files Browse the repository at this point in the history
Close #43
Fixes #9
  • Loading branch information
weierophinney committed Jun 1, 2017
2 parents 13ea806 + 8edaaa1 commit f68543c
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 17 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 1.5.0

- You may now configure rules per-route within zend-mvc route configuration.
When detected, these will override any rules that were general to the
application. See the ["Configuring the Module" section of the
README](README.md#configuring-the-module) for full details.

# 1.4.1

- ZfrCors now properly disallows `Access-Control-Allow-Origin: *` when the
Expand Down
101 changes: 85 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,91 @@ of -1. This means that this listener is executed AFTER the route has been matche

### Configuring the module

As of now, all the various options are set globally for all routes:

* `allowed_origins`: (array) List of allowed origins. To allow any origin, you can use the wildcard (`*`) character. If
multiple origins are specified, ZfrCors will automatically check the `"Origin"` header's value, and only return the
allowed domain (if any) in the `"Allow-Access-Control-Origin"` response header. To allow any sub-domain, you can prefix
the domain with the wildcard character (i.e. *.example.com). Please note that you don't need to
add your host URI (so if your website is hosted as "example.com", "example.com" is automatically allowed.
* `allowed_methods`: (array) List of allowed HTTP methods. Those methods will be returned for the preflight request to
indicate which methods are allowed to the user agent. You can even specify custom HTTP verbs.
* `allowed_headers`: (array) List of allowed headers that will be returned for the preflight request. This indicates
to the user agent which headers are permitted to be sent when doing the actual request.
* `max_age`: (int) Maximum age (seconds) the preflight request should be cached by the user agent. This prevents the
user agent from sending a preflight request for each request.
* `exposed_headers`: (array) List of response headers that are allowed to be read in the user agent. Please note that
some browsers do not implement this feature correctly.
* `allowed_credentials`: (boolean) If true, it allows the browser to send cookies along with the request.
As by default, all the various options are set globally for all routes:

- `allowed_origins`: (array) List of allowed origins. To allow any origin, you can use the wildcard (`*`) character. If
multiple origins are specified, ZfrCors will automatically check the `"Origin"` header's value, and only return the
allowed domain (if any) in the `"Allow-Access-Control-Origin"` response header. To allow any sub-domain, you can prefix
the domain with the wildcard character (i.e. `*.example.com`). Please note that you don't need to
add your host URI (so if your website is hosted as "example.com", "example.com" is automatically allowed.
- `allowed_methods`: (array) List of allowed HTTP methods. Those methods will be returned for the preflight request to
indicate which methods are allowed to the user agent. You can even specify custom HTTP verbs.
- `allowed_headers`: (array) List of allowed headers that will be returned for the preflight request. This indicates
to the user agent which headers are permitted to be sent when doing the actual request.
- `max_age`: (int) Maximum age (seconds) the preflight request should be cached by the user agent. This prevents the
user agent from sending a preflight request for each request.
- `exposed_headers`: (array) List of response headers that are allowed to be read in the user agent. Please note that
some browsers do not implement this feature correctly.
- `allowed_credentials`: (boolean) If true, it allows the browser to send cookies along with the request.

If you want to configure specific routes, you can add `ZfrCors\Options\CorsOptions::ROUTE_PARAM` to your route configuration:

```php
<?php

return [
'zfr_cors' => [
'allowed_origins' => ['*'],
'allowed_methods' => ['GET', 'POST', 'DELETE'],
],
'router' => [
'routes' => [
'readOnlyRoute' => [
'type' => 'literal',
'options' => [
'route' => '/foo/bar',
'defaults' => [
// This will replace allowed_methods configuration to only allow GET requests
// and only allow a specific origin instead of the wildcard origin
ZfrCors\Options\CorsOptions::ROUTE_PARAM => [
'allowed_origins' => ['http://example.org'],
'allowed_methods' => ['GET'],
],
],
],
],
'someAjaxCalls' => [
'type' => 'literal',
'options' => [
'route' => '/ajax',
'defaults' => [
// This overrides the wildcard origin
ZfrCors\Options\CorsOptions::ROUTE_PARAM => [
'allowed_origins' => ['http://example.org'],
],
],
],
'may_terminate' => false,
'child_routes' => [
'blog' => [
'type' => 'literal',
'options' => [
'route' => '/blogpost',
'defaults' => [
// This would only allow `http://example.org` to GET this route
'allowed_methods' => ['GET'],
],
],
'may_terminate' => true,
'child_routes' => [
'delete' => [
'type' => 'segment',
'options' => [
'route' => ':id',
// This would only allow origin `http://example.org` to apply DELETE on this route
'defaults' => [
'allowed_methods' => ['DELETE'],
],
],
],
],
],
],
],
],
],
];
```

### Preflight request

Expand Down
2 changes: 1 addition & 1 deletion src/ZfrCors/Mvc/CorsRequestListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public function onCorsPreflight(MvcEvent $event)
// Preflight -- return a response now!
$this->isPreflight = true;

return $this->corsService->createPreflightCorsResponse($request);
return $this->corsService->createPreflightCorsResponseWithRouteOptions($request, $event->getRouteMatch());
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/ZfrCors/Options/CorsOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
*/
class CorsOptions extends AbstractOptions
{
const ROUTE_PARAM = 'cors';

/**
* Set the list of allowed origins domain with protocol.
*
Expand Down
21 changes: 21 additions & 0 deletions src/ZfrCors/Service/CorsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

namespace ZfrCors\Service;

use Zend\Mvc\Router\Http\RouteMatch as DeprecatedRouteMatch;
use Zend\Router\Http\RouteMatch;
use Zend\Http\Header;
use Zend\Uri\UriFactory;
use ZfrCors\Exception\DisallowedOriginException;
Expand Down Expand Up @@ -120,6 +122,25 @@ public function createPreflightCorsResponse(HttpRequest $request)
return $response;
}

/**
* Create a preflight response by adding the correspoding headers which are merged with per-route configuration
*
* @param HttpRequest $request
* @param RouteMatch|DeprecatedRouteMatch|null $routeMatch
*
* @return HttpResponse
*/
public function createPreflightCorsResponseWithRouteOptions(HttpRequest $request, $routeMatch = null)
{
$options = $this->options;
if ($routeMatch instanceof RouteMatch || $routeMatch instanceof DeprecatedRouteMatch) {
$options->setFromArray($routeMatch->getParam(CorsOptions::ROUTE_PARAM) ?: []);
}
$response = $this->createPreflightCorsResponse($request);

return $response;
}

/**
* Populate a simple CORS response
*
Expand Down
65 changes: 65 additions & 0 deletions tests/ZfrCorsTest/Service/CorsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Zend\Http\Response as HttpResponse;
use Zend\Http\Request as HttpRequest;
use Zend\Mvc\MvcEvent;
use Zend\Mvc\Router\Http\RouteMatch as DeprecatedRouteMatch;
use Zend\Router\Http\RouteMatch;
use ZfrCors\Options\CorsOptions;
use ZfrCors\Service\CorsService;

Expand Down Expand Up @@ -336,6 +338,69 @@ public function testCanDetectCorsRequestFromSameHostButDifferentScheme()
$this->assertTrue($this->corsService->isCorsRequest($request));
}

public function testCanHandleUnconfiguredRouteMatch()
{
$routeMatch = class_exists(DeprecatedRouteMatch::class) ? new DeprecatedRouteMatch([]) : new RouteMatch([]);

$request = new HttpRequest();
$request->getHeaders()->addHeaderLine('Origin', 'http://example.com');
$response = $this->corsService->createPreflightCorsResponseWithRouteOptions($request, $routeMatch);

$headers = $response->getHeaders();

$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('', $response->getContent());
$this->assertEquals('http://example.com', $headers->get('Access-Control-Allow-Origin')->getFieldValue());
$this->assertEquals(
'GET, POST, PUT, DELETE, OPTIONS',
$headers->get('Access-Control-Allow-Methods')->getFieldValue()
);
$this->assertEquals('Content-Type, Accept', $headers->get('Access-Control-Allow-Headers')->getFieldValue());
$this->assertEquals(10, $headers->get('Access-Control-Max-Age')->getFieldValue());
$this->assertEquals(0, $headers->get('Content-Length')->getFieldValue());

$this->assertEquals('true', $headers->get('Access-Control-Allow-Credentials')->getFieldValue());
}

public function testCanHandleConfiguredRouteMatch()
{

$routeMatchParameters = [
CorsOptions::ROUTE_PARAM => [
'allowed_origins' => ['http://example.org'],
'allowed_methods' => ['POST', 'DELETE', 'OPTIONS'],
'allowed_headers' => ['Content-Type', 'Accept', 'Cookie'],
'exposed_headers' => ['Location'],
'max_age' => 5,
'allowed_credentials' => false,
],
];

$routeMatch = class_exists(DeprecatedRouteMatch::class) ? new DeprecatedRouteMatch($routeMatchParameters) :
new RouteMatch($routeMatchParameters);

$request = new HttpRequest();
$request->getHeaders()->addHeaderLine('Origin', 'http://example.org');
$response = $this->corsService->createPreflightCorsResponseWithRouteOptions($request, $routeMatch);

$headers = $response->getHeaders();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('', $response->getContent());
$this->assertEquals('http://example.org', $headers->get('Access-Control-Allow-Origin')->getFieldValue());
$this->assertEquals(
'POST, DELETE, OPTIONS',
$headers->get('Access-Control-Allow-Methods')->getFieldValue()
);
$this->assertEquals(
'Content-Type, Accept, Cookie',
$headers->get('Access-Control-Allow-Headers')->getFieldValue()
);
$this->assertEquals(5, $headers->get('Access-Control-Max-Age')->getFieldValue());
$this->assertEquals(0, $headers->get('Content-Length')->getFieldValue());

$this->assertFalse($headers->has('Access-Control-Allow-Credentials'));
}

/**
* @see https://github.com/zf-fr/zfr-cors/issues/44
* @expectedException \ZfrCors\Exception\InvalidOriginException
Expand Down

0 comments on commit f68543c

Please sign in to comment.