File Coverage

blib/lib/Mojolicious/Plugin/StrictCORS.pm
Criterion Covered Total %
statement 6 124 4.8
branch 0 60 0.0
condition 0 26 0.0
subroutine 2 10 20.0
pod 1 1 100.0
total 9 221 4.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::StrictCORS;
2 1     1   782 use Mojo::Base 'Mojolicious::Plugin';
  1         2  
  1         9  
3              
4             our $VERSION = "0.02";
5             $VERSION = eval $VERSION;
6              
7 1     1   1240 use constant DEFAULT_MAX_AGE => 3600;
  1         2  
  1         2369  
8              
9             sub register {
10 0     0 1   my ($self, $app, $conf) = @_;
11              
12 0   0       $conf->{max_age} //= DEFAULT_MAX_AGE;
13 0   0       $conf->{expose} //= [];
14              
15             # Recursive get route params
16             my $route_params = sub {
17 0     0     my ($c) = @_;
18              
19 0           my $route = $c->match->endpoint;
20              
21 0           my %params;
22 0           my @fields = qw/origin credentials expose methods headers/;
23              
24 0           while ($route) {
25 0           for my $name (@fields) {
26 0 0         next if exists $params{$name};
27 0 0         next unless exists $route->to->{"cors_${name}"};
28              
29 0           $params{$name} = $route->to->{"cors_${name}"};
30             }
31              
32 0           $route = $route->parent;
33             }
34              
35 0           return \%params;
36 0           };
37              
38             my $check_preflight = sub {
39 0     0     my ($c) = @_;
40              
41 0           my $h = $c->req->headers;
42              
43 0 0 0       return 1 if $h->header('Origin') =~ qr/\S/ms
44             and $h->header('Access-Control-Request-Method') =~ qr/\S/ms;
45              
46 0           return; # Fail
47 0           };
48              
49             # Check Origin header
50             my $check_origin = sub {
51 0     0     my ($c, @allow) = @_;
52              
53 0           my $h = $c->req->headers;
54              
55 0           my $origin = $h->origin;
56 0 0         return unless defined $origin;
57              
58 0 0         return $origin if grep { not ref $_ and $_ eq '*' } @allow;
  0 0          
59              
60             return $origin if grep {
61 0 0         if (not ref $_) { lc $origin eq lc $_ }
  0 0          
  0 0          
62 0           elsif (ref $_ eq 'Regexp') { $origin =~ $_ }
63 0           else { die "Router 'cors_origin' param must be scalar or Regexp\n" }
64             } @allow;
65              
66 0           $app->log->debug("Reject CORS Origin '$origin'");
67              
68 0           return; # Fail
69 0           };
70              
71             # Check request method
72             my $check_methods = sub {
73 0     0     my ($c, @allow) = @_;
74              
75 0           my $h = $c->req->headers;
76              
77 0           my $method = $h->header('Access-Control-Request-Method');
78 0 0         return unless defined $method;
79              
80 0           my $allow = join ", ", @allow;
81              
82             return $allow if grep {
83 0 0         if (not ref $_) { uc $method eq uc $_ }
  0 0          
  0            
84 0           else { die "Router 'cors_methods' param must be scalar\n" }
85             } @allow;
86              
87 0           $app->log->debug("Reject CORS Method '$method'");
88              
89 0           return; # Fail
90 0           };
91              
92             # Check request headers
93             my $check_headers = sub {
94 0     0     my ($c, @allow) = @_;
95              
96 0           my $h = $c->req->headers;
97              
98 0           my @safe_headers = qw/
99             Cache-Control
100             Content-Language
101             Content-Type
102             Expires
103             Last-Modified
104             Pragma
105             /;
106              
107 0           my %safe_headers = map { lc $_ => 1 } @safe_headers;
  0            
108 0           my $allow = join ", ", @allow;
109              
110 0           my $headers = $h->header('Access-Control-Request-Headers');
111 0   0       my @headers = map { lc } grep { $_ } split /,\s*/ms, $headers || '';
  0            
  0            
112              
113 0 0         return $allow unless @headers;
114              
115             return $allow unless grep {
116 0 0         if (not ref $_) { not $safe_headers{ lc $_ } }
  0 0          
  0            
117 0           else { die "Router 'cors_headers' param must be scalar\n" }
118             } @allow;
119              
120 0           $app->log->debug("Reject CORS Headers '$headers'");
121              
122 0           return; # Fail
123 0           };
124              
125             $app->hook(around_action => sub {
126 0     0     my ($next, $c, $action, $last) = @_;
127              
128             # Only endpoints intrested
129 0 0         return $next->() unless $last;
130              
131             # Do not process preflight requests
132 0 0         return $next->() if $c->req->method eq 'OPTIONS';
133              
134 0           my $params = $route_params->($c);
135              
136             # Do not process routes without cors_origin configured
137 0   0       my @params_origin = @{$params->{origin} //= []};
  0            
138 0 0         return $next->() unless @params_origin;
139              
140 0           my $h = $c->res->headers;
141 0           $h->append('Vary' => 'Origin');
142              
143 0           my $origin = $check_origin->($c, @params_origin);
144 0 0         return $next->() unless defined $origin;
145              
146 0           $h->header('Access-Control-Allow-Origin' => $origin);
147              
148             $h->header('Access-Control-Allow-Credentials' => 'true')
149 0 0 0       if $params->{credentials} //= 0;
150              
151 0   0       my @params_expose = (@{$conf->{expose}}, @{$params->{expose} //= []});
  0            
  0            
152 0 0         if (@params_expose) {
153 0           my $params_expose = join ", ", @params_expose;
154 0           $h->header('Access-Control-Expose-Headers' => $params_expose);
155             }
156              
157 0           $app->log->debug("Allow CORS Origin '$origin'");
158              
159 0           return $next->();
160 0           });
161              
162             # CORS Preflight
163             $app->routes->add_shortcut(cors => sub {
164 0     0     my ($r, @args) = @_;
165              
166             $r->options(@args)->to(
167             cb => sub {
168 0           my ($c) = @_;
169              
170 0 0         return $c->render(status => 204, data => '')
171             unless $check_preflight->($c);
172              
173 0           my $params = $route_params->($c);
174              
175 0   0       my @params_origin = @{$params->{origin} //= []};
  0            
176 0 0         return $c->render(status => 204, data => '')
177             unless @params_origin;
178              
179 0           my $h = $c->res->headers;
180 0           $h->append('Vary' => 'Origin');
181              
182 0           my $origin = $check_origin->($c, @params_origin);
183 0 0         return $c->render(status => 204, data => '')
184             unless defined $origin;
185              
186 0   0       my @params_methods = @{$params->{methods} //= []};
  0            
187             push @params_methods, 'HEAD'
188 0           if grep { uc $_ eq 'GET' } @params_methods
189 0 0 0       and not grep { uc $_ eq 'HEAD' } @params_methods;
  0            
190 0 0         return $c->render(status => 204, data => '')
191             unless @params_methods;
192              
193 0           my $methods = $check_methods->($c, @params_methods);
194 0 0         return $c->render(status => 204, data => '')
195             unless defined $methods;
196              
197 0   0       my @params_headers = @{$params->{headers} //= []};
  0            
198              
199 0           my $headers = $check_headers->($c, @params_headers);
200 0 0         return $c->render(status => 204, data => '')
201             unless defined $headers;
202              
203 0           $h->header('Access-Control-Allow-Origin' => $origin);
204 0           $h->header('Access-Control-Allow-Methods' => $methods);
205              
206 0 0         $h->header('Access-Control-Allow-Headers' => $headers)
207             if $headers;
208              
209             $h->header('Access-Control-Allow-Credentials' => 'true')
210 0 0 0       if $params->{credentials} //= 0;
211              
212 0           $h->header('Access-Control-Max-Age' => $conf->{max_age});
213              
214 0           $app->log->debug("Accept CORS '$origin' => '$methods'");
215 0           return $c->render(status => 204, data => '');
216             }
217 0           );
218 0           });
219             }
220              
221             1;
222              
223             __END__