Skip to content

Commit 561a5ee

Browse files
authored
Make responses cacheable by only using dynamic origins when required (#70)
Refactor Vary headers, don't throw an error in the application, let CORS handle it. Use static headers when possible
1 parent df153ce commit 561a5ee

File tree

3 files changed

+223
-142
lines changed

3 files changed

+223
-142
lines changed

src/Asm89/Stack/Cors.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
5353
return $this->cors->handlePreflightRequest($request);
5454
}
5555

56-
if (!$this->cors->isActualRequestAllowed($request)) {
57-
return new Response('Not allowed.', 403);
58-
}
59-
6056
$response = $this->app->handle($request, $type, $catch);
6157

6258
return $this->cors->addActualRequestHeaders($response, $request);

src/Asm89/Stack/CorsService.php

Lines changed: 105 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Asm89\Stack;
1313

14-
use Symfony\Component\HttpKernel\HttpKernelInterface;
1514
use Symfony\Component\HttpFoundation\Request;
1615
use Symfony\Component\HttpFoundation\Response;
1716

@@ -55,9 +54,12 @@ private function normalizeOptions(array $options = array())
5554
return $options;
5655
}
5756

57+
/**
58+
* @deprecated use isOriginAllowed
59+
*/
5860
public function isActualRequestAllowed(Request $request)
5961
{
60-
return $this->checkOrigin($request);
62+
return $this->isOriginAllowed($request);
6163
}
6264

6365
public function isCorsRequest(Request $request)
@@ -72,134 +74,156 @@ public function isPreflightRequest(Request $request)
7274
&& $request->headers->has('Access-Control-Request-Method');
7375
}
7476

75-
public function addActualRequestHeaders(Response $response, Request $request)
77+
public function handlePreflightRequest(Request $request)
7678
{
77-
if (!$this->checkOrigin($request)) {
78-
return $response;
79-
}
79+
$response = new Response();
8080

81-
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
81+
$response->setStatusCode(204);
8282

83-
if (!$response->headers->has('Vary')) {
84-
$response->headers->set('Vary', 'Origin');
85-
} else {
86-
$response->headers->set('Vary', $response->headers->get('Vary') . ', Origin');
87-
}
83+
return $this->addPreflightRequestHeaders($response, $request);
84+
}
8885

89-
if ($this->options['supportsCredentials']) {
90-
$response->headers->set('Access-Control-Allow-Credentials', 'true');
91-
}
86+
public function addPreflightRequestHeaders(Response $response, Request $request)
87+
{
88+
$this->configureAllowedOrigin($response, $request);
9289

93-
if ($this->options['exposedHeaders']) {
94-
$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
95-
}
90+
$this->configureAllowCredentials($response, $request);
91+
92+
$this->configureAllowedMethods($response, $request);
93+
94+
$this->configureAllowedHeaders($response, $request);
95+
96+
$this->configureMaxAge($response, $request);
9697

9798
return $response;
9899
}
99100

100-
public function handlePreflightRequest(Request $request)
101+
public function isOriginAllowed(Request $request)
101102
{
102-
if (true !== $check = $this->checkPreflightRequestConditions($request)) {
103-
return $check;
103+
if ($this->options['allowedOrigins'] === true) {
104+
return true;
104105
}
105106

106-
return $this->buildPreflightCheckResponse($request);
107-
}
107+
if (!$request->headers->has('Origin')) {
108+
return false;
109+
}
108110

109-
private function buildPreflightCheckResponse(Request $request)
110-
{
111-
$response = new Response();
111+
$origin = $request->headers->get('Origin');
112112

113-
if ($this->options['supportsCredentials']) {
114-
$response->headers->set('Access-Control-Allow-Credentials', 'true');
113+
if (in_array($origin, $this->options['allowedOrigins'])) {
114+
return true;
115115
}
116116

117-
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
118-
119-
if ($this->options['maxAge'] !== null) {
120-
$response->headers->set('Access-Control-Max-Age', $this->options['maxAge']);
117+
foreach ($this->options['allowedOriginsPatterns'] as $pattern) {
118+
if (preg_match($pattern, $origin)) {
119+
return true;
120+
}
121121
}
122122

123-
$allowMethods = $this->options['allowedMethods'] === true
124-
? strtoupper($request->headers->get('Access-Control-Request-Method'))
125-
: implode(', ', $this->options['allowedMethods']);
126-
$response->headers->set('Access-Control-Allow-Methods', $allowMethods);
123+
return false;
124+
}
127125

128-
$allowHeaders = $this->options['allowedHeaders'] === true
129-
? strtoupper($request->headers->get('Access-Control-Request-Headers'))
130-
: implode(', ', $this->options['allowedHeaders']);
131-
$response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
126+
public function addActualRequestHeaders(Response $response, Request $request)
127+
{
128+
$this->configureAllowedOrigin($response, $request);
132129

133-
$response->setStatusCode(204);
130+
$this->configureAllowCredentials($response, $request);
131+
132+
$this->configureExposedHeaders($response, $request);
134133

135134
return $response;
136135
}
137136

138-
private function checkPreflightRequestConditions(Request $request)
137+
private function configureAllowedOrigin(Response $response, Request $request)
139138
{
140-
if (!$this->checkOrigin($request)) {
141-
return $this->createBadRequestResponse(403, 'Origin not allowed');
139+
if ($this->options['allowedOrigins'] === true && !$this->options['supportsCredentials']) {
140+
// Safe+cacheable, allow everything
141+
$response->headers->set('Access-Control-Allow-Origin', '*');
142+
} elseif ($this->isSingleOriginAllowed()) {
143+
// Single origins can be safely set
144+
$response->headers->set('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]);
145+
} else {
146+
// For dynamic headers, check the origin first
147+
if ($this->isOriginAllowed($request)) {
148+
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
149+
}
150+
151+
$this->varyHeader($response, 'Origin');
142152
}
153+
}
143154

144-
if (!$this->checkMethod($request)) {
145-
return $this->createBadRequestResponse(405, 'Method not allowed');
155+
private function isSingleOriginAllowed()
156+
{
157+
if ($this->options['allowedOrigins'] === true || !empty($this->options['allowedOriginsPatterns'])) {
158+
return false;
146159
}
147160

148-
$requestHeaders = array();
149-
// if allowedHeaders has been set to true ('*' allow all flag) just skip this check
150-
if ($this->options['allowedHeaders'] !== true && $request->headers->has('Access-Control-Request-Headers')) {
151-
$headers = strtolower($request->headers->get('Access-Control-Request-Headers'));
152-
$requestHeaders = array_filter(explode(',', $headers));
161+
return count($this->options['allowedOrigins']) === 1;
162+
}
153163

154-
foreach ($requestHeaders as $header) {
155-
if (!in_array(trim($header), $this->options['allowedHeaders'])) {
156-
return $this->createBadRequestResponse(403, 'Header not allowed');
157-
}
164+
private function configureAllowedMethods(Response $response, Request $request)
165+
{
166+
if ($this->options['allowedMethods'] === true) {
167+
if ($this->options['supportsCredentials']) {
168+
$allowMethods = strtoupper($request->headers->get('Access-Control-Request-Method'));
169+
$this->varyHeader($response, 'Access-Control-Request-Method');
170+
} else {
171+
$allowMethods = '*';
158172
}
173+
} else {
174+
$allowMethods = implode(', ', $this->options['allowedMethods']);
159175
}
160176

161-
return true;
177+
$response->headers->set('Access-Control-Allow-Methods', $allowMethods);
162178
}
163179

164-
private function createBadRequestResponse($code, $reason = '')
180+
private function configureAllowedHeaders(Response $response, Request $request)
165181
{
166-
return new Response($reason, $code);
182+
if ($this->options['allowedHeaders'] === true) {
183+
if ($this->options['supportsCredentials']) {
184+
$allowHeaders = strtoupper($request->headers->get('Access-Control-Request-Headers'));
185+
$this->varyHeader($response, 'Access-Control-Request-Headers');
186+
} else {
187+
$allowHeaders = '*';
188+
}
189+
} else {
190+
$allowHeaders = implode(', ', $this->options['allowedHeaders']);
191+
}
192+
$response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
167193
}
168194

169-
private function isSameHost(Request $request)
195+
private function configureAllowCredentials(Response $response, Request $request)
170196
{
171-
return $request->headers->get('Origin') === $request->getSchemeAndHttpHost();
197+
if ($this->options['supportsCredentials']) {
198+
$response->headers->set('Access-Control-Allow-Credentials', 'true');
199+
}
172200
}
173201

174-
private function checkOrigin(Request $request)
202+
private function configureExposedHeaders(Response $response, Request $request)
175203
{
176-
if ($this->options['allowedOrigins'] === true) {
177-
// allow all '*' flag
178-
return true;
179-
}
180-
$origin = $request->headers->get('Origin');
181-
182-
if (in_array($origin, $this->options['allowedOrigins'])) {
183-
return true;
204+
if ($this->options['exposedHeaders']) {
205+
$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
184206
}
207+
}
185208

186-
foreach ($this->options['allowedOriginsPatterns'] as $pattern) {
187-
if (preg_match($pattern, $origin)) {
188-
return true;
189-
}
209+
private function configureMaxAge(Response $response, Request $request)
210+
{
211+
if ($this->options['maxAge'] !== null) {
212+
$response->headers->set('Access-Control-Max-Age', (int) $this->options['maxAge']);
190213
}
191-
192-
return false;
193214
}
194215

195-
private function checkMethod(Request $request)
216+
private function varyHeader(Response $response, $header)
196217
{
197-
if ($this->options['allowedMethods'] === true) {
198-
// allow all '*' flag
199-
return true;
218+
if (!$response->headers->has('Vary')) {
219+
$response->headers->set('Vary', $header);
220+
} else {
221+
$response->headers->set('Vary', $response->headers->get('Vary') . ', ' . $header);
200222
}
223+
}
201224

202-
$requestMethod = strtoupper($request->headers->get('Access-Control-Request-Method'));
203-
return in_array($requestMethod, $this->options['allowedMethods']);
225+
private function isSameHost(Request $request)
226+
{
227+
return $request->headers->get('Origin') === $request->getSchemeAndHttpHost();
204228
}
205229
}

0 commit comments

Comments
 (0)