You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
4.9 KiB
166 lines
4.9 KiB
package NGCP::Panel::Role::API;
|
|
use Moose::Role;
|
|
use Sipwise::Base;
|
|
|
|
use JSON qw();
|
|
use HTTP::Status qw(:constants);
|
|
use Safe::Isa qw($_isa);
|
|
use Try::Tiny;
|
|
use Digest::SHA3 qw(sha3_256_base64);
|
|
use DateTime::Format::HTTP qw();
|
|
use DateTime::Format::RFC3339 qw();
|
|
use Types::Standard qw(InstanceOf);
|
|
use Regexp::Common qw(delimited); # $RE{delimited}
|
|
use NGCP::Panel::Utils::ValidateJSON qw();
|
|
|
|
has('last_modified', is => 'rw', isa => InstanceOf['DateTime']);
|
|
|
|
sub get_valid_post_data {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $media_type = $params{media_type};
|
|
|
|
return unless $self->forbid_link_header($c);
|
|
return unless $self->valid_media_type($c, $media_type);
|
|
return unless $self->require_body($c);
|
|
my $json = do { local $/; $c->request->body->getline }; # slurp
|
|
return unless $self->require_wellformed_json($c, $media_type, $json);
|
|
|
|
return JSON::from_json($json);
|
|
}
|
|
|
|
sub validate_form {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $resource = $params{resource};
|
|
my $form = $params{form};
|
|
|
|
my @normalized = ();
|
|
|
|
# move {xxx_id} into {xxx}{id} for FormHandler
|
|
foreach my $key(keys %{ $resource } ) {
|
|
if($key =~ /^([a-z]+)_id$/) {
|
|
push @normalized, $1;
|
|
$resource->{$1}{id} = delete $resource->{$key};
|
|
}
|
|
}
|
|
|
|
use Data::Printer; p $resource;
|
|
|
|
# remove unknown keys
|
|
my %fields = map { $_->name => undef } $form->fields;
|
|
for my $k (keys %{ $resource }) {
|
|
unless(exists $fields{$k}) {
|
|
$c->log->info("deleting unknown key '$k' from message"); # TODO: user, message trace, ...
|
|
delete $resource->{$k};
|
|
}
|
|
$resource->{$k} = DateTime::Format::RFC3339->format_datetime($resource->{$k})
|
|
if $resource->{$k}->$_isa('DateTime');
|
|
}
|
|
|
|
# check keys/vals
|
|
my $result = $form->run(params => $resource);
|
|
if ($result->error_results->size) {
|
|
my $e = $result->error_results->map(sub {
|
|
sprintf 'field=\'%s\', input=\'%s\', errors=\'%s\'', $_->name, $_->input // '', $_->errors->join(q())
|
|
})->join("; ");
|
|
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Validation failed. $e");
|
|
return;
|
|
}
|
|
|
|
# move {xxx}{id} back into {xxx_id} for DB
|
|
foreach my $key(@normalized) {
|
|
$resource->{$key . '_id'} = $resource->{$key}{id};
|
|
delete $resource->{$key};
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
# private
|
|
|
|
sub error {
|
|
my ($self, $c, $code, $message) = @_;
|
|
|
|
$c->log->error("error $code - $message"); # TODO: user, trace etc
|
|
|
|
$c->response->content_type('application/json');
|
|
$c->response->status($code);
|
|
$c->response->body(JSON::to_json({ code => $code, message => $message })."\n");
|
|
}
|
|
|
|
sub forbid_link_header {
|
|
my ($self, $c) = @_;
|
|
return 1 unless $c->request->header('Link');
|
|
$self->error($c, HTTP_BAD_REQUEST, "The request must not contain 'Link' headers. Instead assert relationships in the entity body.");
|
|
return;
|
|
}
|
|
|
|
sub valid_media_type {
|
|
my ($self, $c, $media_type) = @_;
|
|
return 1 if($c->request->header('Content-Type') &&
|
|
index($c->request->header('Content-Type'), $media_type) == 0);
|
|
$self->error($c, HTTP_UNSUPPORTED_MEDIA_TYPE, "Unsupported media type, accepting '$media_type' only.");
|
|
return;
|
|
}
|
|
|
|
sub require_body {
|
|
my ($self, $c) = @_;
|
|
return 1 if $c->request->body;
|
|
$self->error($c, HTTP_BAD_REQUEST, "This request is missing a message body.");
|
|
return;
|
|
}
|
|
|
|
sub require_wellformed_json {
|
|
my ($self, $c, $media_type, $patch) = @_;
|
|
try {
|
|
NGCP::Panel::Utils::ValidateJSON->new($patch);
|
|
} catch {
|
|
$self->error($c, HTTP_BAD_REQUEST, "The entity is not a well-formed '$media_type' document. $_");
|
|
return;
|
|
};
|
|
return 1;
|
|
}
|
|
|
|
sub cached {
|
|
my ($self, $c) = @_;
|
|
my $response = $c->cache->get($c->request->uri->canonical->as_string);
|
|
unless ($response) {
|
|
$c->log->info('not cached');
|
|
return;
|
|
}
|
|
my $matched_tag = $c->request->header('If-None-Match') && ('*' eq $c->request->header('If-None-Match'))
|
|
|| (grep {$response->header('ETag') eq $_} Data::Record->new({
|
|
split => qr/\s*,\s*/, unless => $RE{delimited}{-delim => q(")},
|
|
})->records($c->request->header('If-None-Match')));
|
|
my $not_modified = $c->request->header('If-Modified-Since')
|
|
&& !($self->last_modified < DateTime::Format::HTTP->parse_datetime($c->request->header('If-Modified-Since')));
|
|
if (
|
|
$matched_tag && $not_modified
|
|
|| $matched_tag
|
|
|| $not_modified
|
|
) {
|
|
$c->response->status(HTTP_NOT_MODIFIED);
|
|
$c->response->headers($response->headers);
|
|
$c->log->info('cached');
|
|
return 1;
|
|
}
|
|
$c->log->info('stale');
|
|
return;
|
|
}
|
|
|
|
sub etag {
|
|
my ($self, $octets) = @_;
|
|
return sprintf '"ni:/sha3-256;%s"', sha3_256_base64($octets);
|
|
}
|
|
|
|
sub expires {
|
|
my ($self) = @_;
|
|
return DateTime->now->clone->add(years => 1); # XXX insert product end-of-life
|
|
}
|
|
|
|
1;
|
|
# vim: set tabstop=4 expandtab:
|