line
stmt
bran
cond
sub
pod
time
code
1
package Dancer::Plugin::CORS;
2
3
11
11
2021553
use Modern::Perl;
11
33603
11
51
4
11
11
5330
use Dancer::Plugin::CORS::Sharing;
11
23
11
513
5
6
=head1 NAME
7
8
Dancer::Plugin::CORS - A plugin for using cross origin resource sharing
9
10
=head1 VERSION
11
12
Version 0.11
13
14
=cut
15
16
our $VERSION = '0.11';
17
18
=head1 DESCRIPTION
19
20
Cross origin resource sharing is a feature used by modern web browser to bypass cross site scripting restrictions. A webservice can provide those rules from which origin a client is allowed to make cross-site requests. This module helps you to setup such rules.
21
22
=head1 SYNOPSIS
23
24
use Dancer::Plugin::CORS;
25
26
get '/foo' => sub { ... };
27
share '/foo' =>
28
origin => 'http://localhost/',
29
credentials => 1,
30
expose => [qw[ Content-Type ]],
31
method => 'GET',
32
headers => [qw[ X-Requested-With ]],
33
maxage => 7200,
34
timing => 1,
35
;
36
37
=cut
38
39
11
11
50
use Carp qw(croak confess);
11
16
11
509
40
11
11
1387
use Dancer ':syntax';
11
352812
11
49
41
11
11
8088
use Dancer::Plugin;
11
11765
11
746
42
11
11
56
use Sub::Name;
11
65
11
404
43
11
11
44
use Scalar::Util qw(blessed);
11
11
11
336
44
11
11
40
use URI;
11
12
11
169
45
46
11
11
36
use constant DEBUG => 0;
11
13
11
11565
47
48
my $routes = {};
49
50
sub _isin($@) {
51
14
14
55
shift ~~ \@_;
52
}
53
54
sub _isuri(_) {
55
48
48
330
shift =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|
56
}
57
58
sub _handle;
59
my $current_route;
60
61
sub _prefl_handle {
62
30
30
6066
debug "[CORS] entered preflight request main subroutine" if DEBUG;
63
30
50
68
unless (defined $current_route) {
64
0
0
warning "[CORS] current route not defined!";
65
0
0
return;
66
}
67
30
100
49
unless(_handle($current_route)) {
68
22
133
my $request = Dancer::SharedData->request;
69
22
112
while ($current_route = $current_route->next) {
70
18
100
113
if ($current_route->match($request)) {
71
16
1491
debug "[CORS] going to next handler" if DEBUG;
72
16
90
pass;
73
}
74
}
75
6
144
debug "[CORS] no more rules." if DEBUG;
76
}
77
14
44
$current_route = undef;
78
}
79
80
sub _add_rule($%) {
81
30
30
7196
my ($route, %options) = @_;
82
83
30
100
66
227
if (blessed $route and $route->isa('Dancer::Route')) {
84
22
53
my $prefl = Dancer::App->current->registry->add_route(Dancer::Route->new(
85
method => 'options',
86
code => \&_prefl_handle,
87
options => $route->options,
88
pattern => $route->pattern
89
));
90
22
2531
$options{method} = uc($route->method);
91
22
181
$routes->{$prefl} = [{ %options }];
92
22
76
debug "registered preflight route handler for ".$route->method." pattern: ".$route->pattern."\n" if DEBUG;
93
}
94
95
30
100
88
unless (exists $routes->{$route}) {
96
25
62
$routes->{$route} = [];
97
25
100
52
unless (ref $route) {
98
8
74
debug "registered preflight route handler for any pattern: $route\n" if DEBUG;
99
8
29
options $route => \&_prefl_handle;
100
}
101
}
102
30
1107
push @{ $routes->{$route} } => \%options;
30
101
103
}
104
105
sub _handle {
106
52
52
59
my $route = shift;
107
52
116
my $request = Dancer::SharedData->request;
108
52
180
my $path = $request->path_info;
109
110
52
50
66
392
unless (exists $routes->{$path} or exists $routes->{$route}) {
111
0
0
debug "[CORS] path $path or route $route did not no matched any rule" if DEBUG;
112
}
113
114
52
101
my $preflight = uc $request->method eq 'OPTIONS';
115
116
52
207
debug "[CORS] preflight request" if DEBUG and $preflight;
117
118
52
120
my $origin = scalar($request->header('Origin'));
119
120
52
100
1498
unless (defined $origin) {
121
4
4
debug "[CORS] no origin header present in request" if DEBUG;
122
4
12
return;
123
}
124
125
48
50
92
unless (_isuri($origin)) {
126
0
0
debug "[CORS] origin '$origin' is not a URI" if DEBUG;
127
0
0
return;
128
}
129
130
48
100
238
my $requested_method = $preflight
131
? scalar($request->header('Access-Control-Request-Method'))
132
: $request->method
133
;
134
48
50
712
unless (defined $requested_method) {
135
0
0
debug "[CORS] no request method defined" if DEBUG;
136
}
137
138
48
100
96
my @requested_headers = map { s{\s+}{}g; $_ } split /,+/, (scalar($request->header('Access-Control-Request-Headers')) || '');
2
26
2
3
139
140
48
1393
my ($ok, $i) = (0, 0);
141
48
47
my ($headers, $xoptions);
142
143
48
100
106
if (exists $routes->{$route}) {
144
37
44
$path = "$route";
145
37
36
debug "[CORS] dynamic route" if DEBUG;
146
} else {
147
11
10
debug "[CORS] static route" if DEBUG;
148
}
149
150
48
36
my $n = scalar @{$routes->{$path}};
48
77
151
152
48
42
RULE: foreach my $options (@{$routes->{$path}}) {
48
109
153
49
57
debug "[CORS] testing rule ".++$i." of $n" if DEBUG;
154
49
23
if (DEBUG) {
155
11
11
71
use Data::Dumper;
11
39
11
10954
156
debug Dumper($options);
157
}
158
49
64
$headers = {};
159
49
100
91
if (exists $options->{origin}) {
160
46
75
given (ref $options->{origin}) {
161
46
132
when ('CODE') {
162
5
100
21
if (!$options->{origin}->(URI->new($origin))) {
163
3
162
debug "[CORS] origin $origin did not matched against coderef" if DEBUG;
164
3
8
next RULE;
165
}
166
}
167
41
50
when ('ARRAY') {
168
2
100
2
unless (_isin($origin => @{ $options->{origin} })) {
2
6
169
1
2
debug "[CORS] origin $origin is not in array" if DEBUG;
170
1
2
next RULE;
171
}
172
}
173
39
42
when ('Regexp') {
174
2
100
15
unless ($origin =~ $options->{origin}) {
175
1
1
debug "[CORS] origin $origin did not matched against regexp" if DEBUG;
176
1
3
next RULE;
177
}
178
}
179
37
41
when ('') {
180
37
100
119
unless ($options->{origin} eq $origin) {
181
5
5
debug "[CORS] origin $origin did not matched against static string" if DEBUG;
182
5
12
next RULE;
183
}
184
}
185
0
0
default {
186
0
0
confess("unknown origin type: $_");
187
}
188
}
189
} else {
190
3
3
$origin = '*';
191
}
192
39
5829
$headers->{'Access-Control-Allow-Origin'} = $origin;
193
39
100
99
$headers->{'Vary'} = 'Origin' if $origin ne '*';
194
195
39
100
82
if (exists $options->{timing}) {
196
3
100
66
10
if (defined $options->{timing} and $options->{timing} eq '1') {
197
2
4
$headers->{'Timing-Allow-Origin'} = $headers->{'Access-Control-Allow-Origin'};
198
} else {
199
1
2
$headers->{'Timing-Allow-Origin'} = 'null';
200
}
201
}
202
203
39
100
80
if (exists $options->{credentials}) {
204
2
50
5
if (!!$options->{credentials}) {
205
2
100
2
if ($origin eq '*') {
206
1
4
warning('For a resource that supports credentials a origin matcher must be specified.');
207
1
49
next RULE;
208
}
209
1
2
$headers->{'Access-Control-Allow-Credentials'} = 'true' ;
210
}
211
}
212
213
38
100
75
if (exists $options->{expose}) {
214
2
3
$headers->{'Access-Control-Expose-Headers'} = $options->{expose};
215
}
216
217
38
100
97
if (exists $options->{methods}) {
50
218
10
100
12
unless (_isin(lc $requested_method => map lc, @{ $options->{methods} })) {
10
34
219
7
5
debug "[CORS] request method not allowed" if DEBUG;
220
7
16
next RULE;
221
}
222
3
5
$headers->{'Access-Control-Allow-Methods'} = join ', ' => map uc, @{ $options->{methods} };
3
11
223
} elsif (exists $options->{method}) {
224
28
100
71
unless ($options->{method} eq $requested_method) {
225
10
10
debug "[CORS] request method '$requested_method' not allowed: ".$options->{method} if DEBUG;
226
10
16
next RULE;
227
}
228
18
36
$headers->{'Access-Control-Allow-Methods'} = $options->{method};
229
}
230
231
21
100
81
if (exists $options->{headers}) {
50
232
2
2
foreach my $requested_header (@requested_headers) {
233
2
50
4
unless (_isin(lc $requested_header => map lc, @{ $options->{headers} })) {
2
3
234
0
0
debug "[CORS] requested headers did not match allowed in rule" if DEBUG;
235
0
0
next RULE;
236
}
237
}
238
2
2
$headers->{'Access-Control-Allow-Headers'} = join ', ' => @{ $options->{headers} };
2
4
239
} elsif (@requested_headers) {
240
0
0
$headers->{'Access-Control-Allow-Headers'} = join ', ' => @requested_headers;
241
}
242
243
21
100
100
80
if ($preflight and exists $options->{maxage}) {
244
2
4
$headers->{'Access-Control-Max-Age'} = $options->{maxage};
245
}
246
247
21
26
$ok = 1;
248
21
109
var CORS => {%$options};
249
21
225
Dancer::SharedData->response->headers(%$headers);
250
21
2072
if (DEBUG) {
251
11
11
55
use Data::Dumper;
11
12
11
4074
252
debug Dumper({headers => $headers});
253
}
254
21
35
last RULE;
255
}
256
257
48
100
89
if ($ok) {
258
21
19
debug "[CORS] matched!" if DEBUG;
259
} else {
260
27
25
debug "[CORS] no rule matched" if DEBUG;
261
}
262
263
48
210
return $ok;
264
}
265
266
=head1 KEYWORDS
267
268
=head2 share(C<$route>, C<%options>)
269
270
The parameter C<$route> may be any valid path like used I, I, I, I or I but not I.
271
272
Alternatively a L object may be used instead:
273
274
$route = get '/' => sub { ... };
275
share $route => ... ;
276
277
For any route more than one rule may be defined. The order is relevant: the first matching rule wins.
278
279
Following keywords recognized by C<%options>:
280
281
=over 4
282
283
=item I
284
285
This key defines a static origin (scalar), a list (arrayref), a regex or a subroutine.
286
287
If not specified, any origin is allowed.
288
289
If a subroutine is used, the first passed parameter is a L object. It should return a true value if this origin is allowed to access the route in question; otherwise false.
290
291
origin => sub { shift->host ~~ [ 'localhost', '127.0.0.1', '::1' ] } # allow only from localhost
292
293
Hint: a origin consists of protocol, hostname and maybe a port. Examples: C, C, C, C, C
294
295
=item I
296
297
This indicates whether cookies, HTTP authentication and/or client-side SSL certificates may sent by a client. Allowed values are C<0> or C<1>.
298
299
This option must be used together with I.
300
301
=item I
302
303
A comma-seperated list of headers, that a client may extract from response for use in a client application.
304
305
=item I
306
307
A arrayref of allowed methods. If no methods are specified, all methods are allowed.
308
309
=item I
310
311
A string containing a single supported method. This parameter is autofilled when I is used together with a L object. If no method is specified, any method is allowed.
312
313
=item I
314
315
A arrayref of allowed request headers. In most cases that should be C<[ 'X-Requested-With' ]> when ajax requests are made. If not headers are specified, all requested headers are allowed.
316
317
=item I
318
319
A maximum time (in seconds) a client may cache a preflight request. This can decrease the amount of requests made to the webservice.
320
321
=item I
322
323
Allow access to the resource timing information. If set to 1, the header C is set to the same value as I. Otherwise, its set to the value I. If the keyword is not present, no I header will be appended to response. See L for more information.
324
325
=back
326
327
=cut
328
329
register(share => \&_add_rule);
330
331
hook(before => sub {
332
$current_route = shift || return;
333
my $preflight = uc Dancer::SharedData->request->method eq 'OPTIONS';
334
if ($preflight) {
335
debug "[CORS] pre-check: preflight request, handle within main subroutine" if DEBUG;
336
} else {
337
debug "[CORS] pre-check: no preflight, handle actual request now" if DEBUG;
338
_handle($current_route);
339
}
340
});
341
342
my $current_sharing;
343
344
=head2 sharing
345
346
This keyword is a helper for re-using rules for many routes.
347
348
See L for more information about this feature.
349
350
=cut
351
352
register sharing => sub {
353
7
7
25
my $class = __PACKAGE__.'::Sharing';
354
7
66
25
$current_sharing ||= $class->new(@_,_add_rule=>\&_add_rule);
355
7
26
return $current_sharing;
356
};
357
358
=head1 AUTHOR
359
360
David Zurborg, C<< >>
361
362
=head1 BUGS
363
364
Please report any bugs or feature requests trough my project management tool
365
at L. I
366
will be notified, and then you'll automatically be notified of progress on
367
your bug as I make changes.
368
369
=head1 SUPPORT
370
371
You can find documentation for this module with the perldoc command.
372
373
perldoc Dancer::Plugin::CORS
374
375
You can also look for information at:
376
377
=over 4
378
379
=item * Redmine: Homepage of this module
380
381
L
382
383
=item * RT: CPAN's request tracker
384
385
L
386
387
=item * AnnoCPAN: Annotated CPAN documentation
388
389
L
390
391
=item * CPAN Ratings
392
393
L
394
395
=item * Search CPAN
396
397
L
398
399
=back
400
401
=head1 COPYRIGHT & LICENSE
402
403
Copyright 2014 David Zurborg, all rights reserved.
404
405
This program is released under the following license: open-source
406
407
=cut
408
409
register_plugin;
410
1;