File Coverage

blib/lib/Dancer/Plugin/CORS.pm
Criterion Covered Total %
statement 155 167 92.8
branch 66 76 86.8
condition 13 17 76.4
subroutine 18 18 100.0
pod n/a
total 252 278 90.6


line stmt bran cond sub pod time code
1 10     10   2169892 use strict;
  10         21  
  10         385  
2 10     10   46 use warnings;
  10         16  
  10         398  
3             package Dancer::Plugin::CORS;
4             # ABSTRACT: A plugin for using cross origin resource sharing
5              
6              
7 10     10   75 use Carp qw(croak confess);
  10         18  
  10         568  
8 10     10   680 use Dancer ':syntax';
  10         283641  
  10         45  
9 10     10   7456 use Dancer::Plugin;
  10         12272  
  10         675  
10 10     10   59 use Sub::Name;
  10         14  
  10         384  
11 10     10   42 use Scalar::Util qw(blessed);
  10         13  
  10         375  
12 10     10   42 use URI;
  10         13  
  10         179  
13              
14 10     10   3500 use Dancer::Plugin::CORS::Sharing;
  10         20  
  10         274  
15              
16 10     10   53 use constant DEBUG => 0;
  10         13  
  10         8726  
17              
18             our $VERSION = '0.13'; # VERSION
19              
20             my $routes = {};
21              
22             sub _isin($@) {
23 14     14   17 my $test = shift;
24 14         16 scalar grep { $test eq $_ } @_;
  16         52  
25             }
26              
27             sub _isuri(_) {
28 48     48   361 shift =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|
29             }
30              
31             sub _prefl_handle;
32             sub _add_rule($%);
33             sub _handle;
34              
35             my $current_route;
36              
37             sub _prefl_handle {
38 30     30   5960 debug "[CORS] entered preflight request main subroutine" if DEBUG;
39 30 50       74 unless (defined $current_route) {
40 0         0 warning "[CORS] current route not defined!";
41 0         0 return;
42             }
43 30 100       54 unless(_handle($current_route)) {
44 22         64 my $request = Dancer::SharedData->request;
45 22         94 while ($current_route = $current_route->next) {
46 18 100       126 if ($current_route->match($request)) {
47 16         1560 debug "[CORS] going to next handler" if DEBUG;
48 16         42 pass;
49             }
50             }
51 6         147 debug "[CORS] no more rules." if DEBUG;
52             }
53 14         43 $current_route = undef;
54             }
55              
56             sub _add_rule($%) {
57 30     30   7578 my ($route, %options) = @_;
58            
59 30 50       87 if (ref $route eq 'ARRAY') {
60 0         0 return map { _add_rule($_, %options) } @$route;
  0         0  
61             }
62              
63 30 100 66     230 if (blessed $route and $route->isa('Dancer::Route')) {
64 22         55 my $prefl = Dancer::App->current->registry->add_route(Dancer::Route->new(
65             method => 'options',
66             code => \&_prefl_handle,
67             options => $route->options,
68             pattern => $route->pattern
69             ));
70 22         2432 $options{method} = uc($route->method);
71 22         285 $routes->{$prefl} = [{ %options }];
72 22         34 debug "registered preflight route handler for ".$route->method." pattern: ".$route->pattern."\n" if DEBUG;
73             }
74            
75 30 100       85 unless (exists $routes->{$route}) {
76 25         49 $routes->{$route} = [];
77 25 100       60 unless (ref $route) {
78 8         6 debug "registered preflight route handler for any pattern: $route\n" if DEBUG;
79 8         34 options $route => \&_prefl_handle;
80             }
81             }
82 30         1111 push @{ $routes->{$route} } => \%options;
  30         101  
83             }
84              
85             sub _handle {
86 52     52   53 my $route = shift;
87 52         125 my $request = Dancer::SharedData->request;
88 52         191 my $path = $request->path_info;
89            
90 52 50 66     454 unless (exists $routes->{$path} or exists $routes->{$route}) {
91 0         0 debug "[CORS] path $path or route $route did not no matched any rule" if DEBUG;
92             }
93            
94 52         108 my $preflight = uc $request->method eq 'OPTIONS';
95            
96 52         222 debug "[CORS] preflight request" if DEBUG and $preflight;
97            
98 52         129 my $origin = scalar($request->header('Origin'));
99            
100 52 100       1493 unless (defined $origin) {
101 4         5 debug "[CORS] no origin header present in request" if DEBUG;
102 4         23 return;
103             }
104              
105 48 50       110 unless (_isuri($origin)) {
106 0         0 debug "[CORS] origin '$origin' is not a URI" if DEBUG;
107 0         0 return;
108             }
109            
110 48 100       156 my $requested_method = $preflight
111             ? scalar($request->header('Access-Control-Request-Method'))
112             : $request->method
113             ;
114 48 50       737 unless (defined $requested_method) {
115 0         0 debug "[CORS] no request method defined" if DEBUG;
116             }
117              
118 48   100     111 my @requested_headers = map { s{\s+}{}g; $_ } split /,+/, (scalar($request->header('Access-Control-Request-Headers')) || '');
  2         43  
  2         5  
119            
120 48         1585 my ($ok, $i) = (0, 0);
121 48         46 my ($headers, $xoptions);
122            
123 48 100       140 if (exists $routes->{$route}) {
124 37         46 $path = "$route";
125 37         43 debug "[CORS] dynamic route: $path" if DEBUG;
126             } else {
127 11         11 debug "[CORS] static route: $path" if DEBUG;
128             }
129            
130 48         41 my $n = scalar @{$routes->{$path}};
  48         87  
131            
132 48         46 RULE: foreach my $options (@{$routes->{$path}}) {
  48         104  
133 49         54 debug "[CORS] testing rule ".++$i." of $n" if DEBUG;
134 49         39 if (DEBUG) {
135 10     10   59 use Data::Dumper;
  10         12  
  10         5383  
136             debug Dumper($options);
137             }
138 49         63 $headers = {};
139 49 100       108 if (exists $options->{origin}) {
140 46         79 my $reftype = ref $options->{origin};
141 46 100       203 if ($reftype eq 'CODE') {
    100          
    100          
    50          
142 5 100       25 if (!$options->{origin}->(URI->new($origin))) {
143 3         178 debug "[CORS] origin $origin did not matched against coderef" if DEBUG;
144 3         9 next RULE;
145             }
146             } elsif ($reftype eq 'ARRAY') {
147 2 100       3 unless (_isin $origin => @{ $options->{origin} }) {
  2         7  
148 1         2 debug "[CORS] origin $origin is not in array" if DEBUG;
149 1         3 next RULE;
150             }
151             } elsif ($reftype eq 'Regexp') {
152 2 100       14 unless ($origin =~ $options->{origin}) {
153 1         1 debug "[CORS] origin $origin did not matched against regexp" if DEBUG;
154 1         3 next RULE;
155             }
156             } elsif ($reftype eq '') {
157 37 100       93 unless ($options->{origin} eq $origin) {
158 5         4 debug "[CORS] origin $origin did not matched against static string" if DEBUG;
159 5         13 next RULE;
160             }
161             } else {
162 0         0 confess("unknown origin type: $reftype");
163             }
164             } else {
165 3         5 $origin = '*';
166             }
167 39         6048 $headers->{'Access-Control-Allow-Origin'} = $origin;
168 39 100       110 $headers->{'Vary'} = 'Origin' if $origin ne '*';
169            
170 39 100       92 if (exists $options->{timing}) {
171 3 100 66     15 if (defined $options->{timing} and $options->{timing} eq '1') {
172 2         3 $headers->{'Timing-Allow-Origin'} = $headers->{'Access-Control-Allow-Origin'};
173             } else {
174 1         2 $headers->{'Timing-Allow-Origin'} = 'null';
175             }
176             }
177            
178 39 100       95 if (exists $options->{credentials}) {
179 2 50       5 if (!!$options->{credentials}) {
180 2 100       6 if ($origin eq '*') {
181 1         4 warning('For a resource that supports credentials a origin matcher must be specified.');
182 1         58 next RULE;
183             }
184 1         2 $headers->{'Access-Control-Allow-Credentials'} = 'true' ;
185             }
186             }
187            
188 38 100       75 if (exists $options->{expose}) {
189 2         4 $headers->{'Access-Control-Expose-Headers'} = $options->{expose};
190             }
191            
192 38 100       130 if (exists $options->{methods}) {
    50          
193 10 100       14 unless (_isin lc $requested_method => map lc, @{ $options->{methods} }) {
  10         73  
194 7         5 debug "[CORS] request method not allowed" if DEBUG;
195 7         17 next RULE;
196             }
197 3         5 $headers->{'Access-Control-Allow-Methods'} = join ', ' => map uc, @{ $options->{methods} };
  3         13  
198             } elsif (exists $options->{method}) {
199 28 100       87 unless ($options->{method} eq $requested_method) {
200 10         5 debug "[CORS] request method '$requested_method' not allowed: ".$options->{method} if DEBUG;
201 10         22 next RULE;
202             }
203 18         38 $headers->{'Access-Control-Allow-Methods'} = $options->{method};
204             }
205            
206 21 100       97 if (exists $options->{headers}) {
    50          
207 2         7 foreach my $requested_header (@requested_headers) {
208 2 50       3 unless (_isin lc $requested_header => map lc, @{ $options->{headers} }) {
  2         4  
209 0         0 debug "[CORS] requested headers did not match allowed in rule" if DEBUG;
210 0         0 next RULE;
211             }
212             }
213 2         3 $headers->{'Access-Control-Allow-Headers'} = join ', ' => @{ $options->{headers} };
  2         8  
214             } elsif (@requested_headers) {
215 0         0 $headers->{'Access-Control-Allow-Headers'} = join ', ' => @requested_headers;
216             }
217              
218 21 100 100     95 if ($preflight and exists $options->{maxage}) {
219 2         4 $headers->{'Access-Control-Max-Age'} = $options->{maxage};
220             }
221            
222 21         30 $ok = 1;
223 21         131 var CORS => {%$options};
224 21         246 Dancer::SharedData->response->headers(%$headers);
225 21         2100 if (DEBUG) {
226 10     10   58 use Data::Dumper;
  10         11  
  10         3201  
227             debug Dumper({headers => $headers});
228             }
229 21         43 last RULE;
230             }
231              
232 48 100       86 if ($ok) {
233 21         29 debug "[CORS] matched!" if DEBUG;
234             } else {
235 27         29 debug "[CORS] no rule matched" if DEBUG;
236             }
237            
238 48         178 return $ok;
239             }
240              
241              
242             register(share => \&_add_rule);
243              
244             hook(before => sub {
245             $current_route = shift || return;
246             my $preflight = uc Dancer::SharedData->request->method eq 'OPTIONS';
247             if ($preflight) {
248             debug "[CORS] pre-check: preflight request, handle within main subroutine" if DEBUG;
249             } else {
250             debug "[CORS] pre-check: no preflight, handle actual request now" if DEBUG;
251             _handle($current_route);
252             }
253             });
254              
255             my $current_sharing;
256              
257              
258             register sharing => sub {
259 7     7   47 my $class = __PACKAGE__.'::Sharing';
260 7   66     27 $current_sharing ||= $class->new(@_,_add_rule=>\&_add_rule);
261 7         37 return $current_sharing;
262             };
263              
264             register_plugin;
265             1;
266              
267             __END__