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   67518 use strict;
  1         3  
  1         30  
3 1     1   6 use warnings;
  1         2  
  1         51  
4              
5             our $VERSION = '0.013';
6             $VERSION =~ tr/_//d;
7              
8 1     1   17 use 5.008;
  1         10  
9 1     1   494 use parent qw(Plack::Middleware);
  1         303  
  1         5  
10              
11 1     1   14941 use Plack::Util;
  1         3  
  1         31  
12 1         5 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 12141 my ($self) = @_;
88              
89 8 100 33     39 $self->origins([$self->origins || ()])
90             unless ref $self->origins;
91              
92 8 100 66     154 $self->methods([$self->methods || @all_methods])
93             unless ref $self->methods;
94              
95 8 100 66     114 $self->headers([$self->headers || @common_headers])
96             unless ref $self->headers;
97              
98 8 100 66     94 $self->expose_headers([$self->expose_headers || ()])
99             unless ref $self->expose_headers;
100              
101 8         101 $self->{origins_h} = { map { $_ => 1 } @{ $self->origins } };
  8         68  
  8         18  
102             ($self->{origins_re}) =
103             map qr/\A(?:$_)\z/,
104             join '|',
105             map +(
106             join '[a-z.-]*',
107             map quotemeta,
108             split /\*/, $_, -1
109             ),
110 8         30 @{ $self->origins };
  8         18  
111              
112 8         261 $self->{methods_h} = { map { $_ => 1 } @{ $self->methods } };
  114         274  
  8         23  
113 8         20 $self->{headers_h} = { map { lc $_ => 1 } @{ $self->headers } };
  45         139  
  8         30  
114 8         20 $self->{expose_headers_h} = { map { $_ => 1 } @{ $self->expose_headers } };
  4         49  
  8         20  
115             }
116              
117             sub call {
118 24     24 1 169838 my ($self, $env) = @_;
119 24         53 my $origin = $env->{HTTP_ORIGIN};
120 24         43 my $continue_on_failure;
121 24 100 66     205 if ($origin) {
    100 66        
      100        
      100        
      66        
122 17         52 $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         7 $continue_on_failure = 1;
139             }
140             else {
141 4         22 return _with_vary($self->app->($env));
142             }
143              
144 20         103 my $request_method = $env->{HTTP_ACCESS_CONTROL_REQUEST_METHOD};
145 20         36 my $request_headers = $env->{HTTP_ACCESS_CONTROL_REQUEST_HEADERS};
146 20 100       60 my @request_headers = $request_headers ? (split /,\s*/, $request_headers) : ();
147 20   100     74 my $preflight = $env->{REQUEST_METHOD} eq 'OPTIONS' && $request_method;
148              
149 20 100 100     71 my $fail = $continue_on_failure && !$preflight ? $self->app : \&_response_forbidden;
150              
151 20         56 my $allowed_origins_h = $self->{origins_h};
152 20         45 my $allowed_methods = $self->methods;
153 20         91 my $allowed_methods_h = $self->{methods_h};
154 20         49 my $allowed_headers = $self->headers;
155 20         79 my $allowed_headers_h = $self->{headers_h};
156 20         43 my $expose_headers = $self->expose_headers;
157 20         88 my $expose_headers_h = $self->{expose_headers_h};
158              
159 20         35 my @headers;
160              
161 20 100 100     143 if (not ($allowed_origins_h->{'*'} || $origin =~ $self->{origins_re} ) ) {
162 6         20 return _with_vary($fail->($env));
163             }
164              
165 14 100       36 if ($preflight) {
166 7 100       23 if ( $allowed_methods_h->{'*'} ) {
    100          
167 3         8 $allowed_methods = [$request_method];
168             }
169             elsif ( ! $allowed_methods_h->{$request_method} ) {
170 1         8 return _response_forbidden();
171             }
172 6 100       30 if ( $allowed_headers_h->{'*'} ) {
    100          
173 2         6 $allowed_headers = \@request_headers;
174             }
175 2         12 elsif ( grep { ! defined } @{$allowed_headers_h}{map lc, @request_headers} ) {
  4         15  
176 1         3 return _response_forbidden();
177             }
178             }
179 12 100       32 if ($self->credentials) {
    100          
180 1         7 push @headers, 'Access-Control-Allow-Credentials' => 'true';
181             }
182             elsif ($allowed_origins_h->{'*'}) {
183 4         26 $origin = '*';
184             }
185 12         52 push @headers, 'Access-Control-Allow-Origin' => $origin;
186              
187 12         20 my $res;
188 12 100       38 if ($preflight) {
189 5 100       13 if (defined $self->max_age) {
190 3         17 push @headers, 'Access-Control-Max-Age' => $self->max_age;
191             }
192 5         35 push @headers, 'Access-Control-Allow-Methods' => join ', ', @$allowed_methods;
193 5         17 push @headers, 'Access-Control-Allow-Headers' => join ', ', @$allowed_headers;
194              
195 5         13 $res = _response_success();
196             }
197             else {
198 7         28 $res = $self->app->($env);
199             }
200              
201             return $self->response_cb($res, sub {
202 12     12   219 my $res = shift;
203              
204 12 100       45 if (! _vary_headers($res->[1])->{origin}) {
205 11         22 push @{ $res->[1] }, 'Vary' => 'Origin';
  11         36  
206             }
207              
208 12 100       39 if ($expose_headers_h->{'*'}) {
209 3         5 my %headers = @{ $res->[1] };
  3         15  
210 3         12 delete @headers{@simple_response_headers};
211 3         16 $expose_headers = [sort keys %headers];
212             }
213              
214 12         47 push @headers, 'Access-Control-Expose-Headers' => join ', ', @$expose_headers;
215              
216 12         18 push @{ $res->[1] }, @headers;
  12         61  
217 12         135 });
218             }
219              
220             sub _response_forbidden {
221 6     6   37 [403, ['Content-Type' => 'text/plain', 'Content-Length' => 9, 'Vary' => 'Origin'], ['forbidden']];
222             }
223              
224             sub _response_success {
225 5     5   20 [200, [ 'Content-Type' => 'text/plain' ], [] ];
226             }
227              
228             sub _with_vary {
229 10     10   117 my ($res) = @_;
230             return Plack::Util::response_cb($res, sub {
231 10     10   150 my $res = shift;
232              
233 10 100       28 if (! _vary_headers($res->[1])->{origin}) {
234 6         8 push @{ $res->[1] }, 'Vary' => 'Origin';
  6         32  
235             }
236 10         78 });
237             }
238              
239             sub _vary_headers {
240 22     22   40 my ($headers) = @_;
241              
242             my %vary =
243 22         68 map { s/\A\s+//; s/\s+\z//; ( lc, 1) }
  7         153  
  7         17  
  7         31  
244             map +(split /,/),
245             Plack::Util::header_get($headers, 'Vary');
246              
247 22         348 return \%vary;
248             }
249              
250             1;
251             __END__