File Coverage

blib/lib/Dancer/Plugin/CORS.pm
Criterion Covered Total %
statement 139 159 87.4
branch 51 62 82.2
condition 9 14 64.2
subroutine 16 17 94.1
pod n/a
total 215 252 85.3


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;