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