Completed
Push — master ( b2cc83...7350db )
by Sam
12s
created

CorsService::configure()   B

Complexity

Conditions 7
Paths 64

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 12
nc 64
nop 1
dl 0
loc 24
rs 8.8333
c 0
b 0
f 0
1
<?php namespace Nord\Lumen\Cors;
2
3
use Closure;
4
use Illuminate\Http\Exceptions\HttpResponseException;
5
use Nord\Lumen\Cors\Contracts\CorsService as CorsServiceContract;
6
use Symfony\Component\HttpFoundation\Request;
7
use Symfony\Component\HttpFoundation\Response;
8
9
class CorsService implements CorsServiceContract
10
{
11
12
    /**
13
     * Allowed request origins.
14
     *
15
     * @var array
16
     */
17
    private $allowOrigins = [];
18
19
    /**
20
     * Allowed HTTP methods.
21
     *
22
     * @var array
23
     */
24
    private $allowMethods = [];
25
26
    /**
27
     * Allowed HTTP headers.
28
     *
29
     * @var array
30
     */
31
    private $allowHeaders = [];
32
33
    /**
34
     * Whether or not the response can be exposed when credentials are present.
35
     *
36
     * @var bool
37
     */
38
    private $allowCredentials = false;
39
40
    /**
41
     * HTTP Headers that are allowed to be exposed to the web browser.
42
     *
43
     * @var array
44
     */
45
    private $exposeHeaders = [];
46
47
    /**
48
     * Indicates how long preflight request can be cached.
49
     *
50
     * @var int
51
     */
52
    private $maxAge = 0;
53
54
55
    /**
56
     * CorsService constructor.
57
     *
58
     * @param array $config
59
     */
60
    public function __construct(array $config = [])
61
    {
62
        $this->configure($config);
63
    }
64
65
66
    /**
67
     * @inheritdoc
68
     */
69
    public function handlePreflightRequest(Request $request)
70
    {
71
        try {
72
            $this->validatePreflightRequest($request);
73
        } catch (HttpResponseException $e) {
74
            return $this->createResponse($request, $e->getResponse());
75
        }
76
77
        return $this->createPreflightResponse($request);
78
    }
79
80
81
    /**
82
     * @inheritdoc
83
     */
84
    public function handleRequest(Request $request, Closure $next)
85
    {
86
        return $this->createResponse($request, $next($request));
87
    }
88
89
90
    /**
91
     * @inheritdoc
92
     */
93
    public function isCorsRequest(Request $request)
94
    {
95
        return $request->headers->has('Origin');
96
    }
97
98
99
    /**
100
     * @inheritdoc
101
     */
102
    public function isPreflightRequest(Request $request)
103
    {
104
        return $this->isCorsRequest($request) && $request->isMethod('OPTIONS') && $request->headers->has('Access-Control-Request-Method');
105
    }
106
107
108
    /**
109
     * Configures the service.
110
     *
111
     * @param array $config
112
     */
113
    protected function configure(array $config)
114
    {
115
        if (isset($config['allow_origins'])) {
116
            $this->allowOrigins = $config['allow_origins'];
117
        }
118
119
        if (isset($config['allow_headers'])) {
120
            $this->setAllowHeaders($config['allow_headers']);
121
        }
122
123
        if (isset($config['allow_methods'])) {
124
            $this->setAllowMethods($config['allow_methods']);
125
        }
126
127
        if (isset($config['allow_credentials'])) {
128
            $this->allowCredentials = $config['allow_credentials'];
129
        }
130
131
        if (isset($config['expose_headers'])) {
132
            $this->setExposeHeaders($config['expose_headers']);
133
        }
134
135
        if (isset($config['max_age'])) {
136
            $this->setMaxAge($config['max_age']);
137
        }
138
    }
139
140
141
    /**
142
     * @param Request $request
143
     *
144
     * @throws HttpResponseException
145
     */
146
    protected function validatePreflightRequest(Request $request)
147
    {
148
        $origin = $request->headers->get('Origin');
149
150
        if (!$this->isOriginAllowed($origin)) {
151
            throw new HttpResponseException(new Response('Origin not allowed', 403));
152
        }
153
154
        $method = $request->headers->get('Access-Control-Request-Method');
155
156
        if ($method && !$this->isMethodAllowed($method)) {
157
            throw new HttpResponseException(new Response('Method not allowed', 405));
158
        }
159
160
        if (!$this->isAllHeadersAllowed()) {
161
            $headers = str_replace(' ', '', $request->headers->get('Access-Control-Request-Headers'));
162
163
            foreach (explode(',', $headers) as $header) {
164
                if (!$this->isHeaderAllowed($header)) {
165
                    throw new HttpResponseException(new Response('Header not allowed', 403));
166
                }
167
            }
168
        }
169
    }
170
171
172
    /**
173
     * Creates a preflight response.
174
     *
175
     * @param Request $request
176
     *
177
     * @return Response
178
     */
179
    protected function createPreflightResponse(Request $request)
180
    {
181
        $response = new Response();
182
183
        $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
184
185
        if ($this->allowCredentials) {
186
            $response->headers->set('Access-Control-Allow-Credentials', 'true');
187
        }
188
189
        if ($this->maxAge) {
190
            $response->headers->set('Access-Control-Max-Age', (string)$this->maxAge);
191
        }
192
193
        $allowMethods = $this->isAllMethodsAllowed()
194
            ? strtoupper($request->headers->get('Access-Control-Request-Method'))
195
            : implode(', ', $this->allowMethods);
196
197
        $response->headers->set('Access-Control-Allow-Methods', $allowMethods);
198
199
        $allowHeaders = $this->isAllHeadersAllowed()
200
            ? strtolower($request->headers->get('Access-Control-Request-Headers'))
201
            : implode(', ', $this->allowHeaders);
202
203
        $response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
204
205
        return $response;
206
    }
207
208
209
    /**
210
     * @param Request  $request
211
     * @param Response $response
212
     *
213
     * @return Response
214
     */
215
    protected function createResponse(Request $request, Response $response)
216
    {
217
        $origin = $request->headers->get('Origin');
218
219
        if ($this->isOriginAllowed($origin)) {
220
            $response->headers->set('Access-Control-Allow-Origin', $origin);
221
        }
222
223
        $vary = $request->headers->has('Vary') ? $request->headers->get('Vary') . ', Origin' : 'Origin';
224
        $response->headers->set('Vary', $vary);
225
226
        if ($this->allowCredentials) {
227
            $response->headers->set('Access-Control-Allow-Credentials', 'true');
228
        }
229
230
        if (!empty($this->exposeHeaders)) {
231
            $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->exposeHeaders));
232
        }
233
234
        return $response;
235
    }
236
237
238
    /**
239
     * Returns whether or not the origin is allowed.
240
     *
241
     * @param string|null $origin
242
     *
243
     * @return bool
244
     */
245
    protected function isOriginAllowed(?string $origin)
246
    {
247
        if ($this->isAllOriginsAllowed()) {
248
            return true;
249
        }
250
251
        return in_array($origin, $this->allowOrigins);
252
    }
253
254
255
    /**
256
     * Returns whether or not the method is allowed.
257
     *
258
     * @param string|null $method
259
     *
260
     * @return bool
261
     */
262
    protected function isMethodAllowed(?string $method)
263
    {
264
        if ($this->isAllMethodsAllowed()) {
265
            return true;
266
        }
267
268
        return in_array(strtoupper($method), $this->allowMethods);
269
    }
270
271
272
    /**
273
     * Returns whether or not the header is allowed.
274
     *
275
     * @param string|null $header
276
     *
277
     * @return bool
278
     */
279
    protected function isHeaderAllowed(?string $header)
280
    {
281
        if ($this->isAllHeadersAllowed()) {
282
            return true;
283
        }
284
285
        return in_array(strtolower($header), $this->allowHeaders);
286
    }
287
288
289
    /**
290
     * @return bool
291
     */
292
    protected function isAllOriginsAllowed()
293
    {
294
        return in_array('*', $this->allowOrigins);
295
    }
296
297
298
    /**
299
     * @return bool
300
     */
301
    protected function isAllMethodsAllowed()
302
    {
303
        return in_array('*', $this->allowMethods);
304
    }
305
306
307
    /**
308
     * @return bool
309
     */
310
    protected function isAllHeadersAllowed()
311
    {
312
        return in_array('*', $this->allowHeaders);
313
    }
314
315
316
    /**
317
     * @param array $allowMethods
318
     */
319
    protected function setAllowMethods(array $allowMethods)
320
    {
321
        $this->allowMethods = array_map('strtoupper', $allowMethods);
322
    }
323
324
325
    /**
326
     * @param array $allowHeaders
327
     */
328
    protected function setAllowHeaders(array $allowHeaders)
329
    {
330
        $this->allowHeaders = array_map('strtolower', $allowHeaders);
331
    }
332
333
334
    /**
335
     * @param array $exposeHeaders
336
     */
337
    protected function setExposeHeaders(array $exposeHeaders)
338
    {
339
        $this->exposeHeaders = array_map('strtolower', $exposeHeaders);
340
    }
341
342
343
    /**
344
     * @param int $maxAge
345
     */
346
    protected function setMaxAge(int $maxAge)
347
    {
348
        if ($maxAge < 0) {
349
            throw new \InvalidArgumentException('Max age must be a positive number or zero.');
350
        }
351
352
        $this->maxAge = $maxAge;
353
    }
354
}
355