Skip to content

Commit

Permalink
KD-2391: CORS - Integrate CORS into Mojolicious::Plugin::OpenAPI
Browse files Browse the repository at this point in the history
  • Loading branch information
Olli-Antti Kivilahti authored and Lari Taskula committed Dec 12, 2017
1 parent 6b29f29 commit 9bab266
Show file tree
Hide file tree
Showing 4 changed files with 473 additions and 7 deletions.
47 changes: 40 additions & 7 deletions Mojolicious/Plugin/OpenAPI.pm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use Mojo::Base 'Mojolicious::Plugin';
use JSON::Validator::OpenAPI::Mojolicious;
use Mojo::JSON;
use Mojo::Util 'deprecated';
use Mojolicious::Plugin::OpenAPI::CORS;
use constant DEBUG => $ENV{MOJO_OPENAPI_DEBUG} || 0;

our $VERSION = '1.17';
Expand Down Expand Up @@ -48,8 +49,11 @@ sub _add_routes {
my $route_prefix = "";
my %uniq;

my $placeholder = $api_spec->get('/x-mojo-placeholder') || ':';
$route = $route->any($base_path) if $route and !$route->pattern->unparsed;
$route = $app->routes->any($base_path) unless $route;
my $xcors = Mojolicious::Plugin::OpenAPI::CORS->use_CORS($app, $self);
Mojolicious::Plugin::OpenAPI::CORS->set_default_CORS($route, $xcors) if $xcors;
$base_path = $api_spec->data->{basePath} = $route->to_string;
$base_path =~ s!/$!!;

Expand All @@ -64,19 +68,25 @@ sub _add_routes {

for my $path (_sort_paths(keys %$paths)) {
next if $path =~ $X_RE;
my @http_methods;
my @parameters = @{$paths->{$path}{parameters} || []};
my $route_path = $path;
my $has_options;

for my $http_method (keys %{$paths->{$path}}) {
next if $http_method =~ $X_RE or $http_method eq 'parameters';
push @http_methods, $http_method;
my $op_spec = $paths->{$path}{$http_method};
my $name = $op_spec->{'x-mojo-name'} || $op_spec->{operationId};
my $to = $op_spec->{'x-mojo-to'};
my $endpoint;

$has_options = 1 if lc $http_method eq 'options';
$route_path = _route_path($path, $op_spec);
$route_path = _route_path($path, $op_spec, $placeholder);

my $xcors = Mojolicious::Plugin::OpenAPI::CORS->use_CORS($app, $self);
my $route_params = {}; #Add params for the route here
Mojolicious::Plugin::OpenAPI::CORS->get_opts($route_params, $xcors, $route_path, $op_spec) if $xcors; #Set CORS options to $route_params

die qq([OpenAPI] operationId "$op_spec->{operationId}" is not unique)
if $op_spec->{operationId} and $uniq{o}{$op_spec->{operationId}}++;
Expand All @@ -89,7 +99,7 @@ sub _add_routes {
$route->add_child($endpoint);
}
if (!$endpoint) {
$endpoint = $route->$http_method($route_path);
$endpoint = $route->$http_method($route_path, $route_params);
$endpoint->name("$route_prefix$name") if $name;
}

Expand All @@ -99,7 +109,18 @@ sub _add_routes {
}

unless ($has_options) {
$route->options($route_path => sub { _render_route_spec($_[0], $path) });
my $route_params = {};
my $xcors = Mojolicious::Plugin::OpenAPI::CORS->use_CORS($app, $self);
Mojolicious::Plugin::OpenAPI::CORS->get_opts($route_params, $xcors, $route_path, $paths->{$path}) if $xcors;
$route_params->{available_methods} = \@http_methods;
$route_params->{path_spec} = $paths->{$path};
$route->options($route_path => sub {
my $c = shift;
$c->res->headers->header('Allow' => $c->stash('available_methods'));
my $errors = Mojolicious::Plugin::OpenAPI::CORS->handle_preflight_cors($c);
$errors ? $c->render(status => 200, json => $errors)
: _render_route_spec($c, $path);
}, $route_params);
}
}
}
Expand Down Expand Up @@ -243,12 +264,14 @@ sub _reply_spec {
}

sub _route_path {
my ($path, $op_spec) = @_;
my ($path, $op_spec, $placeholder) = @_;
my %parameters = map { ($_->{name}, $_) } @{$op_spec->{parameters} || []};
$path =~ s/{([^}]+)}/{
my $pname = $1;
my $type = $parameters{$pname}{'x-mojo-placeholder'} || ':';
"($type$pname)";
my $name = $1;
my $type = (%parameters && $parameters{$name})
? $parameters{$name}{'x-mojo-placeholder'} || $placeholder
: $placeholder;
"($type$name)";
}/ge;
return $path;
}
Expand Down Expand Up @@ -277,6 +300,14 @@ sub _validate {
my $self = _self($c);
my $op_spec = $c->openapi->spec;
my $cors_errors = Mojolicious::Plugin::OpenAPI::CORS->handle_simple_cors($c);
if ($cors_errors) {
$self->_log($c, '<<<', $cors_errors);
$c->render(data => $self->{renderer}->($c, {errors => $cors_errors, status => 403}), status => 403)
if $args->{auto_render} // 1;
return @$cors_errors;
}
# Write validated data to $c->validation->output
my @errors = $self->_validator->validate_request($c, $op_spec, $c->validation->output);
Expand Down Expand Up @@ -533,6 +564,8 @@ the terms of the Artistic License version 2.0.
=item * L<Mojolicious::Plugin::OpenAPI::Guides::Tutorial>
=item * L<Mojolicious::Plugin::OpenAPI::CORS> - CORS support
=item * L<http://thorsen.pm/perl/programming/2015/07/05/mojolicious-swagger2.html>.
=item * L<OpenAPI specification|https://openapis.org/specification>
Expand Down
21 changes: 21 additions & 0 deletions t/Mojolicious/Plugin/OpenAPI/CORS/Api.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package t::Mojolicious::Plugin::OpenAPI::CORS::Api;
use Mojo::Base 'Mojolicious::Controller';

sub add_pet {
my $c = shift->openapi->valid_input or return;
$c->render(openapi => $c->validation->params->to_hash, status => 200);
}
sub cors_list_pets {
my $c = shift->openapi->valid_input or return;
$c->render(openapi => {pet1 => 'George', pet2 => 'Georgina'}, status => 200);
}
sub cors_list_humans {
my $c = shift->openapi->valid_input or return;
$c->render(openapi => {pet1 => 'George', pet2 => 'Georgina'}, status => 200);
}
sub cors_delete_pets {
my $c = shift->openapi->valid_input or return;
$c->render(openapi => {delete => 'ok'}, status => 204);
}

1;
53 changes: 53 additions & 0 deletions t/Mojolicious/Plugin/OpenAPI/CORS/Helpers.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package t::Mojolicious::Plugin::OpenAPI::CORS::Helpers;

=head1 IN THIS FILE
We implement test subroutines to test CORS operations.
=cut

=head2 origin_whitelist
Used to test the "x-cors-access-control-allow-origin-list" CORS option.
@param {Mojolicious::Controller} $c
@param {String} $origin, the origin to accept or deny.
@returns {String or undef}, The $origin if it is accepted or undef.
=cut

use Scalar::Util qw(blessed);

sub origin_whitelist {
my ($c, $origin) = @_;
my @cc = caller(0);
die $cc[3]."($c, $origin):> \$c '$c' is not a Mojolicious::Controller!" unless(blessed($c) && $c->isa('Mojolicious::Controller'));
return $origin if($origin && $origin =~ /example/);
return undef;
}

=head2 fake_authenticate
Implements the OpenAPI::Guides::ProtectedApi to authenticate using x-mojo-around-action
By default fails all requests with HTTP status 401
Increments $ENV{'OPENAPI-CORS-FAKE-AUTHENTICATE'} every time this subroutine is called.
@returns {undef} but renders 401 and JSON error.
=cut

sub fake_authenticate {
my ($next, $c, $opObj) = @_;

$ENV{'OPENAPI-CORS-FAKE-AUTHENTICATE'} = 0 unless $ENV{'OPENAPI-CORS-FAKE-AUTHENTICATE'};
$ENV{'OPENAPI-CORS-FAKE-AUTHENTICATE'}++;

return $c->render(
json => {errors => [{message => "Always fail auth", path => "/"}]},
status => 401
);
}

1; #Make compiler happy!
Loading

0 comments on commit 9bab266

Please sign in to comment.