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.
425 lines
11 KiB
425 lines
11 KiB
package NGCP::BulkProcessor::RestConnectors::NGCPRestApi;
|
|
use strict;
|
|
|
|
## no critic
|
|
|
|
use threads qw();
|
|
use threads::shared qw(shared_clone);
|
|
|
|
use HTTP::Status qw(:constants :is status_message);
|
|
|
|
use Encode qw();
|
|
|
|
use JSON -support_by_pp, -no_export;
|
|
use IO::Uncompress::Unzip qw();
|
|
|
|
use NGCP::BulkProcessor::Globals qw(
|
|
$LongReadLen_limit
|
|
$working_path
|
|
);
|
|
use NGCP::BulkProcessor::Logging qw(
|
|
getlogger
|
|
restdebug
|
|
restinfo
|
|
);
|
|
use NGCP::BulkProcessor::LogError qw(
|
|
resterror
|
|
restwarn
|
|
restrequesterror
|
|
restresponseerror
|
|
fileerror
|
|
);
|
|
|
|
use NGCP::BulkProcessor::RestConnector qw(_add_headers convert_bools);
|
|
|
|
use NGCP::BulkProcessor::Calendar qw(get_fake_now_string);
|
|
|
|
use NGCP::BulkProcessor::Utils qw(makepath cleanupdir booltostring);
|
|
|
|
require Exporter;
|
|
our @ISA = qw(Exporter NGCP::BulkProcessor::RestConnector);
|
|
our @EXPORT_OK = qw(
|
|
$ITEM_REL_PARAM
|
|
cleanupcertfiles
|
|
);
|
|
|
|
my $defaulturi = 'https://127.0.0.1:443';
|
|
my $defaultusername = 'administrator';
|
|
my $defaultpassword = 'administrator';
|
|
my $defaultrealm = 'api_admin_http';
|
|
my $defaultcrtauth = 1;
|
|
my $API_CERT_DIR = 'apicerts/';
|
|
my $API_CERT_FILENAME_FORMAT = '%s%s.pem';
|
|
|
|
my $default_collection_page_size = 10;
|
|
my $first_collection_page_num = 1;
|
|
my $first_page_num = 1;
|
|
|
|
my $contenttype = 'application/json';
|
|
my $patchcontenttype = 'application/json-patch+json';
|
|
|
|
my $defaultfaketime = 0;
|
|
my $faketime_header = 'X-Fake-Clienttime';
|
|
|
|
our $ITEM_REL_PARAM = 'item_rel';
|
|
#my $logger = getlogger(__PACKAGE__);
|
|
|
|
my $request_charset = 'utf-8';
|
|
my $response_charset = 'utf-8';
|
|
|
|
my $timeout = 5*60;
|
|
|
|
sub _get_api_cert_dir {
|
|
return $working_path . $API_CERT_DIR;
|
|
}
|
|
|
|
sub cleanupcertfiles {
|
|
|
|
return cleanupdir(_get_api_cert_dir(),1,\&filewarn,getlogger(__PACKAGE__));
|
|
|
|
}
|
|
|
|
sub new {
|
|
|
|
my $class = shift;
|
|
|
|
my $self = NGCP::BulkProcessor::RestConnector->new(@_);
|
|
|
|
bless($self,$class);
|
|
|
|
my $lock :shared;
|
|
$self->{lock_ref} = \$lock;
|
|
|
|
$self->setup();
|
|
|
|
restdebug($self,__PACKAGE__ . ' connector created',getlogger(__PACKAGE__));
|
|
|
|
return $self;
|
|
|
|
}
|
|
|
|
sub setup {
|
|
|
|
my $self = shift;
|
|
my ($baseuri,$username,$password,$realm,$crtauth,$faketime) = @_;
|
|
$self->baseuri($baseuri // $defaulturi);
|
|
$self->{username} = $username // $defaultusername;
|
|
$self->{password} = $password // $defaultpassword;
|
|
$self->{realm} = $realm // $defaultrealm;
|
|
$self->{faketime} = $faketime // $defaultfaketime;
|
|
|
|
$self->{crtauth} = $crtauth // $defaultcrtauth;
|
|
$self->{crt_path} = undef;
|
|
|
|
}
|
|
|
|
sub connectidentifier {
|
|
|
|
my $self = shift;
|
|
if ($self->{uri}) {
|
|
return ($self->{username} ? $self->{username} . '@' : '') . $self->{uri};
|
|
} else {
|
|
return undef;
|
|
}
|
|
|
|
}
|
|
|
|
sub _setup_ua {
|
|
|
|
my $self = shift;
|
|
my ($ua,$netloc) = @_;
|
|
$ua->ssl_opts(
|
|
verify_hostname => 0,
|
|
SSL_verify_mode => 0,
|
|
);
|
|
$ua->timeout($timeout) if $timeout;
|
|
if ($self->{username}) {
|
|
$ua->credentials($netloc, $self->{realm}, $self->{username}, $self->{password});
|
|
}
|
|
$self->_init_ssl_cert($ua) if $self->{crtauth};
|
|
restdebug($self,"ua configured",getlogger(__PACKAGE__));
|
|
|
|
}
|
|
|
|
sub _init_ssl_cert {
|
|
my ($self, $ua) = @_;
|
|
lock ${$self->{lock_ref}};
|
|
if (not defined $self->{crt_path}) { #($crtauth // $defaultcrtauth) && $self->{username}) {
|
|
makepath(_get_api_cert_dir(),\&fileerror,getlogger(__PACKAGE__));
|
|
$self->{crt_path} = sprintf($API_CERT_FILENAME_FORMAT,_get_api_cert_dir(),$self->{username}=~s/[^a-z0-9 _]/_/gir);
|
|
}
|
|
unless(-f $self->{crt_path}) {
|
|
my $res = $ua->post(
|
|
$self->_get_request_uri('/api/admincerts/'),
|
|
Content_Type => $contenttype,
|
|
Content => '{}'
|
|
);
|
|
if ($res->is_success) {
|
|
my $zip = $res->decoded_content;
|
|
my $z = IO::Uncompress::Unzip->new(\$zip, MultiStream => 0, Append => 1);
|
|
my $data;
|
|
while(!$z->eof() && (my $hdr = $z->getHeaderInfo())) {
|
|
unless($hdr->{Name} =~ /\.pem$/) {
|
|
# wrong file, just read stream, clear buffer and try next
|
|
while($z->read($data) > 0) {}
|
|
$data = undef;
|
|
$z->nextStream();
|
|
next;
|
|
}
|
|
while($z->read($data) > 0) {}
|
|
last;
|
|
}
|
|
$z->close();
|
|
if ($data) {
|
|
if (open(my $fh,">:raw",$self->{crt_path})) {
|
|
print $fh $data;
|
|
close $fh;
|
|
restdebug($self,"$self->{crt_path} saved",getlogger(__PACKAGE__));
|
|
} else {
|
|
fileerror("failed to open $self->{crt_path}: $!",getlogger(__PACKAGE__));
|
|
}
|
|
} else {
|
|
resterror($self,'failed to find PEM file in client certificate zip file',getlogger(__PACKAGE__));
|
|
}
|
|
} else {
|
|
resterror($self,'failed to fetch client certificate: '. $res->code . ' ' . $res->message,getlogger(__PACKAGE__));
|
|
}
|
|
} else {
|
|
restdebug($self,"$self->{crt_path} found",getlogger(__PACKAGE__));
|
|
}
|
|
$ua->ssl_opts(
|
|
SSL_cert_file => $self->{crt_path},
|
|
SSL_key_file => $self->{crt_path},
|
|
);
|
|
}
|
|
|
|
sub _encode_request_content {
|
|
my $self = shift;
|
|
my ($data) = @_;
|
|
return Encode::encode($request_charset,JSON::to_json($data,{ allow_nonref => 1, allow_blessed => 1, convert_blessed => 1, pretty => 0, }));
|
|
}
|
|
|
|
sub _decode_response_content {
|
|
my $self = shift;
|
|
my ($data) = @_;
|
|
my $decoded;
|
|
$decoded = JSON::from_json(Encode::decode($response_charset,$data),{ allow_nonref => 1, }) if $data;
|
|
convert_bools($decoded);
|
|
return $decoded // $data;
|
|
}
|
|
|
|
sub _add_post_headers {
|
|
my $self = shift;
|
|
my ($req,$headers) = @_;
|
|
_add_headers($req,{
|
|
'Content-Type' => $contenttype,
|
|
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
|
|
});
|
|
# allow providing custom headers to post(),
|
|
# e.g { 'X-Fake-Clienttime' => ... }
|
|
$self->SUPER::_add_post_headers($req,$headers);
|
|
}
|
|
|
|
sub _add_get_headers {
|
|
my $self = shift;
|
|
my ($req,$headers) = @_;
|
|
_add_headers($req,{
|
|
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
|
|
});
|
|
$self->SUPER::_add_get_headers($req,$headers);
|
|
}
|
|
|
|
sub _add_patch_headers {
|
|
my $self = shift;
|
|
my ($req,$headers) = @_;
|
|
_add_headers($req,{
|
|
'Prefer' => 'return=representation',
|
|
'Content-Type' => $patchcontenttype,
|
|
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
|
|
});
|
|
$self->SUPER::_add_patch_headers($req,$headers);
|
|
}
|
|
|
|
sub _encode_patch_content {
|
|
my $self = shift;
|
|
my ($data) = @_;
|
|
return JSON::to_json(
|
|
[ map { local $_ = $_; { op => 'replace', path => '/'.$_ , value => $data->{$_} }; } keys %$data ]
|
|
);
|
|
}
|
|
|
|
sub _add_put_headers {
|
|
my $self = shift;
|
|
my ($req,$headers) = @_;
|
|
_add_headers($req,{
|
|
'Prefer' => 'return=representation',
|
|
'Content-Type' => $contenttype,
|
|
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
|
|
});
|
|
$self->SUPER::_add_put_headers($req,$headers);
|
|
}
|
|
|
|
sub _add_delete_headers {
|
|
my $self = shift;
|
|
my ($req,$headers) = @_;
|
|
_add_headers($req,{
|
|
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
|
|
});
|
|
$self->SUPER::_add_delete_headers($req,$headers);
|
|
}
|
|
|
|
sub _get_page_num_query_param {
|
|
my $self = shift;
|
|
my ($page_num) = @_;
|
|
if (defined $page_num and length($page_num) > 0) {
|
|
return 'page=' . $page_num;
|
|
}
|
|
return undef;
|
|
#if (defined $page_num) {
|
|
# $page_num += $first_page_num;
|
|
#} else {
|
|
# $page_num = $first_page_num;
|
|
#}
|
|
#return (defined $page_num ? 'p=' . $page_num : undef);
|
|
}
|
|
|
|
sub _get_page_size_query_param {
|
|
my $self = shift;
|
|
my ($page_size) = @_;
|
|
$page_size //= $default_collection_page_size;
|
|
return 'size=' . $page_size;
|
|
}
|
|
|
|
sub _get_total_count_expected_query_param {
|
|
my $self = shift;
|
|
my ($total_count_expected) = @_;
|
|
return ($total_count_expected ? '' : 'no_count=' . booltostring(1));
|
|
}
|
|
|
|
sub extract_collection_items {
|
|
my $self = shift;
|
|
my ($data,$page_size,$page_num,$params) = @_;
|
|
my $result = undef;
|
|
if (defined $data and 'HASH' eq ref $data
|
|
and defined $data->{'_embedded'} and 'HASH' eq ref $data->{'_embedded'}) {
|
|
$result = $data->{'_embedded'}->{$params->{$ITEM_REL_PARAM}};
|
|
if ('ARRAY' eq ref $result) {
|
|
|
|
} elsif ('HASH' eq ref $result) {
|
|
$result = [ $result ];
|
|
} else {
|
|
undef $result;
|
|
}
|
|
}
|
|
$result //= [];
|
|
return shared_clone($result);
|
|
}
|
|
|
|
sub get_defaultcollectionpagesize {
|
|
my $self = shift;
|
|
return $default_collection_page_size;
|
|
}
|
|
|
|
sub get_firstcollectionpagenum {
|
|
my $self = shift;
|
|
return $first_collection_page_num;
|
|
}
|
|
|
|
sub _request_error {
|
|
my $self = shift;
|
|
my $msg = undef;
|
|
if (defined $self->responsedata()
|
|
and 'HASH' eq ref $self->responsedata()) {
|
|
$msg = $self->responsedata()->{'message'};
|
|
}
|
|
resterror($self,$self->response->code . ' ' . $self->response->message .
|
|
(defined $msg && length($msg) > 0 ? ': ' . $msg : ''),getlogger(__PACKAGE__));
|
|
}
|
|
|
|
sub _extract_ids_from_response_location {
|
|
|
|
my $self = shift;
|
|
my $location = $self->response()->header('Location');
|
|
my @ids = ();
|
|
foreach my $segment (split('/',$location)) {
|
|
push(@ids,$segment) if $segment =~ /^\d+$/;
|
|
}
|
|
return @ids;
|
|
|
|
}
|
|
|
|
sub get {
|
|
my $self = shift;
|
|
if ($self->_get(@_)->code() != HTTP_OK) {
|
|
$self->_request_error();
|
|
return undef;
|
|
} else {
|
|
return $self->responsedata();
|
|
}
|
|
}
|
|
|
|
sub post {
|
|
my $self = shift;
|
|
if ($self->_post(@_)->code() != HTTP_CREATED) {
|
|
$self->_request_error();
|
|
return ();
|
|
} else {
|
|
return $self->_extract_ids_from_response_location();
|
|
}
|
|
}
|
|
|
|
sub post_get {
|
|
my $self = shift;
|
|
my ($path_query,$post_headers,$get_headers) = @_;
|
|
if ($self->_post($path_query,$post_headers)->code() != HTTP_CREATED) {
|
|
$self->_request_error();
|
|
return undef;
|
|
} else {
|
|
my @ids = $self->_extract_ids_from_response_location();
|
|
my $item = $self->get($self->response()->header('Location'),$get_headers);
|
|
$item->{id} = $ids[0] if (scalar @ids) > 0;
|
|
return $item;
|
|
}
|
|
}
|
|
|
|
sub put {
|
|
my $self = shift;
|
|
if ($self->_put(@_)->code() != HTTP_OK) {
|
|
$self->_request_error();
|
|
return undef;
|
|
} else {
|
|
return $self->responsedata();
|
|
}
|
|
}
|
|
|
|
sub patch {
|
|
my $self = shift;
|
|
if ($self->_patch(@_)->code() != HTTP_OK) {
|
|
$self->_request_error();
|
|
return undef;
|
|
} else {
|
|
return $self->responsedata();
|
|
}
|
|
}
|
|
|
|
sub delete {
|
|
my $self = shift;
|
|
if ($self->_delete(@_)->code() != HTTP_NO_CONTENT) {
|
|
$self->_request_error();
|
|
return 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
sub faketime {
|
|
my $self = shift;
|
|
if (@_) {
|
|
$self->{faketime} = shift;
|
|
restdebug($self,"fake time " . ($self->{faketime} ? 'enabled' : 'disabled'),getlogger(__PACKAGE__));
|
|
}
|
|
return $self->{faketime};
|
|
}
|
|
|
|
1;
|