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})});
}