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.
ngcp-panel/lib/NGCP/Panel/Role/API.pm

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: