File Coverage

blib/lib/Plack/Middleware/CrossOrigin.pm
Criterion Covered Total %
statement 107 107 100.0
branch 42 42 100.0
condition 28 36 77.7
subroutine 14 14 100.0
pod 2 2 100.0
total 193 201 96.0


line stmt bran cond sub pod time code
1             package Plack::Middleware::CrossOrigin;
2 1     1   56028 use strict;
  1         2  
  1         26  
3 1     1   5 use warnings;
  1         2  
  1         43  
4              
5             our $VERSION = '0.014';
6             $VERSION =~ tr/_//d;
7              
8 1     1   15 use 5.008;
  1         3  
9 1     1   464 use parent qw(Plack::Middleware);
  1         253  
  1         5  
10              
11 1     1   12280 use Plack::Util;
  1         2  
  1         25  
12 1         3 use Plack::Util::Accessor qw(
13             origins
14             headers
15             methods
16             max_age
17             expose_headers
18             credentials
19             continue_on_failure
20 1     1   5 );
  1         2  
21              
22             my @simple_headers = qw(
23             Accept
24             Accept-Language
25             Content-Language
26             );
27             my @simple_response_headers = qw(
28             Cache-Control
29             Content-Language
30             Content-Type
31             Expires
32             Last-Modified
33             Pragma
34             );
35             my @common_headers = qw(
36             Cache-Control
37             Depth
38             If-Modified-Since
39             User-Agent
40             X-File-Name
41             X-File-Size
42             X-Requested-With
43             X-Prototype-Version
44             );
45              
46             # RFC 7231
47             my @http_methods = qw(
48             GET
49             HEAD
50             POST
51             PUT
52             DELETE
53             CONNECT
54             OPTIONS
55             TRACE
56             );
57              
58             # RFC 5789
59             my @rfc_5789_methods = qw(
60             PATCH
61             );
62              
63             my @webdav_methods = qw(
64             CANCELUPLOAD
65             CHECKIN
66             CHECKOUT
67             COPY
68             DELETE
69             GETLIB
70             LOCK
71             MKCOL
72             MOVE
73             OPTIONS
74             PROPFIND
75             PROPPATCH
76             PUT
77             REPORT
78             UNCHECKOUT
79             UNLOCK
80             UPDATE
81             VERSION-CONTROL
82             );
83              
84             my @all_methods = ( @http_methods, @rfc_5789_methods, @webdav_methods );
85              
86             sub prepare_app {
87 8     8 1 9260 my ($self) = @_;
88              
89 8 100 33     23 $self->origins([$self->origins || ()])
90             unless ref $self->origins;
91              
92 8 100 66     120 $self->methods([$self->methods || @all_methods])
93             unless ref $self->methods;
94              
95 8 100 66     94 $self->headers([$self->headers || @common_headers])
96             unless ref $self->headers;
97              
98 8 100 66     86 $self->expose_headers([$self->expose_headers || ()])
99             unless ref $self->expose_headers;
100              
101 8         85 $self->{origins_h} = { map { $_ => 1 } @{ $self->origins } };
  8         49  
  8         16  
102             ($self->{origins_re}) =
103             map qr/\A(?:$_)\z/,
104             join '|',
105             map +(
106             join '[a-z0-9.-]*',
107             map quotemeta,
108             split /\*/, $_, -1
109             ),
110 8         21 @{ $self->origins };
  8         13  
111              
112 8         224 $self->{methods_h} = { map { $_ => 1 } @{ $self->methods } };
  114         225  
  8         21  
113 8         19 $self->{headers_h} = { map { lc $_ => 1 } @{ $self->headers } };
  45         117  
  8         16  
114 8         16 $self->{expose_headers_h} = { map { $_ => 1 } @{ $self->expose_headers } };
  4         40  
  8         14  
115             }
116              
117             sub call {
118 25     25 1 80890 my ($self, $env) = @_;
119 25         45 my $origin = $env->{HTTP_ORIGIN};
120 25         37 my $continue_on_failure;
121 25 100 66     142 if ($origin) {
    100 66        
      100        
      100        
      66        
122 18         51 $continue_on_failure = $self->continue_on_failure;
123             }
124             # for preflighted GET requests, some WebKit versions don't
125             # include Origin with the actual request. Fixed in current versions
126             # of WebKit, Chrome, and Safari.
127             # Work around it using the Referer header.
128             # https://bugs.webkit.org/show_bug.cgi?id=50773
129             # http://code.google.com/p/chromium/issues/detail?id=57836
130             elsif ($env->{REQUEST_METHOD} eq 'GET'
131             && $env->{HTTP_USER_AGENT}
132             && $env->{HTTP_USER_AGENT} =~ m{\bAppleWebKit/(\d+\.\d+)}
133             && $1 < 534.19
134             && $env->{HTTP_REFERER}
135             && $env->{HTTP_REFERER} =~ m{\A ( \w+://[^/]+ )}msx
136             ) {
137 3         10 $origin = $1;
138 3         4 $continue_on_failure = 1;
139             }
140             else {
141 4         19 return _with_vary($self->app->($env));
142             }
143              
144 21         91 my $request_method = $env->{HTTP_ACCESS_CONTROL_REQUEST_METHOD};
145 21         28 my $request_headers = $env->{HTTP_ACCESS_CONTROL_REQUEST_HEADERS};
146 21 100       48 my @request_headers = $request_headers ? (split /,\s*/, $request_headers) : ();
147 21   100     63 my $preflight = $env->{REQUEST_METHOD} eq 'OPTIONS' && $request_method;
148              
149 21 100 100     69 my $fail = $continue_on_failure && !$preflight ? $self->app : \&_response_forbidden;
150              
151 21         44 my $allowed_origins_h = $self->{origins_h};
152 21         45 my $allowed_methods = $self->methods;
153 21         77 my $allowed_methods_h = $self->{methods_h};
154 21         47 my $allowed_headers = $self->headers;
155 21         68 my $allowed_headers_h = $self->{headers_h};
156 21         39 my $expose_headers = $self->expose_headers;
157 21         64 my $expose_headers_h = $self->{expose_headers_h};
158              
159 21         27 my @headers;
160              
161 21 100 100     124 if (not ($allowed_origins_h->{'*'} || $origin =~ $self->{origins_re} ) ) {
162 6         16 return _with_vary($fail->($env));
163             }
164              
165 15 100       31 if ($preflight) {
166 7 100       19 if ( $allowed_methods_h->{'*'} ) {
    100          
167 3         7 $allowed_methods = [$request_method];
168             }
169             elsif ( ! $allowed_methods_h->{$request_method} ) {
170 1         6 return _response_forbidden();
171             }
172 6 100       18 if ( $allowed_headers_h->{'*'} ) {
    100          
173 2         5 $allowed_headers = \@request_headers;
174             }
175 2         9 elsif ( grep { ! defined } @{$allowed_headers_h}{map lc, @request_headers} ) {
  4         10  
176 1         4 return _response_forbidden();
177             }
178             }
179 13 100       29 if ($self->credentials) {
    100          
180 1         5 push @headers, 'Access-Control-Allow-Credentials' => 'true';
181             }
182             elsif ($allowed_origins_h->{'*'}) {
183 4         19 $origin = '*';
184             }
185 13         55 push @headers, 'Access-Control-Allow-Origin' => $origin;
186              
187 13         16 my $res;
188 13 100       34 if ($preflight) {
189 5 100       11 if (defined $self->max_age) {
190 3         15 push @headers, 'Access-Control-Max-Age' => $self->max_age;
191             }
192 5         29 push @headers, 'Access-Control-Allow-Methods' => join ', ', @$allowed_methods;
193 5         17 push @headers, 'Access-Control-Allow-Headers' => join ', ', @$allowed_headers;
194              
195 5         12 $res = _response_success();
196             }
197             else {
198 8         20 $res = $self->app->($env);
199             }
200              
201             return $self->response_cb($res, sub {
202 13     13   200 my $res = shift;
203              
204 13 100       31 if (! _vary_headers($res->[1])->{origin}) {
205 12         16 push @{ $res->[1] }, 'Vary' => 'Origin';
  12         38  
206             }
207              
208 13 100       31 if ($expose_headers_h->{'*'}) {
209 3         5 my %headers = @{ $res->[1] };
  3         13  
210 3         12 delete @headers{@simple_response_headers};
211 3         13 $expose_headers = [sort keys %headers];
212             }
213              
214 13         37 push @headers, 'Access-Control-Expose-Headers' => join ', ', @$expose_headers;
215              
216 13         17 push @{ $res->[1] }, @headers;
  13         44  
217 13         121 });
218             }
219              
220             sub _response_forbidden {
221 6     6   34 [403, ['Content-Type' => 'text/plain', 'Content-Length' => 9, 'Vary' => 'Origin'], ['forbidden']];
222             }
223              
224             sub _response_success {
225 5     5   18 [200, [ 'Content-Type' => 'text/plain' ], [] ];
226             }
227              
228             sub _with_vary {
229 10     10   97 my ($res) = @_;
230             return Plack::Util::response_cb($res, sub {
231 10     10   114 my $res = shift;
232              
233 10 100       22 if (! _vary_headers($res->[1])->{origin}) {
234 6         9 push @{ $res->[1] }, 'Vary' => 'Origin';
  6         26  
235             }
236 10         54 });
237             }
238              
239             sub _vary_headers {
240 23     23   36 my ($headers) = @_;
241              
242             my %vary =
243 23         55 map { s/\A\s+//; s/\s+\z//; ( lc, 1) }
  7         107  
  7         14  
  7         24  
244             map +(split /,/),
245             Plack::Util::header_get($headers, 'Vary');
246              
247 23         255 return \%vary;
248             }
249              
250             1;
251             __END__