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/Utils/DeviceBootstrap/Snom.pm

428 lines
16 KiB

package NGCP::Panel::Utils::DeviceBootstrap::Snom;
use strict;
use warnings;
use URI::Escape;
use Moo;
use Types::Standard qw(Str);
use JSON qw/encode_json decode_json/;
use MIME::Base64;
use Digest::MD5 qw/md5_hex/;
use Digest::SHA qw(hmac_sha256_base64);
use URI;
use TryCatch;
use Data::Dumper;
use Data::UUID;
extends 'NGCP::Panel::Utils::DeviceBootstrap::VendorREST';
sub rpc_server_params {
my $self = shift;
my $cfg = {
proto => 'https',
host => 'secure-provisioning.snom.com',
path => 'api/v1',
};
$self->{rpc_server_params} = $cfg;
return $self->{rpc_server_params};
}
sub rest_prepare_request {
my ($self, $action) = @_;
my $c = $self->params->{c};
my ($op_name, $url, $ret, $res, $data, $rc, $err);
my $new_mac = $self->content_params->{mac};
my $old_mac = $self->content_params->{mac_old};
my $param_uri = $self->content_params->{uri};
my $credentials = {
id => $self->params->{credentials}->{user},
key => $self->params->{credentials}->{password}
};
my $company_url;
my $tx_id = $c->session->{api_request_tx_id} //
uc Data::UUID->create_str() =~ s/-//gr;
$self->{rpc_server_params} //= $self->rpc_server_params;
my $cfg = $self->{rpc_server_params};
$c->log->debug($self->to_log({ name => 'Snom prepare request',
tx_id => $tx_id,
action => $action }));
# first, get company url --------------------------------------------------
$op_name = 'Snom get tokens';
$err = '';
$url = "$$cfg{proto}://$$cfg{host}/$$cfg{path}/tokens/".$credentials->{id};
$c->log->debug($self->to_log({ name => $op_name,
tx_id => $tx_id,
url => $url }));
($data, $rc) = $self->send_http_request($c, $tx_id, $url, 'GET', $credentials);
if ($rc == 0 && $data && ref $data eq 'HASH' && $data->{links}->{company}) {
$company_url = $data->{links}->{company};
$c->log->debug($self->to_log({ name => $op_name,
status => 'success',
tx_id => $tx_id,
url => $url,
data => $self->data_to_str($data) }));
$url = $data->{links}->{company};
} else {
$rc = 1;
}
$c->log->debug($self->to_log({ name => 'Snom get tokens',
status => $rc ? 'failed' : 'success',
tx_id => $tx_id,
url => $url,
data => $self->data_to_str($data) }));
return if $rc;
#--------------------------------------------------------------------------
if ($action eq 'register_content') {
# get product groups --------------------------------------------------
my ($product_group_id, $setting_id);
$op_name = 'Snom get product groups';
$err = '';
$url = "$$cfg{proto}://$$cfg{host}/$$cfg{path}/product-groups/";
$c->log->debug($self->to_log({ name => $op_name,
tx_id => $tx_id,
url => $url }));
($data, $rc) = $self->send_http_request($c, $tx_id, $url, 'GET', $credentials);
if ($rc == 0 && $data && ref $data eq 'ARRAY') {
my ($product_group) = grep {$_->{name} eq $self->params->{redirect_params}->{product_family}} @$data;
if ($product_group) {
$product_group_id = $product_group->{uuid};
}
else {
$err = 'specified product family not found';
$rc = 1;
}
} else {
$rc = 1;
}
$c->log->debug($self->to_log({ name => $op_name,
status => $rc ? 'failed' : 'success',
tx_id => $tx_id,
url => $url,
msg => $err // '',
data => $self->data_to_str($data) }));
return if $rc;
# get settings ---------------------------------------------------------
$op_name = 'Snom get settings';
$err = '';
$url = "$$cfg{proto}://$$cfg{host}/$$cfg{path}/settings/";
$c->log->debug($self->to_log({ name => $op_name,
tx_id => $tx_id,
url => $url }));
($data, $rc) = $self->send_http_request($c, $tx_id, $url, 'GET', $credentials);
if ($rc == 0 && $data && ref $data eq 'ARRAY') {
foreach my $setting (@$data) {
if ( ($setting->{param_name} eq 'setting_server') &&
($setting->{section} eq 'phone-settings') )
{
$setting_id = $setting->{uuid};
last;
}
}
unless ($setting_id) {
$err = 'setting for redirection server not found.';
$rc = 1;
}
} else {
$rc = 1;
}
$c->log->debug($self->to_log({ name => $op_name,
status => $rc ? 'failed' : 'success',
tx_id => $tx_id,
url => $url,
msg => $err // '',
data => $self->data_to_str($data) }));
return if $rc;
# fetch profile -------------------------------------------------------
$op_name = 'Snom check profiles';
$err = '';
$url = "$company_url/provisioning-profiles/";
$c->log->debug($self->to_log({ name => $op_name,
tx_id => $tx_id,
url => $url }));
($data, $rc) = $self->send_http_request($c, $tx_id, $url, 'GET', $credentials);
if ($rc == 0 && $data && ref $data eq 'ARRAY') {
# ok, noop
} else {
$rc = 1;
}
$c->log->debug($self->to_log({ name => $op_name,
status => $rc ? 'failed' : 'success',
tx_id => $tx_id,
url => $url,
msg => $err // '',
data => $self->data_to_str($data) }));
return if $rc;
# process profile -----------------------------------------------------
my $profile_id;
my ($profile) = grep {$_->{name} eq $self->params->{redirect_params}->{profile}} @$data;
if ($profile) {
$profile_id = $profile->{uuid};
} elsif (length $self->params->{redirect_params}->{profile}) {
#profile does not exist, create it
$op_name = 'Snom create profile';
$err = '';
my $body = encode_json({
name => $self->params->{redirect_params}->{profile},
product_group => $product_group_id,
autoprovisioning_enabled => 'true',
});
my $body_ct = 'application/json';
$c->log->debug($self->to_log({ name => $op_name,
tx_id => $tx_id,
url => $url,
data => $self->data_to_str($body) }));
($data, $rc) = $self->send_http_request($c, $tx_id, $url, 'POST', $credentials, $body_ct, $body);
if ($rc == 0 && $data && ref $data eq 'HASH' && $data->{uuid}) {
$profile_id = $data->{uuid};
} else {
$err = 'could not fetch profile uuid';
$rc = 1;
}
$c->log->debug($self->to_log({ name => $op_name,
status => $rc ? 'failed' : 'success',
tx_id => $tx_id,
url => $url,
msg => $err // '',
data => $self->data_to_str($data) }));
return if $rc;
}
# update profile ------------------------------------------------------
$op_name = 'Snom prepare profile update (register)';
$err = '';
my $body = {
mac => $new_mac,
autoprovisioning_enabled => 'true',
settings_manager => {
$setting_id => {
value => $param_uri,
attrs => {
perm => 'RW'
}
}
}
};
$body->{provisioning_profile} = $profile_id if ($profile_id);
$url = "$company_url/endpoints/$new_mac";
$c->log->debug($self->to_log({ name => $op_name,
tx_id => $tx_id,
url => $url }));
$ret = {
method =>'PUT',
url => $url,
body => $body,
hawk => $self->generate_header($url, 'PUT',
{
credentials => $credentials,
content_type => '',
payload => '',
}),
};
} elsif ($action eq 'unregister_content') {
# we've to fetch the id first before constructing the delete request --
$op_name = 'Snom prepare profile delete (unregister)';
$err = '';
$url = "$company_url/endpoints/";
$c->log->debug($self->to_log({ name => $op_name,
tx_id => $tx_id,
url => $url }));
($data, $rc) = $self->send_http_request($c, $tx_id, $url, 'GET', $credentials);
if ($rc == 0 && $data && ref $data eq 'ARRAY') {
my $device_id;
my ($device) = grep {uc($_->{mac}) eq uc($old_mac)} @$data;
if ($device) {
$device_id = $device->{mac};
}
$url = "$company_url/endpoints/$device_id";
$ret = {
method =>'DELETE',
url => $url,
body => undef,
hawk => $self->generate_header($url, 'DELETE',
{
credentials => $credentials,
content_type => '',
payload => ''
}),
};
} else {
$rc = 1;
}
$c->log->debug($self->to_log({ name => $op_name,
status => $rc ? 'failed' : 'success',
tx_id => $tx_id,
url => $url,
msg => $err // '',
data => $self->data_to_str($data) }));
return if $rc;
}
unless ($ret) {
$c->log->error($self->to_log({ name => 'Snom prepare request',
status => 'failed',
tx_id => $tx_id,
msg => 'no prepared register/unregister request' }));
}
return $ret;
}
sub generate_header {
my ($self, $uri, $method, $options) = @_;
my $time = time;
my $credentials = $options->{credentials};
my @chars = ("A".."Z", "a".."z");
my $nonce;
$nonce .= $chars[rand @chars] for 1..8;
$uri = URI->new($uri);
my $hash = $self->calculate_payload_hash(
$options->{payload},
$options->{content_type},
$credentials->{key}
);
my $artifacts = {
ts => $time,
nonce => $nonce,
method => $method,
resource => $uri->path_query,
host => $uri->host,
port => $uri->port,
hash => $hash || ''
};
my $mac = $self->calculate_mac($credentials, $artifacts);
my $auth = 'Hawk';
$auth .= ' mac="' . $mac . '",';
$auth .= ' hash="' . $artifacts->{hash} . '",' unless $hash eq '';
$auth .= ' id="' . $credentials->{id} . '",';
$auth .= ' ts="' . $artifacts->{ts} . '",';
$auth .= ' nonce="' . $artifacts->{nonce} .'"';
return $auth;
}
sub send_http_request {
my ($self, $c, $tx_id, $url, $method, $credentials, $body_ct, $body) = @_;
my ($res, $data, $rc);
my $req = HTTP::Request->new($method => $url);
$req->header('Authorization' => $self->generate_header($url, $method,
{
credentials => $credentials,
content_type => '',
payload => ''
}
));
$req->header('accept' => 'application/json');
if ($method eq 'POST') {
if ($body_ct) {
$req->content_type($body_ct);
}
$req->content($body);
}
$res = $self->_ua->request($req);
if ($res->is_success) {
if ($res->decoded_content) {
try {
$data = decode_json($res->decoded_content);
} catch($e) {
$c->log->error($self->to_log({ name => 'Failed to parse JSON content',
status => 'failed',
tx_id => $tx_id,
url => $url,
msg => $e,
data => $self->data_to_str($res->decoded_content) }));
return ($data, 1);
};
}
} else {
$c->log->error($self->to_log({ name => "$method reqeuest",
status => 'failed',
tx_id => $tx_id,
url => $url,
msg => $res->status_line,
data => $self->data_to_str($res->decoded_content) }));
return ($data, 1);
}
return ($data, 0);
}
sub calculate_mac {
my ($self, $credentials, $options) = @_;
my $normalized = $self->generate_normalized_string($options);
my $result_b64 = "";
$result_b64 = hmac_sha256_base64($normalized, $credentials->{key});
while (length($result_b64) % 4) {
$result_b64 .= '=';
}
return $result_b64;
}
sub calculate_payload_hash {
my ($self, $payload, $content_type, $key) = @_;
return '' if $payload eq '';
my $pload = "hawk.1.payload\n";
$pload .= $content_type . "\n";
$pload .= ($payload || '');
my $result_b64 = hmac_sha256_base64($pload, $key);
while (length($result_b64) % 4) {
$result_b64 .= '=';
}
return $result_b64;
}
sub generate_normalized_string {
my ($self, $options) = @_;
my $normalized = "hawk.1.header\n";
$normalized .= $options->{ts}."\n";
$normalized .= $options->{nonce}."\n";
$normalized .= uc($options->{method}) . "\n";
$normalized .= $options->{resource}."\n";
$normalized .= $options->{host}."\n";
$normalized .= $options->{port}."\n";
$normalized .= "\n";
$normalized .= "\n"; # this is also needed for a healthy header ( and mac ) since an extension is allowed in hawk
return $normalized;
}
around 'process_bootstrap_uri' => sub {
my($orig_method, $self, $uri) = @_;
$uri = $self->$orig_method($uri);
$uri = $self->bootstrap_uri_mac($uri, "{mac}");
$self->content_params->{uri} = $uri;
return $self->content_params->{uri};
};
1;