File Coverage

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