From 5a020e3740603fff472a41ea5e3bdfd9fc44ce62 Mon Sep 17 00:00:00 2001 From: Flaviu Mates Date: Mon, 5 Oct 2020 18:03:56 +0300 Subject: [PATCH] TT#91507 - Implements SNOM devices autoporovisioning with SRAPS * Remove old XMLRPC redirection logic * Implement Hawk header generation for SRAPS authorization * Implement bootstraping provisioning profile on SRAPS, then add the device into said profile * Implement deletion of device * Add 'Profile' and 'Product family' fields in SNOM device models Change-Id: I44ecf5199a7c04c6b0cb2e969aaa7f75578d874c --- lib/NGCP/Panel/Form/Device/Model.pm | 38 +- lib/NGCP/Panel/Form/Device/ModelAdmin.pm | 2 +- lib/NGCP/Panel/Utils/DeviceBootstrap/Snom.pm | 358 +++++++++++++----- .../Panel/Utils/DeviceBootstrap/VendorREST.pm | 4 + 4 files changed, 300 insertions(+), 102 deletions(-) diff --git a/lib/NGCP/Panel/Form/Device/Model.pm b/lib/NGCP/Panel/Form/Device/Model.pm index 3cda590d1a..7c579612be 100644 --- a/lib/NGCP/Panel/Form/Device/Model.pm +++ b/lib/NGCP/Panel/Form/Device/Model.pm @@ -412,23 +412,51 @@ has_field 'bootstrap_config_redirect_polycom_profile' => ( has_field 'bootstrap_config_redirect_snom_user' => ( type => 'Text', required => 0, - label => 'Snom username', + label => 'Snom access key id', default => '', wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_snom/], element_attr => { rel => ['tooltip'], - title => ['Username used to configure bootstrap url on Snom redirect server. Obtained from Snom.'], + title => ['Id used to configure bootstrap url on Snom redirect server. Obtained from Snom\'s Secure Redirection and Provisioning Service (SRAPS).'], }, ); has_field 'bootstrap_config_redirect_snom_password' => ( type => 'Text', required => 0, - label => 'Snom password', + label => 'Snom access key secret', default => '', wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_snom/], element_attr => { rel => ['tooltip'], - title => ['Password used to configure bootstrap url on Snom redirect server. Obtained from Snom.'], + title => ['Secret key used to configure bootstrap url on Snom redirect server. Obtained from Snom\'s Secure Redirection and Provisioning Service (SRAPS).'], + }, +); +has_field 'bootstrap_config_redirect_snom_profile' => ( + type => 'Text', + required => 0, + label => 'Snom profile', + default => '', + wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_snom/], + element_attr => { + rel => ['tooltip'], + title => ['Profile name used to create or use an existing a profile on Snom redirect server.'], + }, +); +has_field 'bootstrap_config_redirect_snom_product_family' => ( + type => 'Select', + required => 0, + label => 'Snom product family', + options => [ + { label => 'C-Series', value => 'C-Series' }, + { label => 'D-Series', value => 'D-Series' }, + { label => 'M-Series', value => 'M-Series' }, + { label => 'MSC-Series', value => 'MSC-Series' }, + ], + default => '', + wrapper_class => [qw/ngcp-devicetype ngcp-devicetype-phone ngcp-bootstrap-config ngcp-bootstrap-config-redirect_snom/], + element_attr => { + rel => ['tooltip'], + title => ['The Snom device Product Family. Used to create the profile on Snom redirect server.'], }, ); has_field 'bootstrap_config_redirect_grandstream_cid' => ( @@ -532,7 +560,7 @@ has_field 'save' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/vendor model type extensions_num connectable_models linerange linerange_add bootstrap_method bootstrap_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile bootstrap_config_redirect_grandstream_cid bootstrap_config_redirect_grandstream_key bootstrap_config_redirect_sipwise_user bootstrap_config_redirect_sipwise_password bootstrap_config_profile_sipwise_user bootstrap_config_profile_sipwise_password bootstrap_config_redirect_ale_user bootstrap_config_redirect_ale_password front_image front_thumbnail mac_image/], + render_list => [qw/vendor model type extensions_num connectable_models linerange linerange_add bootstrap_method bootstrap_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile bootstrap_config_redirect_snom_user bootstrap_config_redirect_snom_password bootstrap_config_redirect_snom_profile bootstrap_config_redirect_snom_product_family bootstrap_config_redirect_grandstream_cid bootstrap_config_redirect_grandstream_key bootstrap_config_redirect_sipwise_user bootstrap_config_redirect_sipwise_password bootstrap_config_profile_sipwise_user bootstrap_config_profile_sipwise_password bootstrap_config_redirect_ale_user bootstrap_config_redirect_ale_password front_image front_thumbnail mac_image/], ); has_block 'actions' => ( diff --git a/lib/NGCP/Panel/Form/Device/ModelAdmin.pm b/lib/NGCP/Panel/Form/Device/ModelAdmin.pm index e11dd552b0..e07b69b9ac 100644 --- a/lib/NGCP/Panel/Form/Device/ModelAdmin.pm +++ b/lib/NGCP/Panel/Form/Device/ModelAdmin.pm @@ -29,7 +29,7 @@ has_field 'save' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/reseller vendor model type extensions_num connectable_models linerange linerange_add bootstrap_method bootstrap_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile bootstrap_config_redirect_snom_user bootstrap_config_redirect_snom_password bootstrap_config_redirect_grandstream_cid bootstrap_config_redirect_grandstream_key bootstrap_config_redirect_ale_user bootstrap_config_redirect_ale_password front_image front_thumbnail mac_image/], + render_list => [qw/reseller vendor model type extensions_num connectable_models linerange linerange_add bootstrap_method bootstrap_uri bootstrap_config_http_sync_method bootstrap_config_http_sync_uri bootstrap_config_http_sync_params bootstrap_config_redirect_panasonic_user bootstrap_config_redirect_panasonic_password bootstrap_config_redirect_yealink_user bootstrap_config_redirect_yealink_password bootstrap_config_redirect_polycom_user bootstrap_config_redirect_polycom_password bootstrap_config_redirect_polycom_profile bootstrap_config_redirect_snom_user bootstrap_config_redirect_snom_password bootstrap_config_redirect_snom_profile bootstrap_config_redirect_snom_product_family bootstrap_config_redirect_grandstream_cid bootstrap_config_redirect_grandstream_key bootstrap_config_redirect_ale_user bootstrap_config_redirect_ale_password front_image front_thumbnail mac_image/], ); has_block 'actions' => ( diff --git a/lib/NGCP/Panel/Utils/DeviceBootstrap/Snom.pm b/lib/NGCP/Panel/Utils/DeviceBootstrap/Snom.pm index 164e9ffd26..9226c82485 100644 --- a/lib/NGCP/Panel/Utils/DeviceBootstrap/Snom.pm +++ b/lib/NGCP/Panel/Utils/DeviceBootstrap/Snom.pm @@ -1,130 +1,296 @@ package NGCP::Panel::Utils::DeviceBootstrap::Snom; use strict; +use warnings; + +use URI::Escape; use Moo; -use Data::Dumper; -extends 'NGCP::Panel::Utils::DeviceBootstrap::VendorRPC'; +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; + +extends 'NGCP::Panel::Utils::DeviceBootstrap::VendorREST'; sub rpc_server_params{ my $self = shift; my $cfg = { proto => 'https', host => 'secure-provisioning.snom.com', - port => '8083', - path => '/xmlrpc', + path => 'api/v1', }; - $cfg->{headers} = { %{$self->get_basic_authorization($self->params->{credentials})} }; $self->{rpc_server_params} = $cfg; return $self->{rpc_server_params}; } -sub register_content { - my $self = shift; - - my $param_mac = $self->content_params->{mac}; +sub rest_prepare_request { + my ($self, $action) = @_; + my $c = $self->params->{c}; + my $ret; + 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} + }; -#".URI::Escape::uri_escape($self->content_params->{uri})." -#http://fox.snom.com/prv2.php?mac={mac} - - $self->{register_content} = < - - redirect.registerPhone - - - $param_mac - - - $param_uri - - - -EOS_XML - return $self->{register_content}; -} + $self->{rpc_server_params} //= $self->rpc_server_params; + my $cfg = $self->{rpc_server_params}; -sub unregister_content { - my $self = shift; + $c->log->debug("Snom prepare request for action $action"); - my $param_macold = $self->content_params->{mac_old} // ''; - - $self->{unregister_content} = < - - redirect.deregisterPhone - - - $param_macold - - - -EOS_XML - return $self->{unregister_content}; -} -around 'extract_response_description' => sub { - my($orig_method, $self, $rpc_value) = @_; - my $c = $self->params->{c}; - my $res = ''; - - if(ref $rpc_value eq 'ARRAY'){ - #1 - success; 0 - error, error string is a second param - if($rpc_value->[0] eq '1'){ - $res = '';#clear the error - }elsif($rpc_value->[0] eq '0'){ - return $rpc_value->[1]; - }else{ - $res = $self->unknown_error; - } - }else{ - $res = $self->unknown_error; + # first, get company url + my $url = "$$cfg{proto}://$$cfg{host}/$$cfg{path}/tokens/".$credentials->{id}; + $c->log->debug("Snom get tokens '$url'"); + my $req = HTTP::Request->new(GET => $url); + $req->header('Authorization' => $self->generate_header($url, "GET", { credentials => $credentials, content_type => '', payload => '' })); + $req->header('accept' => 'application/json'); + my $res = $self->_ua->request($req); + my $data = decode_json($res->decoded_content); + my $company_url = $data->{links}->{company}; + if ($res->is_success && $data->{links}->{company}) { + $c->log->debug("Tokens fetching successful, data: " . $res->decoded_content); + $url = $data->{links}->{company}; + } else { + $c->log->error("Tokens fetching failed (" . $res->status_line . "): " . $res->decoded_content); + return; } - return $res; -}; - -around 'process_bootstrap_uri' => sub { - my($orig_method, $self, $uri) = @_; - $uri = $self->$orig_method($uri); - $uri = $self->bootstrap_uri_mac($uri); - $self->content_params->{uri} = $uri; - return $self->content_params->{uri}; -}; - -around 'bootstrap_uri_mac' => sub { - my($orig_method, $self, $uri) = @_; - if ($uri !~/\{mac\}$/){ - if ($uri !~/\/$/){ - $uri .= '/' ; + + if ($action eq 'register_content') { + # fetch product groups + $url = "$$cfg{proto}://$$cfg{host}/$$cfg{path}/product-groups/"; + $c->log->debug("Snom fetch product groups '$url'"); + $req = HTTP::Request->new(GET => $url); + $req->header('Authorization' => $self->generate_header($url, "GET", { credentials => $credentials, content_type => '', payload => '' })); + $req->header('accept' => 'application/json'); + $res = $self->_ua->request($req); + + my $product_group_id; + my $setting_id; + $data = decode_json($res->decoded_content); + if ($res->is_success && scalar @$data) { + $c->log->debug("Snom fetch product groups successful, data: " . $res->decoded_content); + my ($product_group) = grep {$_->{name} eq $self->params->{redirect_params}->{product_family}} @$data; + if ($product_group) { + $product_group_id = $product_group->{uuid}; + } + else { + $c->log->error("Snom product group of specified product family not found."); + return; + } + + #fetch settings + $url = "$$cfg{proto}://$$cfg{host}/$$cfg{path}/settings/"; + $c->log->debug("Snom fetch settings '$url'"); + $req = HTTP::Request->new(GET => $url); + $req->header('Authorization' => $self->generate_header($url, "GET", { credentials => $credentials, content_type => '', payload => '' })); + $req->header('accept' => 'application/json'); + $res = $self->_ua->request($req); + + $data = decode_json($res->decoded_content); + if ($res->is_success && scalar @$data) { + $c->log->debug("Snom fetch settings successful, data: " . $res->decoded_content); + foreach my $setting (@$data) { + if ($setting->{param_name} eq 'setting_server') { + $setting_id = $setting->{uuid}; + } + } + unless ($setting_id) { + $c->log->error("Snom setting for redirection server not found."); + return; + } + } + } else { + $c->log->error("Snom fetch product groups quey failed (" . $res->status_line . "): " . $res->decoded_content); + return; + } + + # fetch profile + $url = "$company_url/provisioning-profiles/"; + $c->log->debug("Snom check profiles '$url'"); + $req = HTTP::Request->new(GET => $url); + $req->header('Authorization' => $self->generate_header($url, "GET", { credentials => $credentials, content_type => '', payload => '' })); + $req->header('accept' => 'application/json'); + $res = $self->_ua->request($req); + + $data = decode_json($res->decoded_content); + if ($res->is_success && scalar @$data) { + $c->log->debug("Snom check profiles query successful, data: " . $res->decoded_content); + my $profile_id; + my ($profile) = grep {$_->{name} eq $self->params->{redirect_params}->{profile}} @$data; + if ($profile) { + $profile_id = $profile->{uuid}; + } + else { + #profile does not exist, create it + $c->log->debug("Snom create profile '$url'"); + $req = HTTP::Request->new(POST => $url); + my $body = encode_json({ + name => $self->params->{redirect_params}->{profile}, + product_group => $product_group_id, + autoprovisioning_enabled => 'true', + }); + $req->header('Authorization' => $self->generate_header($url, "POST", { credentials => $credentials, content_type => '', payload => '' })); + $req->header('accept' => 'application/json'); + $req->content_type('application/json'); + $req->content($body); + $res = $self->_ua->request($req); + $data = decode_json($res->decoded_content); + if ($res->is_success && $res->code == 201) { + $c->log->debug("Snom create profile query successful, data: " . $res->decoded_content); + $profile_id = $data->{uuid}; + } + else{ + $c->log->error("Snom create profile query failed (" . $res->status_line . "): " . $res->decoded_content); + return; + } + } + + my $body = { + mac => $new_mac, + provisioning_profile => $profile_id, + autoprovisioning_enabled => 'true', + settings_manager => { + $setting_id => { + value => $param_uri, + attrs => { + perm => 'RW' + } + + } + } + }; + $url = "$company_url/endpoints/$new_mac"; + $ret = { + method =>'PUT', + url => $url, + body => $body, + hawk => $self->generate_header($url, "PUT", { credentials => $credentials, content_type => '', payload => '' }), + }; + } else { + $c->log->error("Snom check profile query failed (" . $res->status_line . "): " . $res->decoded_content); + return; + } + } elsif ($action eq 'unregister_content') { + # we've to fetch the id first before constructing the delete request + $url = "$company_url/endpoints/"; + $c->log->debug("Snom check devices '$url'"); + $req = HTTP::Request->new(GET => $url); + $req->header('Authorization' => $self->generate_header($url, "GET", { credentials => $credentials, content_type => '', payload => '' })); + $req->header('accept' => 'application/json'); + $res = $self->_ua->request($req); + $data = decode_json($res->decoded_content); + if ($res->is_success && scalar @$data) { + $c->log->debug("Snom check devices query successful, data: " . $res->decoded_content); + my $device_id; + my ($device) = grep {uc($_->{mac}) eq uc($old_mac)} @$data; + if ($device) { + $device_id = $device->{mac}; + } + $c->log->debug("Snom unregister query successful, data: " . $res->decoded_content); + $data = decode_json($res->decoded_content); + $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 { + $c->log->error("Snom unregister query failed (" . $res->status_line . "): " . $res->decoded_content); + return; } - $uri .= '?mac={mac}' ; } - return $uri; -}; -1; -=head1 NAME + 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; -NGCP::Panel::Utils::DeviceBootstrap +} + +sub calculate_mac { + my ($self, $credentials, $options) = @_; -=head1 DESCRIPTION + my $normalized = $self->generate_normalized_string($options); -Make API requests to configure remote redirect servers for requested MAC with autorpov uri. -See http://wiki.snom.com/Category:HowTo:XMLRPC_Redirection. + my $result_b64 = ""; + $result_b64 = hmac_sha256_base64($normalized, $credentials->{key}); + while (length($result_b64) % 4) { + $result_b64 .= '='; + } -=head1 METHODS + return $result_b64; +} -=head2 bootstrap +sub calculate_payload_hash { + my ($self, $payload, $content_type, $key) = @_; -Dispatch to proper vendor API call. + return '' if $payload eq ''; -=head1 AUTHOR + my $pload = "hawk.1.payload\n"; + $pload .= $content_type . "\n"; + $pload .= ($payload || ''); -Irina Peshinskaya C<< >> + my $result_b64 = hmac_sha256_base64($pload, $key); -=head1 LICENSE + while (length($result_b64) % 4) { + $result_b64 .= '='; + } -This library is free software. You can redistribute it and/or modify -it under the same terms as Perl itself. + return $result_b64; +} -=cut -# vim: set tabstop=4 expandtab: +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; +} + +1; diff --git a/lib/NGCP/Panel/Utils/DeviceBootstrap/VendorREST.pm b/lib/NGCP/Panel/Utils/DeviceBootstrap/VendorREST.pm index 708ad3cbfa..2b091609d2 100644 --- a/lib/NGCP/Panel/Utils/DeviceBootstrap/VendorREST.pm +++ b/lib/NGCP/Panel/Utils/DeviceBootstrap/VendorREST.pm @@ -26,6 +26,10 @@ sub redirect_server_call { if ($data->{token}) { $req->header(token => $data->{token}); } + elsif ($data->{hawk}) { + $req->header(Authorization => $data->{hawk}); + $req->header('accept' => 'application/json'); + } else { $req->header(%{$self->get_basic_authorization($self->params->{credentials})}); }