line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Mojolicious::Plugin::OpenAPI::Cors; |
2
|
48
|
|
|
48
|
|
336
|
use Mojo::Base -base; |
|
48
|
|
|
|
|
102
|
|
|
48
|
|
|
|
|
648
|
|
3
|
|
|
|
|
|
|
|
4
|
|
|
|
|
|
|
require Mojolicious::Routes::Route; |
5
|
|
|
|
|
|
|
my $methods = Mojolicious::Routes::Route->can('methods') ? 'methods' : 'via'; |
6
|
|
|
|
|
|
|
|
7
|
48
|
|
50
|
48
|
|
13126
|
use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0; |
|
48
|
|
|
|
|
110
|
|
|
48
|
|
|
|
|
93243
|
|
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
our %SIMPLE_METHODS = map { ($_ => 1) } qw(GET HEAD POST); |
10
|
|
|
|
|
|
|
our %SIMPLE_CONTENT_TYPES |
11
|
|
|
|
|
|
|
= map { ($_ => 1) } qw(application/x-www-form-urlencoded multipart/form-data text/plain); |
12
|
|
|
|
|
|
|
our %SIMPLE_HEADERS = map { (lc $_ => 1) } |
13
|
|
|
|
|
|
|
qw(Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width); |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
our %PREFLIGHTED_CONTENT_TYPES = %SIMPLE_CONTENT_TYPES; |
16
|
|
|
|
|
|
|
our %PREFLIGHTED_METHODS = map { ($_ => 1) } qw(CONNECT DELETE OPTIONS PATCH PUT TRACE); |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
my $X_RE = qr{^x-}; |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
sub register { |
21
|
59
|
|
|
59
|
1
|
801
|
my ($self, $app, $config) = @_; |
22
|
59
|
|
|
|
|
158
|
my $openapi = $config->{openapi}; |
23
|
|
|
|
|
|
|
|
24
|
59
|
100
|
|
|
|
225
|
if ($config->{add_preflighted_routes}) { |
25
|
1
|
|
|
1
|
|
7
|
$app->plugins->once(openapi_routes_added => sub { $self->_add_preflighted_routes($app, @_) }); |
|
1
|
|
|
|
|
80
|
|
26
|
|
|
|
|
|
|
} |
27
|
|
|
|
|
|
|
|
28
|
59
|
|
|
|
|
372
|
my %defaults = ( |
29
|
|
|
|
|
|
|
openapi_cors_allowed_origins => [], |
30
|
|
|
|
|
|
|
openapi_cors_default_exchange_callback => \&_default_cors_exchange_callback, |
31
|
|
|
|
|
|
|
openapi_cors_default_max_age => 1800, |
32
|
|
|
|
|
|
|
); |
33
|
|
|
|
|
|
|
|
34
|
59
|
|
|
|
|
198
|
$app->defaults($_ => $defaults{$_}) for grep { !$app->defaults($_) } keys %defaults; |
|
177
|
|
|
|
|
1667
|
|
35
|
59
|
|
|
11
|
|
2710
|
$app->helper('openapi.cors_exchange' => sub { $self->_exchange(@_) }); |
|
11
|
|
|
|
|
131628
|
|
36
|
|
|
|
|
|
|
} |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
sub _add_preflighted_routes { |
39
|
1
|
|
|
1
|
|
3
|
my ($self, $app, $openapi, $routes) = @_; |
40
|
1
|
|
|
|
|
14
|
my $c = $app->build_controller; |
41
|
1
|
|
|
|
|
169
|
my $match = Mojolicious::Routes::Match->new(root => $app->routes); |
42
|
|
|
|
|
|
|
|
43
|
1
|
|
|
|
|
17
|
for my $route (@$routes) { |
44
|
4
|
|
|
|
|
12
|
my $route_path = $route->to_string; |
45
|
4
|
100
|
|
|
|
156
|
next if $self->_takeover_exchange_route($route); |
46
|
3
|
50
|
|
|
|
17
|
next if $match->find($c, {method => 'options', path => $route_path}); |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
# Make a given action also handle OPTIONS |
49
|
3
|
|
|
|
|
1794
|
push @{$route->$methods}, 'OPTIONS'; |
|
3
|
|
|
|
|
10
|
|
50
|
3
|
|
|
|
|
19
|
$route->to->{'openapi.cors_preflighted'} = 1; |
51
|
3
|
|
|
|
|
54
|
warn "[OpenAPI] Add route options $route_path (@{[$route->name // '']})\n" if DEBUG; |
52
|
|
|
|
|
|
|
} |
53
|
|
|
|
|
|
|
} |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
sub _default_cors_exchange_callback { |
56
|
2
|
|
|
2
|
|
5
|
my $c = shift; |
57
|
2
|
|
50
|
|
|
7
|
my $allowed = $c->stash('openapi_cors_allowed_origins') || []; |
58
|
2
|
|
50
|
|
|
21
|
my $origin = $c->req->headers->origin // ''; |
59
|
|
|
|
|
|
|
|
60
|
2
|
50
|
|
|
|
41
|
return scalar(grep { $origin =~ $_ } @$allowed) ? undef : '/Origin'; |
|
2
|
|
|
|
|
27
|
|
61
|
|
|
|
|
|
|
} |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
sub _exchange { |
64
|
11
|
|
|
11
|
|
42
|
my ($self, $c) = (shift, shift); |
65
|
11
|
|
66
|
|
|
394
|
my $cb = shift || $c->stash('openapi_cors_default_exchange_callback'); |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
# Not a CORS request |
68
|
11
|
100
|
|
|
|
99
|
unless (defined $c->req->headers->origin) { |
69
|
3
|
|
|
|
|
80
|
my $method = $c->req->method; |
70
|
|
|
|
|
|
|
_render_bad_request($c, 'OPTIONS is only for preflighted CORS requests.') |
71
|
3
|
50
|
66
|
|
|
48
|
if $method eq 'OPTIONS' and $c->match->endpoint->to->{'openapi.cors_preflighted'}; |
72
|
3
|
|
|
|
|
550
|
return $c; |
73
|
|
|
|
|
|
|
} |
74
|
|
|
|
|
|
|
|
75
|
8
|
|
100
|
|
|
280
|
my $type = $self->_is_simple_request($c) || $self->_is_preflighted_request($c) || 'real'; |
76
|
8
|
|
|
|
|
132
|
$c->stash(openapi_cors_type => $type); |
77
|
|
|
|
|
|
|
|
78
|
8
|
|
|
|
|
193
|
my $errors = $c->$cb; |
79
|
8
|
100
|
|
|
|
921
|
return _render_bad_request($c, $errors) if $errors; |
80
|
|
|
|
|
|
|
|
81
|
6
|
|
|
|
|
31
|
_set_default_headers($c); |
82
|
6
|
100
|
|
|
|
121
|
return $type eq 'preflighted' ? $c->tap('render', data => '', status => 200) : $c; |
83
|
|
|
|
|
|
|
} |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
sub _is_preflighted_request { |
86
|
8
|
|
|
8
|
|
107
|
my ($self, $c) = @_; |
87
|
8
|
|
|
|
|
28
|
my $req_h = $c->req->headers; |
88
|
|
|
|
|
|
|
|
89
|
8
|
100
|
|
|
|
113
|
return undef unless $c->req->method eq 'OPTIONS'; |
90
|
4
|
100
|
|
|
|
53
|
return 'preflighted' if $req_h->header('Access-Control-Request-Headers'); |
91
|
3
|
100
|
|
|
|
40
|
return 'preflighted' if $req_h->header('Access-Control-Request-Method'); |
92
|
|
|
|
|
|
|
|
93
|
2
|
|
50
|
|
|
21
|
my $ct = lc($req_h->content_type || ''); |
94
|
2
|
50
|
33
|
|
|
42
|
return 'preflighted' if $ct and $PREFLIGHTED_CONTENT_TYPES{$ct}; |
95
|
|
|
|
|
|
|
|
96
|
0
|
|
|
|
|
0
|
return undef; |
97
|
|
|
|
|
|
|
} |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
sub _is_simple_request { |
100
|
8
|
|
|
8
|
|
31
|
my ($self, $c) = @_; |
101
|
8
|
100
|
|
|
|
28
|
return undef unless $SIMPLE_METHODS{$c->req->method}; |
102
|
|
|
|
|
|
|
|
103
|
3
|
|
|
|
|
49
|
my $req_h = $c->req->headers; |
104
|
3
|
|
|
|
|
41
|
my @names = grep { !$SIMPLE_HEADERS{lc($_)} } @{$req_h->names}; |
|
14
|
|
|
|
|
120
|
|
|
3
|
|
|
|
|
14
|
|
105
|
3
|
50
|
|
|
|
45
|
return undef if @names; |
106
|
|
|
|
|
|
|
|
107
|
0
|
|
0
|
|
|
0
|
my $ct = lc $req_h->content_type || ''; |
108
|
0
|
0
|
0
|
|
|
0
|
return undef if $ct and $SIMPLE_CONTENT_TYPES{$ct}; |
109
|
|
|
|
|
|
|
|
110
|
0
|
|
|
|
|
0
|
return 'simple'; |
111
|
|
|
|
|
|
|
} |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
sub _render_bad_request { |
114
|
4
|
|
|
4
|
|
85
|
my ($c, $errors) = @_; |
115
|
|
|
|
|
|
|
|
116
|
4
|
100
|
66
|
|
|
63
|
$errors = [{message => "Invalid $1 header.", path => $errors}] |
117
|
|
|
|
|
|
|
if !ref $errors and $errors =~ m!^/([\w-]+)!; |
118
|
4
|
100
|
|
|
|
16
|
$errors = [{message => $errors, path => '/'}] unless ref $errors; |
119
|
|
|
|
|
|
|
|
120
|
4
|
|
|
|
|
38
|
return $c->tap('render', openapi => {errors => $errors, status => 400}, status => 400); |
121
|
|
|
|
|
|
|
} |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
sub _set_default_headers { |
124
|
7
|
|
|
7
|
|
17
|
my $c = shift; |
125
|
7
|
|
|
|
|
22
|
my $req_h = $c->req->headers; |
126
|
7
|
|
|
|
|
120
|
my $res_h = $c->res->headers; |
127
|
|
|
|
|
|
|
|
128
|
7
|
100
|
|
|
|
116
|
unless ($res_h->access_control_allow_origin) { |
129
|
3
|
|
|
|
|
30
|
$res_h->access_control_allow_origin($req_h->origin); |
130
|
|
|
|
|
|
|
} |
131
|
|
|
|
|
|
|
|
132
|
7
|
100
|
|
|
|
82
|
return unless $c->stash('openapi_cors_type') eq 'preflighted'; |
133
|
|
|
|
|
|
|
|
134
|
4
|
100
|
|
|
|
55
|
unless ($res_h->header('Access-Control-Allow-Headers')) { |
135
|
3
|
|
100
|
|
|
28
|
$res_h->header( |
136
|
|
|
|
|
|
|
'Access-Control-Allow-Headers' => $req_h->header('Access-Control-Request-Headers') // ''); |
137
|
|
|
|
|
|
|
} |
138
|
|
|
|
|
|
|
|
139
|
4
|
100
|
|
|
|
118
|
unless ($res_h->header('Access-Control-Allow-Methods')) { |
140
|
3
|
|
|
|
|
28
|
my $op_spec = $c->openapi->spec('for_path'); |
141
|
3
|
50
|
|
|
|
473
|
my @methods = sort grep { !/$X_RE/ } keys %{$op_spec || {}}; |
|
7
|
|
|
|
|
65
|
|
|
3
|
|
|
|
|
21
|
|
142
|
3
|
|
|
|
|
24
|
$res_h->header('Access-Control-Allow-Methods' => uc join ', ', @methods); |
143
|
|
|
|
|
|
|
} |
144
|
|
|
|
|
|
|
|
145
|
4
|
100
|
|
|
|
90
|
unless ($res_h->header('Access-Control-Max-Age')) { |
146
|
3
|
|
|
|
|
31
|
$res_h->header('Access-Control-Max-Age' => $c->stash('openapi_cors_default_max_age')); |
147
|
|
|
|
|
|
|
} |
148
|
|
|
|
|
|
|
} |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
sub _takeover_exchange_route { |
151
|
4
|
|
|
4
|
|
6
|
my ($self, $route) = @_; |
152
|
4
|
|
|
|
|
10
|
my $defaults = $route->to; |
153
|
|
|
|
|
|
|
|
154
|
4
|
50
|
|
|
|
55
|
return 0 if $defaults->{controller}; |
155
|
4
|
100
|
66
|
|
|
21
|
return 0 unless $defaults->{action} and $defaults->{action} eq 'openapi_plugin_cors_exchange'; |
156
|
1
|
50
|
|
|
|
3
|
return 0 unless grep { $_ eq 'OPTIONS' } @{$route->$methods}; |
|
1
|
|
|
|
|
9
|
|
|
1
|
|
|
|
|
3
|
|
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
$defaults->{cb} = sub { |
159
|
3
|
|
|
3
|
|
32385
|
my $c = shift; |
160
|
3
|
100
|
|
|
|
20
|
$c->openapi->valid_input or return; |
161
|
2
|
100
|
|
|
|
7
|
$c->req->headers->origin or return _render_bad_request($c, '/Origin'); |
162
|
1
|
|
|
|
|
140
|
$c->stash(openapi_cors_type => 'preflighted'); |
163
|
1
|
|
|
|
|
29
|
_set_default_headers($c); |
164
|
1
|
|
|
|
|
32
|
$c->render(data => '', status => 200); |
165
|
1
|
|
|
|
|
20
|
}; |
166
|
|
|
|
|
|
|
|
167
|
1
|
|
|
|
|
5
|
return 1; |
168
|
|
|
|
|
|
|
} |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
1; |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
=encoding utf8 |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=head1 NAME |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
Mojolicious::Plugin::OpenAPI::Cors - OpenAPI plugin for Cross-Origin Resource Sharing |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
=head1 SYNOPSIS |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
=head2 Application |
181
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
Set L to 1, if you want "Preflighted" CORS requests to |
183
|
|
|
|
|
|
|
be sent to your already existing actions. |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
$app->plugin(OpenAPI => {add_preflighted_routes => 1, %openapi_parameters}); |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
See L for what |
188
|
|
|
|
|
|
|
C<%openapi_parameters> might contain. |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
=head2 Simple exchange |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
The following example will automatically set default CORS response headers |
193
|
|
|
|
|
|
|
after validating the request against L: |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
package MyApp::Controller::User; |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
sub get_user { |
198
|
|
|
|
|
|
|
my $c = shift->openapi->cors_exchange->openapi->valid_input or return; |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
# Will only run this part if both the cors_exchange and valid_input was successful. |
201
|
|
|
|
|
|
|
$c->render(openapi => {user => {}}); |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
=head2 Using the specification |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
It's possible to enable preflight and simple CORS support directly in the |
207
|
|
|
|
|
|
|
specification. Here is one example: |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
"/user/{id}/posts": { |
210
|
|
|
|
|
|
|
"parameters": [ |
211
|
|
|
|
|
|
|
{ "in": "header", "name": "Origin", "type": "string", "pattern": "https?://example.com" } |
212
|
|
|
|
|
|
|
], |
213
|
|
|
|
|
|
|
"options": { |
214
|
|
|
|
|
|
|
"x-mojo-to": "#openapi_plugin_cors_exchange", |
215
|
|
|
|
|
|
|
"responses": { |
216
|
|
|
|
|
|
|
"200": { "description": "Cors exchange", "schema": { "type": "string" } } |
217
|
|
|
|
|
|
|
} |
218
|
|
|
|
|
|
|
}, |
219
|
|
|
|
|
|
|
"put": { |
220
|
|
|
|
|
|
|
"x-mojo-to": "user#add_post", |
221
|
|
|
|
|
|
|
"responses": { |
222
|
|
|
|
|
|
|
"200": { "description": "Add a new post.", "schema": { "type": "object" } } |
223
|
|
|
|
|
|
|
} |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
} |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
The special part can be found in the "OPTIONS" request It has the C |
228
|
|
|
|
|
|
|
key set to "#openapi_plugin_cors_exchange". This will enable |
229
|
|
|
|
|
|
|
L to take over the route and add a custom |
230
|
|
|
|
|
|
|
callback to validate the input headers using regular OpenAPI rules and respond |
231
|
|
|
|
|
|
|
with a "200 OK" and the default headers as listed under |
232
|
|
|
|
|
|
|
L if the input is valid. The only extra part that needs |
233
|
|
|
|
|
|
|
to be done in the C action is this: |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
sub add_post { |
236
|
|
|
|
|
|
|
my $c = shift->openapi->valid_input or return; |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
# Need to respond with a "Access-Control-Allow-Origin" header if |
239
|
|
|
|
|
|
|
# the input "Origin" header was validated |
240
|
|
|
|
|
|
|
$c->res->headers->access_control_allow_origin($c->req->headers->origin) |
241
|
|
|
|
|
|
|
if $c->req->headers->origin; |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
# Do the rest of your custom logic |
244
|
|
|
|
|
|
|
$c->respond(openapi => {}); |
245
|
|
|
|
|
|
|
} |
246
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
=head2 Custom exchange |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
If you need full control, you must pass a callback to |
250
|
|
|
|
|
|
|
L: |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
package MyApp::Controller::User; |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
sub get_user { |
255
|
|
|
|
|
|
|
# Validate incoming CORS request with _validate_cors() |
256
|
|
|
|
|
|
|
my $c = shift->openapi->cors_exchange("_validate_cors")->openapi->valid_input or return; |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
# Will only run this part if both the cors_exchange and valid_input was |
259
|
|
|
|
|
|
|
# successful. |
260
|
|
|
|
|
|
|
$c->render(openapi => {user => {}}); |
261
|
|
|
|
|
|
|
} |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
# This method must return undef on success. Any true value will be used as an error. |
264
|
|
|
|
|
|
|
sub _validate_cors { |
265
|
|
|
|
|
|
|
my $c = shift; |
266
|
|
|
|
|
|
|
my $req_h = $c->req->headers; |
267
|
|
|
|
|
|
|
my $res_h = $c->res->headers; |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
# The following "Origin" header check is the same for both simple and |
270
|
|
|
|
|
|
|
# preflighted. |
271
|
|
|
|
|
|
|
return "/Origin" unless $req_h->origin =~ m!^https?://whatever.example.com!; |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
# The following checks are only valid if preflighted... |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
# Check the Access-Control-Request-Headers header |
276
|
|
|
|
|
|
|
my $headers = $req_h->header('Access-Control-Request-Headers'); |
277
|
|
|
|
|
|
|
return "Bad stuff." if $headers and $headers =~ /X-No-Can-Do/; |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
# Check the Access-Control-Request-Method header |
280
|
|
|
|
|
|
|
my $method = $req_h->header('Access-Control-Request-Methods'); |
281
|
|
|
|
|
|
|
return "Not cool." if $method and $method eq "DELETE"; |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
# Set the following header for both simple and preflighted on success |
284
|
|
|
|
|
|
|
# or just let the auto-renderer handle it. |
285
|
|
|
|
|
|
|
$c->res->headers->access_control_allow_origin($req_h->origin); |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
# Set Preflighted response headers, instead of using the default |
288
|
|
|
|
|
|
|
if ($c->stash("openapi_cors_type") eq "preflighted") { |
289
|
|
|
|
|
|
|
$c->res->headers->header("Access-Control-Allow-Headers" => "X-Whatever, X-Something"); |
290
|
|
|
|
|
|
|
$c->res->headers->header("Access-Control-Allow-Methods" => "POST, GET, OPTIONS"); |
291
|
|
|
|
|
|
|
$c->res->headers->header("Access-Control-Max-Age" => 86400); |
292
|
|
|
|
|
|
|
} |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
# Return undef on success. |
295
|
|
|
|
|
|
|
return undef; |
296
|
|
|
|
|
|
|
} |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
=head1 DESCRIPTION |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
L is a plugin for accepting Preflighted or |
301
|
|
|
|
|
|
|
Simple Cross-Origin Resource Sharing requests. See |
302
|
|
|
|
|
|
|
L for more details. |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
This plugin is loaded by default by L. |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
Note that this plugin currently EXPERIMENTAL! Please comment on |
307
|
|
|
|
|
|
|
L if |
308
|
|
|
|
|
|
|
you have any feedback or create a new issue. |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=head1 STASH VARIABLES |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
The following "stash variables" can be set in L, |
313
|
|
|
|
|
|
|
L or L. |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
=head2 openapi_cors_allowed_origins |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
This variable should hold an array-ref of regexes that will be matched against |
318
|
|
|
|
|
|
|
the "Origin" header in case the default |
319
|
|
|
|
|
|
|
L is used. Examples: |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
$app->defaults(openapi_cors_allowed_origins => [qr{^https?://whatever.example.com}]); |
322
|
|
|
|
|
|
|
$c->stash(openapi_cors_allowed_origins => [qr{^https?://whatever.example.com}]); |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
=head2 openapi_cors_default_exchange_callback |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
This value holds a default callback that will be used by |
327
|
|
|
|
|
|
|
L, unless you pass on a C<$callback>. The default |
328
|
|
|
|
|
|
|
provided by this plugin will simply validate the C header against |
329
|
|
|
|
|
|
|
L. |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
Here is an example to allow every "Origin" |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
$app->defaults(openapi_cors_default_exchange_callback => sub { |
334
|
|
|
|
|
|
|
my $c = shift; |
335
|
|
|
|
|
|
|
$c->res->headers->header("Access-Control-Allow-Origin" => "*"); |
336
|
|
|
|
|
|
|
return undef; |
337
|
|
|
|
|
|
|
}); |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
=head2 openapi_cors_default_max_age |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
Holds the default value for the "Access-Control-Max-Age" response header |
342
|
|
|
|
|
|
|
set by L. Examples: |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
$app->defaults(openapi_cors_default_max_age => 86400); |
345
|
|
|
|
|
|
|
$c->stash(openapi_cors_default_max_age => 86400); |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
Default value is 1800. |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
=head2 openapi_cors_type |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
This stash variable is available inside the callback passed on to |
352
|
|
|
|
|
|
|
L. It will be either "preflighted", "real" or "simple". |
353
|
|
|
|
|
|
|
"real" is the type that comes after "preflighted" when the actual request |
354
|
|
|
|
|
|
|
is sent to the server, but with "Origin" header set. |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
=head1 HELPERS |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
=head2 openapi.cors_exchange |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
$c = $c->openapi->cors_exchange($callback); |
361
|
|
|
|
|
|
|
$c = $c->openapi->cors_exchange("MyApp::cors_validator"); |
362
|
|
|
|
|
|
|
$c = $c->openapi->cors_exchange("_some_controller_method"); |
363
|
|
|
|
|
|
|
$c = $c->openapi->cors_exchange(sub { ... }); |
364
|
|
|
|
|
|
|
$c = $c->openapi->cors_exchange; |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
Used to validate either a simple CORS request, preflighted CORS request or a |
367
|
|
|
|
|
|
|
real request. It will be called as soon as the "Origin" request header is seen. |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
The C<$callback> will be called with the current L |
370
|
|
|
|
|
|
|
object and must return an error or C on success: |
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
my $error = $callback->($c); |
373
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
The C<$error> must be in one of the following formats: |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
=over 2 |
377
|
|
|
|
|
|
|
|
378
|
|
|
|
|
|
|
=item * C |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
Returning C means that the CORS request is valid. |
381
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
=item * A string starting with "/" |
383
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
Shortcut for generating a 400 Bad Request response with a header name. Example: |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
return "/Access-Control-Request-Headers"; |
387
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
=item * Any other string |
389
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
Used to generate a 400 Bad Request response with a completely custom message. |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
=item * An array-ref |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
Used to generate a completely custom 400 Bad Request response. Example: |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
return [{message => "Some error!", path => "/Whatever"}]; |
397
|
|
|
|
|
|
|
return [{message => "Some error!"}]; |
398
|
|
|
|
|
|
|
return [JSON::Validator::Error->new]; |
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
=back |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
On success, the following headers will be set, unless already set by |
403
|
|
|
|
|
|
|
C<$callback>: |
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
=over 2 |
406
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
=item * Access-Control-Allow-Headers |
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
Set to the header of the incoming "Access-Control-Request-Headers" header. |
410
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
=item * Access-Control-Allow-Methods |
412
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
Set to the list of HTTP methods defined in the OpenAPI spec for this path. |
414
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
=item * Access-Control-Allow-Origin |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
Set to the "Origin" header in the request. |
418
|
|
|
|
|
|
|
|
419
|
|
|
|
|
|
|
=item * Access-Control-Max-Age |
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
Set to L. |
422
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
=back |
424
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
=head1 METHODS |
426
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
=head2 register |
428
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
Called by L. |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
=head1 SEE ALSO |
432
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
L. |
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
=cut |