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;