#########################################################################################
# Package HiPi::Huawei::Modem
# Description : Base class for HiLink Modem
# Copyright : Copyright (c) 2019 Mark Dootson
# License : This is free software; you can redistribute it and/or modify it under
# the same terms as the Perl 5 programming language system itself.
#########################################################################################
package HiPi::Huawei::Modem;
use strict;
use warnings;
use HiPi::Huawei::HiLink;
use HiPi::Huawei::Errors;
use Digest::SHA qw( sha256_hex );
use MIME::Base64 qw( encode_base64 );
use Try::Tiny;
use Encode ();
use parent qw( HiPi::Class );
my @_package_accessors = qw(
debug
ip_address
http
errors
safety
force_gsm
);
__PACKAGE__->create_accessors( @_package_accessors );
our $VERSION ='0.81';
sub new {
my($class, %params) = @_;
$params{'errors'} = 'HiPi::Huawei::Errors';
$params{'ip_address'} //= '192.168.8.1';
$params{'safety'} //= 1;
my $timeout = $params{'timeout'} || 30;
$params{'http'} = HiPi::Huawei::HiLink->new( timeout => $timeout, debug => $params{'debug'} || 0 );
$params{'_config'} = {};
my $self = $class->SUPER::new(%params);
return $self;
}
sub _get_address {
my $self = shift;
return 'http://' . $self->ip_address;
}
sub generic_get {
my($self, $route) = @_;
my $session = $self->_get_session;
return $session if(exists($session->{'code'}));
my $ref = $self->http->get($self->_get_url($route));
return $self->_response_ref($ref);
}
sub generic_post_xml {
my($self, $route, $xml) = @_;
my $session = $self->_get_session;
return $session if(exists($session->{'code'}));
my $ref = $self->http->post( $self->_get_url($route), $xml );
return $self->_response_ref($ref);
}
sub _get_url {
my($self, $route) = @_;
return $self->_get_address . '/' . $route;
}
sub _get_session {
my( $self ) = @_;
my $ref = $self->http->get( $self->_get_url('api/webserver/SesTokInfo') );
if(exists($ref->{'code'})) {
$ref->{'message'} = 'unable to get session tokens : ' . $self->errors->get_error_message( $ref->{'code'} );
} elsif(exists($ref->{'SesInfo'}) && exists($ref->{'TokInfo'})) {
$self->http->set_security( $ref->{'SesInfo'}, $ref->{'TokInfo'} );
} else {
$ref->{'code'} = '101';
$ref->{'message'} = $self->errors->get_error_message( $ref->{'code'} );
}
return $ref;
}
sub _response_ref {
my ( $self, $ref ) = @_;
if(ref($ref) eq 'HASH' && $ref->{'code'} && !$ref->{'message'}) {
$ref->{'message'} = $self->errors->get_error_message( $ref->{'code'} );
}
return $ref;
}
sub _unsafe_methods {
my $self = shift;
my $rval = 0;
if( $self->safety ) {
# check if we can recover
my $check = $self->hilink_can_modify_password;
unless( ref($check)
&& exists($check->{'hilink_can_modify_password'})
&& $check->{'hilink_can_modify_password'} eq '1' ) {
$rval = 1;
}
}
return $rval;
}
sub _get_datestamp {
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$year += 1900;
my $datestamp = sprintf('%04d-%02d-%02d %02d:%02d:%02d', $year, $mon + 1, $mday, $hour, $min, $sec );
return $datestamp;
}
sub _escape_value {
my($self, $value) = @_;
return '' unless(defined($value));
$value =~ s/&/&/sg;
$value =~ s/</sg;
$value =~ s/>/>/sg;
$value =~ s/"/"/sg;
return $value;
}
#-------------------------------------------------
sub filter_characters {
my($self, $msg) = @_;
$msg //= '';
return $msg unless($msg);
my $filteredchars;
my $usedencoding;
my $allencoded;
my @maps = ( $self->force_gsm )
? ( [ 'gsm0338', 0 ] )
: ( [ 'gsm0338', 1 ], [ 'UCS-2', 1 ], [ 'UCS-2', 0 ] );
for my $mapping ( @maps ) {
my ( $encoding, $check ) = @$mapping;
try {
my $instring = $msg;
my $octets = Encode::encode( $encoding, $instring, $check );
$filteredchars = Encode::decode( $encoding, $octets, 1 );
} catch {
$filteredchars = undef;
};
if( defined( $filteredchars ) ) {
$usedencoding = $encoding;
$allencoded = $check;
last;
}
}
return ( wantarray ) ? ( $filteredchars, $usedencoding, $allencoded ) : $filteredchars;
}
sub login {
my ($self, $username, $password ) = @_;
unless( $username && $password ) {
return $self->_response_ref( { code => 105 } );
}
# what's our login status
my $status = $self->get_login_status();
return $status if $status->{'code'};
my $loginstatus = $status->{'State'};
my $password_type = $status->{'password_type'};
return { success => 'OK' } if $loginstatus eq '0'; # don't need login
my $route = 'api/user/login';
my $tokenised_password = '';
if( $password_type eq '4' ) {
$tokenised_password = encode_base64(
sha256_hex( $username . encode_base64( sha256_hex( $password ), '' ) . $self->http->request_token ),
''
);
} else {
$tokenised_password = encode_base64( $password, '' );
}
my $xml = q(
) . $username . q(
) . $tokenised_password . q(
) . $password_type . q(
);
# post direct - no new session info
my $ref = $self->http->post( $self->_get_url($route), $xml );
return $self->_response_ref( $ref );
}
sub logout {
my $self = shift;
my $route = 'api/user/logout';
my $xml = q(
1
);
my $ref = $self->generic_post_xml( $route, $xml );
$self->http->clear_security;
return $ref;
}
sub get_status {
my $self = shift;
my $ref = $self->generic_get('api/monitoring/status');
return $ref;
}
sub get_traffic_stats {
my $self = shift;
my $ref = $self->generic_get('api/monitoring/traffic-statistics');
return $ref;
}
sub get_month_stats {
my $self = shift;
my $ref = $self->generic_get('api/monitoring/month_statistics');
return $ref;
}
sub get_global_module_switch {
my $self = shift;
my $ref = $self->generic_get('api/global/module-switch');
return $ref;
}
sub get_profiles {
my $self = shift;
my $ref = $self->generic_get('api/dialup/profiles');
return $ref;
}
sub get_login_status {
my $self = shift;
my $ref = $self->generic_get('api/user/state-login');
return $ref;
}
sub get_login_required {
my $self = shift;
my $ref = $self->generic_get('api/user/hilink_login');
return $ref;
}
sub set_login_required {
my ($self, $required) = @_;
$required = ( $required ) ? 1 : 0;
if( $required ) {
return $self->_response_ref( { 'code' => 110 } ) if $self->_unsafe_methods;
}
my $route = 'api/user/hilink_login';
my $xml = q(
) . $required . q(
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub get_encryption_mode {
my $self = shift;
my $ref = $self->generic_get('api/user/password');
if($ref->{'encryption_enable'}) {
return $ref;
} else {
return $self->_response_ref( { 'encryption_enable' => 0 } );
}
}
sub change_password {
my( $self, $username, $oldpassword, $newpassword ) = @_;
return $self->_response_ref( { 'code' => 110 } ) if $self->_unsafe_methods;
unless( $username && $oldpassword && $newpassword ) {
return $self->_response_ref( { code => 102 } );
}
my $lrequired = $self->get_login_required;
unless( $lrequired->{'hilink_login'} ) {
return $self->_response_ref( { code => 103 } );
}
my $route = 'api/user/password';
my $hashmode = $self->get_encryption_mode()->{'encryption_enable'};
my $tokenised_oldpassword;
my $tokenised_newpassword = encode_base64( $newpassword, '' );
if( $hashmode ) {
$tokenised_oldpassword = encode_base64(
sha256_hex( $username . encode_base64( sha256_hex( $oldpassword ), '' ) . $self->http->request_token ),
''
);
} else {
$tokenised_oldpassword = encode_base64( $oldpassword, '' );
}
my $xml = q(
) . $username . q(
) . $tokenised_oldpassword . q(
) . $tokenised_newpassword . q(
) . $hashmode . q(
);
# post direct - no new session info
my $ref = $self->http->post( $self->_get_url($route), $xml );
if( $ref->{'success'} ) {
$self->http->clear_security;
$ref = $self->login( $username, $newpassword );
} else {
$ref = $self->_response_ref( $ref );
}
return $ref;
}
sub get_serial_number {
my $self = shift;
my $ref = $self->get_device_info;
return $ref if $ref->{'code'};
return { 'SerialNumber' => $ref->{'SerialNumber'} };
}
sub get_device_feature_switch {
my $self = shift;
my $ref = $self->generic_get('api/device/device-feature-switch');
return $ref;
}
sub get_global_config {
my $self = shift;
my $ref = $self->generic_get('config/global/config.xml');
return $ref;
}
sub get_deviceinfo_config {
my $self = shift;
my $ref = $self->generic_get('config/deviceinformation/config.xml');
return $ref;
}
sub hilink_can_modify_password {
my $self = shift;
my $gcfg = $self->get_global_config;
return $gcfg if $gcfg->{'code'};
my $can = 0;
my $checkval = $gcfg->{'menu'}->{'settings'}->{'system'}->{'modifypassword'};
$can = 1 if( $checkval && $checkval eq 'modifypassword' );
return { 'hilink_can_modify_password' => $can };
}
sub get_test_func {
my $self = shift;
my $ref = $self->generic_get('api/device/device-feature-switch');
return $ref;
}
sub get_sms_send_status {
my $self = shift;
my $ref = $self->generic_get('api/sms/send-status');
return $ref;
}
sub get_signal_info {
my $self = shift;
my $ref = $self->generic_get('api/device/signal');
return $ref;
}
sub get_basic_info {
my $self = shift;
my $ref = $self->generic_get('api/device/basic_information');
return $ref;
}
sub get_network {
my $self = shift;
my $ref = $self->generic_get('api/net/current-plmn');
return $ref;
}
sub get_network_mode_info {
my $self = shift;
my $ref = $self->generic_get('api/net/network');
return $ref;
}
sub get_network_mode {
my $self = shift;
my $ref = $self->generic_get('api/net/net-mode');
return $ref;
}
sub get_network_mode_list {
my $self = shift;
my $ref = $self->generic_get('api/net/net-mode-list');
return $ref;
}
sub get_device_info {
my $self = shift;
my $ref = $self->generic_get('api/device/information');
return $ref;
}
sub get_connection_info {
my $self = shift;
my $ref = $self->generic_get('api/dialup/connection');
return $ref;
}
sub get_data_plan {
my $self = shift;
my $ref = $self->generic_get('api/monitoring/start_date');
return $ref;
}
sub set_data_plan {
my($self, $startdate, $limit, $threshold ) = @_;
# check start date
unless( $startdate && $startdate =~ /^[1-9][0-9]*$/ && $startdate > 0 && $startdate < 32 ) {
return $self->_response_ref( { code => 111 } );
}
# check limit
unless( $limit && $limit =~ /^[1-9][0-9]*(MB|GB|)$/ ) {
return $self->_response_ref( { code => 112 } );
}
# check threshhold
unless( $threshold && $threshold =~ /^[1-9][0-9]*$/ && $threshold > 0 && $threshold < 101 ) {
return $self->_response_ref( { code => 113 } );
}
my $route = 'api/monitoring/start_date';
my $xml = q(
) . $startdate . q(
) . $limit . q(
) . $threshold . q(
1
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub set_data_plan_off {
my($self) = @_;
my $route = 'api/monitoring/start_date';
my $xml = q(
1
0MB
90
0
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub clear_traffic {
my($self) = @_;
my $route = 'api/monitoring/clear-traffic';
my $xml = q(
1
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub check_notifications {
my $self = shift;
my $ref = $self->generic_get('api/monitoring/check-notifications');
return $ref;
}
sub get_data_status {
my $self = shift;
my $ref = $self->generic_get('api/dialup/mobile-dataswitch');
return $ref;
}
sub set_data {
my($self, $onoff) = @_;
if($onoff) {
return $self->set_data_on;
} else {
return $self->set_data_off;
}
}
sub set_data_off {
my $self = shift;
my $route = 'api/dialup/mobile-dataswitch';
my $xml = q(
0
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub set_data_on {
my $self = shift;
my $route = 'api/dialup/mobile-dataswitch';
my $xml = q(
1
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub device_reboot {
my $self = shift;
my $route = 'api/device/control';
my $xml = q(
1
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub device_restore {
my $self = shift;
my $route = 'api/device/control';
my $xml = q(
2
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
#sub device_backup {
# my $self = shift;
# my $route = 'api/device/control';
# my $xml = q(
#
# 3
# );
# my $ref = $self->generic_post_xml( $route, $xml );
# return $ref;
#}
sub device_serial_restore {
my ($self, $sn ) = @_;
return $self->_response_ref( { code => '104' } ) unless ($sn);
$sn = uc($sn);
my $route = 'api/device/restore-default';
my $xml = q(
) . $sn . q(
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub device_shutdown {
my $self = shift;
my $route = 'api/device/control';
my $xml = q(
4
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub send_sms {
my($self, $numbers, $message) = @_;
my $date = _get_datestamp();
$message = $self->_escape_value( $message );
$message = $self->filter_characters($message);
my $len = length($message);
my @numbers = ( $numbers && ref($numbers) eq 'ARRAY' ) ? @$numbers : ( $numbers );
my $xml = q(
-1
) . qq(\n);
for my $number ( @numbers ) {
$xml .= q( ) . $number . qq(\n);
}
$xml .= q(
) . $message . q(
) . $len . q(
1
) . $date . q(
);
my $ref = $self->generic_post_xml( 'api/sms/send-sms', $xml );
return $ref;
}
sub get_sms_count {
my $self = shift;
my $route = 'api/sms/sms-count';
my $ref = $self->generic_get($route);
return $ref;
}
sub get_inbox {
my $self = shift;
return $self->get_sms(50,1);
}
sub get_outbox {
my $self = shift;
return $self->get_sms(50,2);
}
sub get_drafts {
my $self = shift;
return $self->get_sms(50,3);
}
sub delete_sms {
my($self, $index) = @_;
my $route = 'api/sms/delete-sms';
my $xml = q(
) . $index . q(
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub set_sms_read {
my($self, $index) = @_;
my $route = 'api/sms/set-read';
my $xml = q(
) . $index . q(
);
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub get_sms {
my($self, $count, $boxtype ) = @_;
my $route = 'api/sms/sms-list';
$count ||= 1;
$boxtype ||= 1;
if($boxtype =~ /^in/i ) {
$boxtype = 1;
} elsif($boxtype =~ /^out/i ) {
$boxtype = 2;
} elsif($boxtype =~ /^dra/i ) {
$boxtype = 3;
}
unless($boxtype =~ /^(1|2|3)$/ ) {
$boxtype = 1;
}
my $xml = q(
1
) . $count . q(
) . $boxtype . q(
0
0
1
);
my $ref = $self->generic_post_xml( $route, $xml );
$ref = $self->_response_ref( $ref );
if(exists($ref->{'Count'})) {
$ref->{'BoxType'} = $boxtype;
}
return $ref;
}
sub connect_modem {
my $self = shift;
my $route = 'api/dialup/dial';
my $xml = '1';
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
sub disconnect_modem {
my $self = shift;
my $route = 'api/dialup/dial';
my $xml = '0';
my $ref = $self->generic_post_xml( $route, $xml );
return $ref;
}
1;
__END__