Skip to content

Commit 75bda43

Browse files
committed
Merge pull request #16 from weierophinney/hotfix/cors-preflight-normal-segregation
Better response handling
2 parents 3a3eaf9 + edbe071 commit 75bda43

File tree

6 files changed

+81
-27
lines changed

6 files changed

+81
-27
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 1.1.0
2+
3+
- Segregate preflight vs. inflight CORS requests. Preflight detection continues
4+
to happen during the "route" event. However, inflight requests are detected
5+
now during the "finish" event in order to ensure they operate on the same
6+
response object as will be sent back to the client.
7+
([#16](https://github.com/zf-fr/zfr-cors/pull/16))
8+
19
# 1.0.2
210

311
- Properly set "Access-Control-Allow-Credentials" for normal requests if credentials are allowed ([#13](https://github.com/zf-fr/zfr-cors/pull/13)).

src/ZfrCors/Mvc/CorsRequestListener.php

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class CorsRequestListener extends AbstractListenerAggregate
3939
*/
4040
protected $corsService;
4141

42+
/**
43+
* Whether or not a preflight request was detected
44+
*
45+
* @var bool
46+
*/
47+
protected $isPreflight = false;
48+
4249
/**
4350
* @param CorsService $corsService
4451
*/
@@ -52,17 +59,24 @@ public function __construct(CorsService $corsService)
5259
*/
5360
public function attach(EventManagerInterface $events)
5461
{
55-
$this->listeners[] = $events->attach(MvcEvent::EVENT_ROUTE, array($this, 'onCorsRequest'), -1);
62+
// Preflight can be handled during the route event, and should return early
63+
$this->listeners[] = $events->attach(MvcEvent::EVENT_ROUTE, array($this, 'onCorsPreflight'), -1);
64+
65+
// "in"flight should be handled during "FINISH" to ensure we operate on the actual route being returned
66+
$this->listeners[] = $events->attach(MvcEvent::EVENT_FINISH, array($this, 'onCorsRequest'), 100);
5667
}
5768

5869
/**
59-
* Handle a CORS request (either preflight or normal CORS request)
70+
* Handle a CORS preflight request
6071
*
61-
* @param MvcEvent $event
62-
* @return mixed
72+
* @param MvcEvent $event
73+
* @return null|HttpResponse
6374
*/
64-
public function onCorsRequest(MvcEvent $event)
75+
public function onCorsPreflight(MvcEvent $event)
6576
{
77+
// Reset state flag
78+
$this->isPreflight = false;
79+
6680
/** @var $request HttpRequest */
6781
$request = $event->getRequest();
6882
/** @var $response HttpResponse */
@@ -72,23 +86,49 @@ public function onCorsRequest(MvcEvent $event)
7286
return;
7387
}
7488

75-
// First, the preflight request
76-
if ($this->corsService->isPreflightRequest($request)) {
77-
return $this->corsService->createPreflightCorsResponse($request);
89+
// If this isn't a preflight, done
90+
if (!$this->corsService->isPreflightRequest($request)) {
91+
return;
92+
}
93+
94+
// Preflight -- return a response now!
95+
$this->isPreflight = true;
96+
97+
return $this->corsService->createPreflightCorsResponse($request);
98+
}
99+
100+
/**
101+
* Handle a CORS request (non-preflight, normal CORS request)
102+
*
103+
* @param MvcEvent $event
104+
*/
105+
public function onCorsRequest(MvcEvent $event)
106+
{
107+
// Do nothing if we previously created a preflight response
108+
if ($this->isPreflight) {
109+
return;
110+
}
111+
112+
/** @var $request HttpRequest */
113+
$request = $event->getRequest();
114+
/** @var $response HttpResponse */
115+
$response = $event->getResponse();
116+
117+
if (!$request instanceof HttpRequest || !$this->corsService->isCorsRequest($request)) {
118+
return;
78119
}
79120

80-
// Otherwise, it is the second step of the CORS request, and we let ZF continue
121+
// This is the second step of the CORS request, and we let ZF continue
81122
// processing the response
82123
try {
83124
$response = $this->corsService->populateCorsResponse($request, $response);
84-
$event->setResponse($response);
85125
} catch (DisallowedOriginException $exception) {
86126
$response = new HttpResponse(); // Clear response for security
87-
88127
$response->setStatusCode(403)
89128
->setReasonPhrase($exception->getMessage());
90129

91-
return $response;
92130
}
131+
132+
$event->setResponse($response);
93133
}
94134
}

src/ZfrCors/Options/CorsOptions.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function getAllowedOrigins()
8888
}
8989

9090
/**
91-
* @param array $allowedMethods
91+
* @param array $allowedMethods
9292
* @return void
9393
*/
9494
public function setAllowedMethods(array $allowedMethods)
@@ -109,7 +109,7 @@ public function getAllowedMethods()
109109
}
110110

111111
/**
112-
* @param array $allowedHeaders
112+
* @param array $allowedHeaders
113113
* @return void
114114
*/
115115
public function setAllowedHeaders(array $allowedHeaders)
@@ -126,7 +126,7 @@ public function getAllowedHeaders()
126126
}
127127

128128
/**
129-
* @param int $maxAge
129+
* @param int $maxAge
130130
* @return void
131131
*/
132132
public function setMaxAge($maxAge)
@@ -143,7 +143,7 @@ public function getMaxAge()
143143
}
144144

145145
/**
146-
* @param array $exposedHeaders
146+
* @param array $exposedHeaders
147147
* @return void
148148
*/
149149
public function setExposedHeaders(array $exposedHeaders)
@@ -160,7 +160,7 @@ public function getExposedHeaders()
160160
}
161161

162162
/**
163-
* @param bool $allowedCredentials
163+
* @param bool $allowedCredentials
164164
* @return void
165165
*/
166166
public function setAllowedCredentials($allowedCredentials)

src/ZfrCors/Service/CorsService.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function __construct(CorsOptions $options)
5151
* Check if the HTTP request is a CORS request by checking if the Origin header is present and that the
5252
* request URI is not the same as the one in the Origin
5353
*
54-
* @param HttpRequest $request
54+
* @param HttpRequest $request
5555
* @return bool
5656
*/
5757
public function isCorsRequest(HttpRequest $request)
@@ -70,7 +70,7 @@ public function isCorsRequest(HttpRequest $request)
7070
/**
7171
* Check if the CORS request is a preflight request
7272
*
73-
* @param HttpRequest $request
73+
* @param HttpRequest $request
7474
* @return bool
7575
*/
7676
public function isPreflightRequest(HttpRequest $request)
@@ -109,8 +109,8 @@ public function createPreflightCorsResponse(HttpRequest $request)
109109
/**
110110
* Populate a simple CORS response
111111
*
112-
* @param HttpRequest $request
113-
* @param HttpResponse $response
112+
* @param HttpRequest $request
113+
* @param HttpResponse $response
114114
* @return HttpResponse
115115
* @throws DisallowedOriginException If the origin is not allowed
116116
*/
@@ -147,7 +147,7 @@ public function populateCorsResponse(HttpRequest $request, HttpResponse $respons
147147
$headers->addHeaderLine('Vary', 'Origin');
148148
}
149149
}
150-
150+
151151
if ($this->options->getAllowedCredentials()) {
152152
$headers->addHeaderLine('Access-Control-Allow-Credentials', 'true');
153153
}

tests/ZfrCorsTest/Mvc/CorsRequestListenerTest.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,13 @@ public function testAttach()
6363
$eventManager = $this->getMock('Zend\EventManager\EventManagerInterface');
6464

6565
$eventManager
66-
->expects($this->once())
66+
->expects($this->at(0))
6767
->method('attach')
68-
->with(MvcEvent::EVENT_ROUTE, $this->isType('callable'), $this->lessThan(0));
68+
->with(MvcEvent::EVENT_ROUTE, $this->isType('callable'), $this->LessThan(1));
69+
$eventManager
70+
->expects($this->at(1))
71+
->method('attach')
72+
->with(MvcEvent::EVENT_FINISH, $this->isType('callable'), $this->greaterThan(1));
6973

7074
$this->corsListener->attach($eventManager);
7175
}
@@ -79,6 +83,7 @@ public function testReturnNothingForNonCorsRequest()
7983
$mvcEvent->setRequest($request)
8084
->setResponse($response);
8185

86+
$this->assertNull($this->corsListener->onCorsPreflight($mvcEvent));
8287
$this->assertNull($this->corsListener->onCorsRequest($mvcEvent));
8388
}
8489

@@ -95,7 +100,7 @@ public function testImmediatelyReturnResponseForPreflightCorsRequest()
95100
$mvcEvent->setRequest($request)
96101
->setResponse($response);
97102

98-
$this->assertInstanceOf('Zend\Http\Response', $this->corsListener->onCorsRequest($mvcEvent));
103+
$this->assertInstanceOf('Zend\Http\Response', $this->corsListener->onCorsPreflight($mvcEvent));
99104
}
100105

101106
public function testReturnNothingForNormalAuthorizedCorsRequest()
@@ -125,9 +130,10 @@ public function testReturnUnauthorizedResponseForNormalUnauthorizedCorsRequest()
125130
$mvcEvent->setRequest($request)
126131
->setResponse($response);
127132

128-
$newResponse = $this->corsListener->onCorsRequest($mvcEvent);
133+
$this->corsListener->onCorsRequest($mvcEvent);
129134

130135
// NOTE: a new response is created for security purpose
136+
$newResponse = $mvcEvent->getResponse();
131137
$this->assertNotEquals($response, $newResponse);
132138
$this->assertEquals(403, $newResponse->getStatusCode());
133139
$this->assertEquals('', $newResponse->getContent());

tests/ZfrCorsTest/Util/ServiceManagerFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static function getApplicationConfig()
5656
}
5757

5858
/**
59-
* @param array|null $config
59+
* @param array|null $config
6060
* @return ServiceManager
6161
*/
6262
public static function getServiceManager(array $config = null)

0 commit comments

Comments
 (0)