File Coverage

blib/lib/Dancer/Plugin/CORS.pm
Criterion Covered Total %
statement 152 163 93.2
branch 58 66 87.8
condition 13 17 76.4
subroutine 17 17 100.0
pod n/a
total 240 263 91.2


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;