File Coverage

blib/lib/Plack/Middleware/CrossOrigin.pm
Criterion Covered Total %
statement 91 91 100.0
branch 40 40 100.0
condition 25 33 75.7
subroutine 11 11 100.0
pod 2 2 100.0
total 169 177 95.4


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