2020-01-26 16:14:16 +01:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
"""
socket_protocol ( Socket Protocol )
== == == == == == == == == == == == == == == == =
* * Author : * *
* Dirk Alders < sudo - dirk @mount - mockery . de >
* * Description : * *
This Module supports point to point communication for client - server issues .
* * Submodules : * *
2021-01-06 22:54:21 +01:00
* : class : ` socket_protocol . data_storage `
2020-01-26 16:14:16 +01:00
* : class : ` socket_protocol . pure_json_protocol `
2021-01-01 20:16:12 +01:00
* : class : ` socket_protocol . struct_json_protocol `
2020-01-26 16:14:16 +01:00
* * Unittest : * *
2021-01-06 22:54:21 +01:00
See also the : download : ` unittest < socket_protocol / _testresults_ / unittest . pdf > ` documentation .
2021-01-01 20:16:12 +01:00
* * Module Documentation : * *
2020-01-26 16:14:16 +01:00
"""
__DEPENDENCIES__ = [ ' stringtools ' ]
import stringtools
import binascii
import hashlib
import json
import logging
import os
import struct
import sys
import time
2020-12-21 22:34:26 +01:00
try :
from config import APP_NAME as ROOT_LOGGER_NAME
except ImportError :
ROOT_LOGGER_NAME = ' root '
logger = logging . getLogger ( ROOT_LOGGER_NAME ) . getChild ( __name__ )
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
__DESCRIPTION__ = """ The Module { \\ tt %s } is designed for point to point communication for client-server issues.
2020-01-26 16:14:16 +01:00
For more Information read the sphinx documentation . """ % __name__.replace( ' _ ' , ' \ _ ' )
""" The Module Description """
__INTERPRETER__ = ( 2 , 3 )
""" The Tested Interpreter-Versions """
2021-01-06 22:54:21 +01:00
SID_AUTH_REQUEST = 0
""" SID for authentification request """
SID_AUTH_RESPONSE = 1
""" SID for authentification response """
DID_AUTH_SEED = 0
""" DID for authentification (seed) """
DID_AUTH_KEY = 1
""" DID for authentification (key) """
SID_CHANNEL_NAME_REQUEST = 8
""" SID for channel name exchange request """
SID_CHANNEL_NAME_RESPONSE = 9
""" SID for channel name exchange response """
DID_CHANNEL_NAME = 0
""" DID for channel name """
SID_READ_REQUEST = 10
""" SID for a read data request """
SID_READ_RESPONSE = 11
""" SID for read data response """
SID_WRITE_REQUEST = 20
""" SID for a write data request """
SID_WRITE_RESPONSE = 21
""" SID for a write data response """
SID_EXECUTE_REQUEST = 30
""" SID for a execute request """
SID_EXECUTE_RESPONSE = 31
""" SID for a execute response """
STATUS_OKAY = 0
""" Status for ' okay ' """
STATUS_BUFFERING_UNHANDLED_REQUEST = 1
""" Status for ' unhandled request ' """
STATUS_CALLBACK_ERROR = 2
""" Status for ' callback errors ' """
STATUS_AUTH_REQUIRED = 3
""" Status for ' authentification is required ' """
STATUS_SERVICE_OR_DATA_UNKNOWN = 4
""" Status for ' service or data unknown ' """
STATUS_CHECKSUM_ERROR = 5
""" Status for ' checksum error ' """
STATUS_OPERATION_NOT_PERMITTED = 6
""" Status for ' operation not permitted ' """
2021-01-11 07:36:18 +01:00
STATUS_LOG_LVL = {
STATUS_OKAY : logging . INFO ,
STATUS_BUFFERING_UNHANDLED_REQUEST : logging . WARNING ,
STATUS_CALLBACK_ERROR : logging . ERROR ,
STATUS_AUTH_REQUIRED : logging . WARNING ,
STATUS_SERVICE_OR_DATA_UNKNOWN : logging . ERROR ,
STATUS_CHECKSUM_ERROR : logging . ERROR ,
STATUS_OPERATION_NOT_PERMITTED : logging . WARNING ,
}
""" Status depending log level for messages """
2021-01-06 22:54:21 +01:00
AUTH_STATE_UNTRUSTED_CONNECTION = 0
""" Authentification Status for an ' Untrusted Connection ' """
AUTH_STATE_SEED_REQUESTED = 1
""" Authentification Status for ' Seed was requested ' """
AUTH_STATE_SEED_TRANSFERRED = 2
""" Authentification Status for ' Seed has been sent ' """
AUTH_STATE_KEY_TRANSFERRED = 3
""" Authentification Status for ' Key has been sent ' """
AUTH_STATE_TRUSTED_CONNECTION = 4
""" Authentification Status for a ' Trusted Connection ' """
AUTH_STATE__NAMES = { AUTH_STATE_UNTRUSTED_CONNECTION : ' Untrusted Connection ' ,
AUTH_STATE_SEED_REQUESTED : ' Seed was requested ' ,
AUTH_STATE_SEED_TRANSFERRED : ' Seed has been sent ' ,
AUTH_STATE_KEY_TRANSFERRED : ' Key has been sent ' ,
AUTH_STATE_TRUSTED_CONNECTION : ' Trusted Connection ' }
""" Authentification Status names for previous defined authentification states """
class RequestSidExistsError ( Exception ) :
pass
class ResponseSidExistsError ( Exception ) :
pass
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
class _callback_storage ( dict ) :
2020-12-25 17:01:16 +01:00
DEFAULT_CHANNEL_NAME = ' all_others '
2021-01-06 22:54:21 +01:00
def __init__ ( self , channel_name , log_prefix ) :
2020-12-25 17:01:16 +01:00
self . init_channel_name ( channel_name )
2021-01-06 22:54:21 +01:00
self . __log_prefix__ = log_prefix
2020-01-26 16:14:16 +01:00
dict . __init__ ( self )
2020-12-25 17:01:16 +01:00
def init_channel_name ( self , channel_name ) :
if channel_name is None :
self . logger = logging . getLogger ( ROOT_LOGGER_NAME ) . getChild ( __name__ + ' . ' + self . DEFAULT_CHANNEL_NAME )
else :
self . logger = logging . getLogger ( ROOT_LOGGER_NAME ) . getChild ( __name__ + ' . ' + channel_name )
2020-01-26 16:14:16 +01:00
def get ( self , service_id , data_id ) :
2021-01-06 22:54:21 +01:00
if dict . get ( self , service_id , { } ) . get ( data_id , None ) is not None :
return self [ service_id ] [ data_id ]
elif dict . get ( self , service_id , { } ) . get ( None , None ) is not None :
return self [ service_id ] [ None ]
elif dict . get ( self , None , { } ) . get ( data_id , None ) is not None :
return self [ None ] [ data_id ]
elif dict . get ( self , None , { } ) . get ( None , None ) is not None :
2020-01-26 16:14:16 +01:00
return self [ None ] [ None ]
2021-01-06 22:54:21 +01:00
else :
return ( None , None , None )
2020-01-26 16:14:16 +01:00
2020-09-20 19:24:45 +02:00
def add ( self , service_id , data_id , callback , * args , * * kwargs ) :
2020-12-25 17:01:16 +01:00
cb_data = self . get ( service_id , data_id )
2021-01-06 22:54:21 +01:00
if dict . get ( self , service_id , { } ) . get ( data_id , None ) is not None :
if callback is None :
self . logger . warning ( " %s Deleting existing callback %s for service_id ( %s ) and data_id ( %s )! " , self . __log_prefix__ ( ) , repr ( cb_data [ 0 ] . __name__ ) , repr ( service_id ) , repr ( data_id ) )
del ( self [ service_id ] [ data_id ] )
return
else :
self . logger . warning ( " %s Overwriting existing callback %s for service_id ( %s ) and data_id ( %s ) to %s ! " , self . __log_prefix__ ( ) , repr ( cb_data [ 0 ] . __name__ ) , repr ( service_id ) , repr ( data_id ) , repr ( callback . __name__ ) )
else :
self . logger . debug ( " %s Adding callback %s for SID= %s and DID= %s " , self . __log_prefix__ ( ) , repr ( callback . __name__ ) , repr ( service_id ) , repr ( data_id ) )
2020-01-26 16:14:16 +01:00
if service_id not in self :
self [ service_id ] = { }
2020-09-20 19:24:45 +02:00
self [ service_id ] [ data_id ] = ( callback , args , kwargs )
2020-01-26 16:14:16 +01:00
class data_storage ( dict ) :
2021-01-01 20:16:12 +01:00
"""
2021-01-06 22:54:21 +01:00
This is a storage object for socket_protocol messages .
2021-01-01 20:16:12 +01:00
: param status : The message status .
: type status : int
: param service_id : The Service - ID .
: type service_id : int
: param data_id : The Data - ID .
: type data_id : int
: param data : The transfered data .
: type data : any
"""
2020-01-26 16:14:16 +01:00
KEY_STATUS = ' status '
KEY_SERVICE_ID = ' service_id '
KEY_DATA_ID = ' data_id '
KEY_DATA = ' data '
2021-01-06 22:54:21 +01:00
ALL_KEYS = [ KEY_DATA , KEY_DATA_ID , KEY_SERVICE_ID , KEY_STATUS ]
2020-01-26 16:14:16 +01:00
def __init__ ( self , * args , * * kwargs ) :
dict . __init__ ( self , * args , * * kwargs )
2021-01-06 22:54:21 +01:00
for key in self . ALL_KEYS :
if key not in self :
self [ key ] = None
2020-01-26 16:14:16 +01:00
def get_status ( self , default = None ) :
2021-01-01 20:16:12 +01:00
"""
This Method returns the message status .
2021-01-06 22:54:21 +01:00
: param default : The default value , if no data is available .
2021-01-01 20:16:12 +01:00
"""
2020-01-26 16:14:16 +01:00
return self . get ( self . KEY_STATUS , default )
def get_service_id ( self , default = None ) :
2021-01-01 20:16:12 +01:00
"""
This Method returns the message Service - ID .
2021-01-06 22:54:21 +01:00
: param default : The default value , if no data is available .
2021-01-01 20:16:12 +01:00
"""
2020-01-26 16:14:16 +01:00
return self . get ( self . KEY_SERVICE_ID , default )
def get_data_id ( self , default = None ) :
2021-01-01 20:16:12 +01:00
"""
This Method returns the message Data - ID .
2021-01-06 22:54:21 +01:00
: param default : The default value , if no data is available .
2021-01-01 20:16:12 +01:00
"""
2020-01-26 16:14:16 +01:00
return self . get ( self . KEY_DATA_ID , default )
def get_data ( self , default = None ) :
2021-01-01 20:16:12 +01:00
"""
This Method returns the message data .
2021-01-06 22:54:21 +01:00
: param default : The default value , if no data is available .
2021-01-01 20:16:12 +01:00
"""
2020-01-26 16:14:16 +01:00
return self . get ( self . KEY_DATA , default )
2021-01-01 20:16:12 +01:00
class pure_json_protocol ( object ) :
2020-01-26 16:14:16 +01:00
"""
2021-01-06 22:54:21 +01:00
This ` class ` supports to transfer a message and it ' s data.
2021-01-01 20:16:12 +01:00
: param comm_instance : A communication instance .
2020-01-26 16:14:16 +01:00
: type comm_instance : instance
2021-01-01 20:16:12 +01:00
: param secret : An optinal secret ( e . g . created by ` ` binascii . hexlify ( os . urandom ( 24 ) ) ` ` ) .
2020-01-26 16:14:16 +01:00
: type secret : str
2021-01-06 22:54:21 +01:00
: param auto_auth : An optional parameter to enable ( True ) automatic authentification , otherwise you need to do it manually , if needed .
2021-01-01 20:16:12 +01:00
: type auto_auth : bool
: param channel_name : An optional parameter to set a channel name for logging of the communication .
: type channel_name : str
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
. . hint : :
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
* The Service - ID is designed to identify the type of the communication ( e . g . : const : ` READ_REQUEST ` , : const : ` WRITE_REQUEST ` , : const : ` READ_RESPONSE ` , : const : ` WRITE_RESPONSE ` , . . . )
2021-01-01 20:16:12 +01:00
* The Data - ID is designed to identify the requests / responses using the same Service_ID .
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
. . note : : The : class : ` comm_instance ` needs to have at least the following interface :
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
* A Method : func : ` comm_instance . init_channel_name ` to set the channel name .
2021-01-01 20:16:12 +01:00
* A Constant : const : ` comm_instance . IS_CLIENT ` to identify that the : class : ` comm_instance ` is a client ( True ) or a server ( False ) .
* A Method : func : ` comm_instance . is_connected ` to identify if the instance is connected ( True ) or not ( False ) .
* A Method : func : ` comm_instance . reconnect ` to initiate a reconnect .
* A Method : func : ` comm_instance . register_callback ` to register a data available callback .
* A Method : func : ` comm_instance . register_connect_callback ` to register a connect callback .
* A Method : func : ` comm_instance . register_disconnect_callback ` to register a disconnect callback .
* A Method : func : ` comm_instance . send ` to send data via the : class : ` comm_instance ` .
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
. . note : : The parameter : const : ` auto_auth ` is only relevant , if a secret is given and the : class : ` comm_instance ` is a client . The authentification is initiated directly after the connection is established .
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
. . note : : The : const : ` channel_name ` - exchange will be initiated by the client directly after the the connection is established .
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
* If a channel_name is given at both communication sides and they are different , the client name is taken over and the server will log a warning message .
2020-01-26 16:14:16 +01:00
"""
2020-12-25 17:01:16 +01:00
DEFAULT_CHANNEL_NAME = ' all_others '
2020-01-26 16:14:16 +01:00
2020-12-25 17:01:16 +01:00
def __init__ ( self , comm_instance , secret = None , auto_auth = False , channel_name = None ) :
self . __comm_inst__ = comm_instance
2020-01-26 16:14:16 +01:00
self . __secret__ = secret
2020-09-10 21:55:01 +02:00
self . __auto_auth__ = auto_auth
2020-12-25 17:01:16 +01:00
#
2021-01-06 22:54:21 +01:00
self . __auth_whitelist__ = { }
self . __sid_response_dict__ = { }
self . __sid_name_dict__ = { }
self . __did_name_dict__ = { }
#
self . __status_name_dict = { }
self . add_status ( STATUS_OKAY , ' okay ' )
2021-01-11 07:36:18 +01:00
self . add_status ( STATUS_BUFFERING_UNHANDLED_REQUEST , ' no callback for service, data buffered ' )
self . add_status ( STATUS_CALLBACK_ERROR , ' callback error ' )
2021-01-06 22:54:21 +01:00
self . add_status ( STATUS_AUTH_REQUIRED , ' authentification required ' )
self . add_status ( STATUS_SERVICE_OR_DATA_UNKNOWN , ' service or data unknown ' )
self . add_status ( STATUS_CHECKSUM_ERROR , ' checksum error ' )
self . add_status ( STATUS_OPERATION_NOT_PERMITTED , ' operation not permitted ' )
#
self . __callbacks__ = _callback_storage ( channel_name , self . __log_prefix__ )
2020-12-25 17:01:16 +01:00
self . __init_channel_name__ ( channel_name )
#
2020-01-26 16:14:16 +01:00
self . __clean_receive_buffer__ ( )
2021-01-06 22:54:21 +01:00
self . add_service ( SID_AUTH_REQUEST , SID_AUTH_RESPONSE , ' authentification request ' , ' authentification response ' )
self . add_data ( ( SID_AUTH_REQUEST , SID_AUTH_RESPONSE ) , DID_AUTH_SEED , ' seed ' )
self . add_data ( SID_AUTH_REQUEST , DID_AUTH_KEY , ' key ' )
self . add_data ( SID_AUTH_RESPONSE , DID_AUTH_KEY , ' key ' )
self . add_msg_to_auth_whitelist_ ( SID_AUTH_REQUEST , DID_AUTH_SEED )
self . add_msg_to_auth_whitelist_ ( SID_AUTH_RESPONSE , DID_AUTH_SEED )
self . add_msg_to_auth_whitelist_ ( SID_AUTH_REQUEST , DID_AUTH_KEY )
self . add_msg_to_auth_whitelist_ ( SID_AUTH_RESPONSE , DID_AUTH_KEY )
self . __callbacks__ . add ( SID_AUTH_REQUEST , DID_AUTH_SEED , self . __authentificate_create_seed__ )
self . __callbacks__ . add ( SID_AUTH_RESPONSE , DID_AUTH_SEED , self . __authentificate_create_key__ )
self . __callbacks__ . add ( SID_AUTH_REQUEST , DID_AUTH_KEY , self . __authentificate_check_key__ )
self . __callbacks__ . add ( SID_AUTH_RESPONSE , DID_AUTH_KEY , self . __authentificate_process_feedback__ )
2020-01-26 16:14:16 +01:00
self . __authentification_state_reset__ ( )
2021-01-06 22:54:21 +01:00
self . add_service ( SID_CHANNEL_NAME_REQUEST , SID_CHANNEL_NAME_RESPONSE , ' channel name request ' , ' channel name response ' )
self . add_data ( ( SID_CHANNEL_NAME_REQUEST , SID_CHANNEL_NAME_RESPONSE ) , DID_CHANNEL_NAME , ' name ' )
self . add_msg_to_auth_whitelist_ ( SID_CHANNEL_NAME_REQUEST , DID_CHANNEL_NAME )
self . add_msg_to_auth_whitelist_ ( SID_CHANNEL_NAME_RESPONSE , DID_CHANNEL_NAME )
self . __callbacks__ . add ( SID_CHANNEL_NAME_REQUEST , DID_CHANNEL_NAME , self . __channel_name_request__ )
self . __callbacks__ . add ( SID_CHANNEL_NAME_RESPONSE , DID_CHANNEL_NAME , self . __channel_name_response__ )
self . add_service ( SID_READ_REQUEST , SID_READ_RESPONSE , ' read data request ' , ' read data response ' )
self . add_service ( SID_WRITE_REQUEST , SID_WRITE_RESPONSE , ' write data request ' , ' write data response ' )
self . add_service ( SID_EXECUTE_REQUEST , SID_EXECUTE_RESPONSE , ' execute request ' , ' execute response ' )
2020-01-26 16:14:16 +01:00
self . __seed__ = None
self . __comm_inst__ . register_callback ( self . __data_available_callback__ )
2020-09-10 21:55:01 +02:00
self . __comm_inst__ . register_connect_callback ( self . __connection_established__ )
2020-01-26 16:14:16 +01:00
self . __comm_inst__ . register_disconnect_callback ( self . __authentification_state_reset__ )
2021-01-06 22:54:21 +01:00
logger . info ( ' %s Initialisation finished. ' , self . __log_prefix__ ( ) )
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def __analyse_frame__ ( self , frame ) :
if sys . version_info > = ( 3 , 0 ) :
return data_storage ( json . loads ( frame [ : - 4 ] . decode ( ' utf-8 ' ) ) )
2020-12-25 17:01:16 +01:00
else :
2021-01-01 20:16:12 +01:00
return data_storage ( json . loads ( frame [ : - 4 ] ) )
def __authentificate_check_key__ ( self , msg ) :
key = msg . get_data ( )
if key == self . __authentificate_salt_and_hash__ ( self . __seed__ ) :
2021-01-06 22:54:21 +01:00
self . __authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION
return STATUS_OKAY , True
2021-01-01 20:16:12 +01:00
else :
2021-01-06 22:54:21 +01:00
self . __authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
return STATUS_OKAY , False
2021-01-01 20:16:12 +01:00
def __authentificate_create_key__ ( self , msg ) :
2021-01-06 22:54:21 +01:00
self . __authentification_state__ = AUTH_STATE_KEY_TRANSFERRED
2021-01-01 20:16:12 +01:00
seed = msg . get_data ( )
key = self . __authentificate_salt_and_hash__ ( seed )
2021-01-06 22:54:21 +01:00
self . send ( SID_AUTH_REQUEST , DID_AUTH_KEY , key )
2021-01-01 20:16:12 +01:00
def __authentificate_create_seed__ ( self , msg ) :
2021-01-06 22:54:21 +01:00
self . __authentification_state__ = AUTH_STATE_SEED_TRANSFERRED
2021-01-01 20:16:12 +01:00
if sys . version_info > = ( 3 , 0 ) :
self . __seed__ = binascii . hexlify ( os . urandom ( 32 ) ) . decode ( ' utf-8 ' )
else :
self . __seed__ = binascii . hexlify ( os . urandom ( 32 ) )
2021-01-06 22:54:21 +01:00
return STATUS_OKAY , self . __seed__
2021-01-01 20:16:12 +01:00
def __authentificate_process_feedback__ ( self , msg ) :
feedback = msg . get_data ( )
if feedback :
2021-01-06 22:54:21 +01:00
self . __authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION
2021-01-01 20:16:12 +01:00
self . logger . info ( " %s Got positive authentification feedback " , self . __log_prefix__ ( ) )
else :
2021-01-06 22:54:21 +01:00
self . __authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
2021-01-01 20:16:12 +01:00
self . logger . warning ( " %s Got negative authentification feedback " , self . __log_prefix__ ( ) )
2021-01-06 22:54:21 +01:00
return STATUS_OKAY , None
2021-01-01 20:16:12 +01:00
def __authentificate_salt_and_hash__ ( self , seed ) :
if sys . version_info > = ( 3 , 0 ) :
return hashlib . sha512 ( bytes ( seed , ' utf-8 ' ) + self . __secret__ ) . hexdigest ( )
else :
return hashlib . sha512 ( seed . encode ( ' utf-8 ' ) + self . __secret__ . encode ( ' utf-8 ' ) ) . hexdigest ( )
def __authentification_state_reset__ ( self ) :
2021-01-06 22:54:21 +01:00
self . logger . info ( " %s Resetting authentification state to AUTH_STATE_UNTRUSTED_CONNECTION " , self . __log_prefix__ ( ) )
self . __authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
def __authentification_required__ ( self , service_id , data_id ) :
return data_id not in self . __auth_whitelist__ . get ( service_id , [ ] )
2021-01-01 20:16:12 +01:00
def __buffer_received_data__ ( self , msg ) :
if not msg . get_service_id ( ) in self . __msg_buffer__ :
self . __msg_buffer__ [ msg . get_service_id ( ) ] = { }
if not msg . get_data_id ( ) in self . __msg_buffer__ [ msg . get_service_id ( ) ] :
self . __msg_buffer__ [ msg . get_service_id ( ) ] [ msg . get_data_id ( ) ] = [ ]
self . __msg_buffer__ [ msg . get_service_id ( ) ] [ msg . get_data_id ( ) ] . append ( msg )
self . logger . debug ( " %s Message data is stored in buffer and is now ready to be retrieved by receive method " , self . __log_prefix__ ( ) )
2021-01-11 07:36:18 +01:00
def __build_frame__ ( self , msg ) :
data_frame = json . dumps ( self . __mk_msg__ ( msg . get_status ( ) , msg . get_service_id ( ) , msg . get_data_id ( ) , msg . get_data ( ) ) )
2021-01-01 20:16:12 +01:00
if sys . version_info > = ( 3 , 0 ) :
data_frame = bytes ( data_frame , ' utf-8 ' )
checksum = self . __calc_chksum__ ( data_frame )
return data_frame + checksum
def __calc_chksum__ ( self , raw_data ) :
return struct . pack ( ' >I ' , binascii . crc32 ( raw_data ) & 0xffffffff )
2020-12-25 17:01:16 +01:00
@property
def __channel_name__ ( self ) :
cn = self . logger . name . split ( ' . ' ) [ - 1 ]
if cn != self . DEFAULT_CHANNEL_NAME :
return cn
2021-01-01 20:16:12 +01:00
def __channel_name_response__ ( self , msg ) :
data = msg . get_data ( )
if self . __channel_name__ is None and data is not None :
self . __init_channel_name__ ( data )
self . logger . info ( ' %s channel name is now %s ' , self . __log_prefix__ ( ) , repr ( self . __channel_name__ ) )
2021-01-06 22:54:21 +01:00
return STATUS_OKAY , None
2020-09-10 21:55:01 +02:00
2020-12-25 17:01:16 +01:00
def __channel_name_request__ ( self , msg ) :
data = msg . get_data ( )
if data is None :
2021-01-06 22:54:21 +01:00
return STATUS_OKAY , self . __channel_name__
2020-12-25 17:01:16 +01:00
else :
prev_channel_name = self . __channel_name__
self . __init_channel_name__ ( data )
if prev_channel_name is not None and prev_channel_name != data :
self . logger . warning ( ' %s overwriting user defined channel name from %s to %s ' , self . __log_prefix__ ( ) , repr ( prev_channel_name ) , repr ( data ) )
elif prev_channel_name is None :
self . logger . info ( ' %s channel name is now %s ' , self . __log_prefix__ ( ) , repr ( self . __channel_name__ ) )
2021-01-06 22:54:21 +01:00
return STATUS_OKAY , None
2020-12-25 17:01:16 +01:00
2020-01-26 16:14:16 +01:00
def __check_frame_checksum__ ( self , frame ) :
2021-01-01 20:16:12 +01:00
return self . __calc_chksum__ ( frame [ : - 4 ] ) == frame [ - 4 : ]
def __clean_receive_buffer__ ( self ) :
self . logger . debug ( " %s Cleaning up receive-buffer " , self . __log_prefix__ ( ) )
self . __msg_buffer__ = { }
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
def __connection_established__ ( self ) :
self . __clean_receive_buffer__ ( )
if self . __comm_inst__ . IS_CLIENT :
self . send ( SID_CHANNEL_NAME_REQUEST , 0 , self . __channel_name__ )
if self . __auto_auth__ and self . __comm_inst__ . IS_CLIENT and self . __secret__ is not None :
self . authentificate ( )
2021-01-11 07:36:18 +01:00
def __log_msg__ ( self , msg , rx_tx_prefix ) :
self . logger . log (
self . __status_log_lvl__ ( msg . get_status ( ) ) ,
' %s %s %s , %s , data: " %s " ' ,
self . __log_prefix__ ( ) ,
rx_tx_prefix ,
self . __get_message_name__ ( msg . get_service_id ( ) , msg . get_data_id ( ) ) ,
self . __get_status_name__ ( msg . get_status ( ) ) ,
repr ( msg . get_data ( ) )
)
2020-01-26 16:14:16 +01:00
def __data_available_callback__ ( self , comm_inst ) :
frame = comm_inst . receive ( )
2021-01-06 22:54:21 +01:00
msg = self . __analyse_frame__ ( frame )
2020-01-26 16:14:16 +01:00
if not self . __check_frame_checksum__ ( frame ) :
2021-01-06 22:54:21 +01:00
# Wrong Checksum
2021-01-11 07:36:18 +01:00
self . logger . log ( self . __status_log_lvl__ ( STATUS_CHECKSUM_ERROR ) , " %s Received message has an invalid checksum. Message will be ignored. " , self . __log_prefix__ ( ) )
2021-01-06 22:54:21 +01:00
return # No response needed
elif not self . check_authentification_state ( ) and self . __authentification_required__ ( msg . get_service_id ( ) , msg . get_data_id ( ) ) :
# Authentification required
2021-01-11 07:36:18 +01:00
self . __log_msg__ ( msg , ' RX <- ' )
2021-01-06 22:54:21 +01:00
if msg . get_service_id ( ) in self . __sid_response_dict__ . keys ( ) :
2021-01-11 07:36:18 +01:00
self . logger . log ( self . __status_log_lvl__ ( STATUS_AUTH_REQUIRED ) , " %s Authentification is required. Just sending negative response. " , self . __log_prefix__ ( ) )
2021-01-06 22:54:21 +01:00
status = STATUS_AUTH_REQUIRED
data = None
else :
2021-01-11 07:36:18 +01:00
self . logger . log ( self . __status_log_lvl__ ( STATUS_AUTH_REQUIRED ) , " %s Authentification is required. Incomming message will be ignored. " , self . __log_prefix__ ( ) )
2021-01-06 22:54:21 +01:00
return # No response needed
2020-01-26 16:14:16 +01:00
else :
2021-01-06 22:54:21 +01:00
# Valid message
2021-01-11 07:36:18 +01:00
self . __log_msg__ ( msg , ' RX <- ' )
2020-09-20 19:24:45 +02:00
callback , args , kwargs = self . __callbacks__ . get ( msg . get_service_id ( ) , msg . get_data_id ( ) )
2021-01-06 22:54:21 +01:00
if msg . get_service_id ( ) in self . __sid_response_dict__ . keys ( ) :
2020-01-26 16:14:16 +01:00
#
# REQUEST RECEIVED
#
2021-01-06 22:54:21 +01:00
if callback is None :
2021-01-11 07:36:18 +01:00
self . logger . warning ( " %s Incomming message with no registered callback. Sending negative response. " , self . __log_prefix__ ( ) )
2021-01-06 22:54:21 +01:00
status = STATUS_BUFFERING_UNHANDLED_REQUEST
2020-01-26 16:14:16 +01:00
data = None
else :
2021-01-11 11:43:07 +01:00
self . logger . debug ( " %s Executing callback %s to process received data " , self . __log_prefix__ ( ) , callback . __name__ )
2020-01-26 16:14:16 +01:00
try :
2020-09-20 19:24:45 +02:00
status , data = callback ( msg , * args , * * kwargs )
2021-01-11 11:30:22 +01:00
except Exception as e :
2021-01-11 11:43:07 +01:00
logger . error ( ' {lp} Exception raised. Check callback {callback_name} : " {message} " and it \' s return values for {msg_info} ' . format ( lp = self . __log_prefix__ ( ) , callback_name = callback . __name__ , message = str ( e ) , msg_info = self . __get_message_name__ ( msg . get_service_id ( ) , msg . get_data_id ( ) ) ) )
2021-01-06 22:54:21 +01:00
status = STATUS_CALLBACK_ERROR
data = None
2020-01-26 16:14:16 +01:00
else :
#
# RESPONSE RECEIVED
#
if callback is None :
self . __buffer_received_data__ ( msg )
else :
2021-01-06 22:54:21 +01:00
self . logger . debug ( " %s Executing callback %s to process received data " , self . __log_prefix__ ( ) , callback . __name__ )
2021-01-11 11:43:07 +01:00
try :
callback ( msg , * args , * * kwargs )
except Exception as e :
logger . error ( ' {lp} Exception raised. Check callback {callback_name} : " {message} " for {msg_info} ' . format ( lp = self . __log_prefix__ ( ) , callback_name = callback . __name__ , message = str ( e ) , msg_info = self . __get_message_name__ ( msg . get_service_id ( ) , msg . get_data_id ( ) ) ) )
2021-01-06 22:54:21 +01:00
return # No response needed
self . send ( self . __sid_response_dict__ [ msg . get_service_id ( ) ] , msg . get_data_id ( ) , data , status = status )
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
def __get_message_name__ ( self , service_id , data_id ) :
return ' service: %s , data_id: %s ' % (
self . __sid_name_dict__ . get ( service_id , repr ( service_id ) ) ,
self . __did_name_dict__ . get ( service_id , { } ) . get ( data_id , repr ( data_id ) ) ,
)
def __get_status_name__ ( self , status ) :
return ' status: %s ' % ( self . __status_name_dict . get ( status , ' unknown status: %s ' % repr ( status ) ) )
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def __init_channel_name__ ( self , channel_name ) :
self . __comm_inst__ . init_channel_name ( channel_name )
self . __callbacks__ . init_channel_name ( channel_name )
if channel_name is None :
self . logger = logging . getLogger ( ROOT_LOGGER_NAME ) . getChild ( __name__ + ' . ' + self . DEFAULT_CHANNEL_NAME )
else :
self . logger = logging . getLogger ( ROOT_LOGGER_NAME ) . getChild ( __name__ + ' . ' + channel_name )
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def __log_prefix__ ( self ) :
2021-01-11 07:36:18 +01:00
return ' prot-client: ' if self . __comm_inst__ . IS_CLIENT else ' prot-server: '
2020-01-26 16:14:16 +01:00
def __mk_msg__ ( self , status , service_id , data_id , data ) :
return data_storage ( { data_storage . KEY_DATA_ID : data_id , data_storage . KEY_SERVICE_ID : service_id , data_storage . KEY_STATUS : status , data_storage . KEY_DATA : data } )
2021-01-11 07:36:18 +01:00
def __status_log_lvl__ ( self , status ) :
return STATUS_LOG_LVL . get ( status , logging . CRITICAL )
2021-01-06 22:54:21 +01:00
def add_data ( self , service_id , data_id , name ) :
"""
Method to add a name for a specific message .
: param service_id : The Service - ID of the message . See class definitions starting with ` ` SID_ ` ` .
: type service_id : int or list of ints
: param data_id : The Data - ID of the message .
: type data_id : int
: param name : The Name for the transfered message .
: type name : str
"""
try :
iter ( service_id )
except Exception :
service_id = ( service_id , )
for sid in service_id :
if sid not in self . __did_name_dict__ :
self . __did_name_dict__ [ sid ] = { }
self . __did_name_dict__ [ sid ] [ data_id ] = name
def add_msg_to_auth_whitelist_ ( self , service_id , data_id ) :
"""
Method to add a specific message to the list , where no authentification is required .
: param service_id : The Service - ID of the message . See class definitions starting with ` ` SID_ ` ` .
: type service_id : int
: param data_id : The Data - ID of the message .
: type data_id : int
"""
if service_id not in self . __auth_whitelist__ :
self . __auth_whitelist__ [ service_id ] = [ ]
self . __auth_whitelist__ [ service_id ] . append ( data_id )
logger . debug ( ' %s Adding Message ( %s ) to the authentification whitelist ' , self . __log_prefix__ ( ) , self . __get_message_name__ ( service_id , data_id ) )
def add_service ( self , req_sid , resp_sid , req_name = None , resp_name = None ) :
"""
Method to add a Service defined by Request - and Response Serivce - ID .
: param req_sid : The Request Service - ID .
: type req_sid : int
: param resp_sid : The Response Service - ID .
: type resp_sid : int
"""
if req_sid in self . __sid_response_dict__ :
logger . error ( ' %s Service with Request-SID= %d and Response-SID= %d not added, because request SID is already registered ' , self . __log_prefix__ ( ) , req_sid , resp_sid )
raise RequestSidExistsError ( " Request for this Service is already registered " )
elif resp_sid in self . __sid_response_dict__ . values ( ) :
logger . error ( ' %s Service with Request-SID= %d and Response-SID= %d not added, because response SID is already registered ' , self . __log_prefix__ ( ) , req_sid , resp_sid )
raise ResponseSidExistsError ( " Response for this Service is already registered " )
else :
self . __sid_response_dict__ [ req_sid ] = resp_sid
if req_name is not None :
self . __sid_name_dict__ [ req_sid ] = req_name
if resp_name is not None :
self . __sid_name_dict__ [ resp_sid ] = resp_name
logger . debug ( ' %s Adding Service with Request= %s and Response= %s ' , self . __log_prefix__ ( ) , req_name or repr ( req_sid ) , resp_name or repr ( resp_sid ) )
def add_status ( self , status , name ) :
"""
Method to add a name for a status .
: param status : The Status . See class definitions starting with ` ` STATUS_ ` ` .
: type status : int
: param name : The Name for the Status .
: type name : str
"""
self . __status_name_dict [ status ] = name
2020-01-26 16:14:16 +01:00
def authentificate ( self , timeout = 2 ) :
"""
2021-01-06 22:54:21 +01:00
This method authetificates the client at the server .
2020-01-26 16:14:16 +01:00
: param timeout : The timeout for the authentification ( requesting seed , sending key and getting authentification_feedback ) .
: type timeout : float
: returns : True , if authentification was successfull ; False , if not .
: rtype : bool
. . note : : An authentification will only processed , if a secret had been given on initialisation .
. . note : : Client and Server needs to use the same secret .
"""
if self . __secret__ is not None :
2021-01-06 22:54:21 +01:00
self . __authentification_state__ = AUTH_STATE_SEED_REQUESTED
self . send ( SID_AUTH_REQUEST , DID_AUTH_SEED , None )
2020-01-26 16:14:16 +01:00
cnt = 0
while cnt < timeout * 10 :
time . sleep ( 0.1 )
2021-01-06 22:54:21 +01:00
if self . __authentification_state__ == AUTH_STATE_TRUSTED_CONNECTION :
2020-01-26 16:14:16 +01:00
return True
2021-01-06 22:54:21 +01:00
elif self . __authentification_state__ == AUTH_STATE_UNTRUSTED_CONNECTION :
2020-01-26 16:14:16 +01:00
break
cnt + = 1
return False
def check_authentification_state ( self ) :
"""
2021-01-06 22:54:21 +01:00
This Method return the Authitification State as boolean value .
2020-01-26 16:14:16 +01:00
: return : True , if authentification state is okay , otherwise False
: rtype : bool
"""
2021-01-06 22:54:21 +01:00
return self . __secret__ is None or self . __authentification_state__ == AUTH_STATE_TRUSTED_CONNECTION
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def connection_established ( self ) :
"""
2021-01-06 22:54:21 +01:00
This Method returns the Connection state including authentification as a boolean value .
2021-01-01 20:16:12 +01:00
: return : True , if the connection is established ( incl . authentification , if a secret has been given )
: rtype : bool
"""
return self . is_connected ( ) and ( self . __secret__ is None or self . check_authentification_state ( ) )
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def is_connected ( self ) :
"""
2021-01-06 22:54:21 +01:00
This Methods returns Connection state of the Communication Instance : func : ` comm_instance . is_connected ` .
2021-01-01 20:16:12 +01:00
: return : True if the : class : ` comm_instance ` is connected , otherwise False . .
: rtype : bool
"""
return self . __comm_inst__ . is_connected ( )
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def receive ( self , service_id , data_id , timeout = 1 ) :
"""
2021-01-06 22:54:21 +01:00
This Method returns a message object for a defined message or None , if this message is not available after the given timout .
2021-01-01 20:16:12 +01:00
: param service_id : The Service - ID for the message . See class definitions starting with ` ` SID_ ` ` .
: type service_id : int
: param data_id : The Data - ID for the message .
: type data_id : int
: param timeout : The timeout for receiving .
: type timeout : float
: returns : The received data storage object or None , if no data was received .
: rtype : data_storage
"""
data = None
cnt = 0
while data is None and cnt < timeout * 10 :
try :
data = self . __msg_buffer__ . get ( service_id , { } ) . get ( data_id , [ ] ) . pop ( 0 )
except IndexError :
data = None
cnt + = 1
time . sleep ( 0.1 )
if data is None and cnt > = timeout * 10 :
self . logger . warning ( ' %s TIMEOUT ( %s s): Requested data (service_id: %s ; data_id: %s ) not in buffer. ' , self . __log_prefix__ ( ) , repr ( timeout ) , repr ( service_id ) , repr ( data_id ) )
return data
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def reconnect ( self ) :
"""
This methods initiates a reconnect by calling : func : ` comm_instance . reconnect ` .
"""
return self . __comm_inst__ . reconnect ( )
def register_callback ( self , service_id , data_id , callback , * args , * * kwargs ) :
"""
2021-01-06 22:54:21 +01:00
This method registers a callback for the given parameters . Giving ` ` None ` ` means , that all Service - IDs or all Data - IDs are used .
If a message hitting these parameters has been received , the callback will be executed .
2021-01-01 20:16:12 +01:00
: param service_id : The Service - ID for the message . See class definitions starting with ` ` SID_ ` ` .
: type service_id : int
: param data_id : The Data - ID for the message .
: type data_id : int
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
. . note : : The : func : ` callback ` is priorised in the following order :
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
* Callbacks with defined Service - ID and Data - ID .
2021-01-06 22:54:21 +01:00
* Callbacks with a defined Service - ID and all Data - IDs .
* Callbacks with a defined Data - ID and all Service - IDs .
* Unspecific Callbacks .
2021-01-01 20:16:12 +01:00
. . note : : The : func : ` callback ` is executed with these arguments :
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
* * Parameters given at the callback call : * *
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
* The first Arguments is the received message as : class : ` data_storage ` object .
* Further arguments given at registration .
* Further keyword arguments given at registration .
* * Return value of the callback : * *
If the Callback is a Request Callback for a registered Service , the return value has to be a tuple or list with
* : const : ` response_status ` : The response status ( see class definitions starting with : const : ` STA_ * ` .
2021-01-01 20:16:12 +01:00
* : const : ` response_data ` : A JSON iterable object to be used as data for the response .
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
. . note : : Only registered services will respond via the callbacks return values with the same data_id .
2021-01-01 20:16:12 +01:00
"""
self . __callbacks__ . add ( service_id , data_id , callback , * args , * * kwargs )
2020-01-26 16:14:16 +01:00
2021-01-06 22:54:21 +01:00
def send ( self , service_id , data_id , data , status = STATUS_OKAY , timeout = 2 ) :
2021-01-01 20:16:12 +01:00
"""
2021-01-06 22:54:21 +01:00
This methods sends out a message with the given content .
2021-01-01 20:16:12 +01:00
: param service_id : The Service - ID for the message . See class definitions starting with ` ` SERVICE_ ` ` .
: type service_id : int
: param data_id : The Data - ID for the message .
: type data_id : int
: param data : The data to be transfered . The data needs to be json compatible .
: type data : str
: param status : The Status for the message . All requests should have ` ` STATUS_OKAY ` ` .
: type status : int
: param timeout : The timeout for sending data ( e . g . time to establish new connection ) .
: type timeout : float
: return : True if data had been sent , otherwise False .
: rtype : bool
"""
2021-01-06 22:54:21 +01:00
if ( self . check_authentification_state ( ) or not self . __authentification_required__ ( service_id , data_id ) ) or ( service_id in self . __sid_response_dict__ . values ( ) and status == STATUS_AUTH_REQUIRED and data is None ) :
2021-01-11 07:36:18 +01:00
msg = data_storage ( service_id = service_id , data_id = data_id , data = data , status = status )
self . __log_msg__ ( msg , ' TX -> ' )
return self . __comm_inst__ . send ( self . __build_frame__ ( msg ) , timeout = timeout )
2021-01-06 22:54:21 +01:00
else :
# Authentification required
2021-01-11 07:36:18 +01:00
self . logger . warning ( " %s Authentification is required. TX-Message %s , %s , data: %s will be ignored. " , self . __log_prefix__ ( ) , self . __get_message_name__ ( service_id , data_id ) , self . __get_status_name__ ( status ) , repr ( data ) )
2021-01-06 22:54:21 +01:00
return False
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
class struct_json_protocol ( pure_json_protocol ) :
"""
This Class has the same functionality like : class : ` pure_json_protocol ` . The message length is less than for : class : ` pure_json_protocol ` , but the functionality and compatibility is reduced .
2021-01-11 07:36:18 +01:00
See also parent : py : class : ` pure_json_protocol ` .
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
. . note : :
This class is depreceated and here for compatibility reasons ( to support old clients or servers ) . Usage of : class : ` pure_json_protocol ` is recommended .
2020-01-26 16:14:16 +01:00
"""
2020-09-10 21:55:01 +02:00
def __init__ ( self , * args , * * kwargs ) :
2021-01-01 20:16:12 +01:00
pure_json_protocol . __init__ ( self , * args , * * kwargs )
2020-01-26 16:14:16 +01:00
2021-01-01 20:16:12 +01:00
def __analyse_frame__ ( self , frame ) :
status , service_id , data_id = struct . unpack ( ' >III ' , frame [ 0 : 12 ] )
2020-01-26 16:14:16 +01:00
if sys . version_info > = ( 3 , 0 ) :
2021-01-01 20:16:12 +01:00
data = json . loads ( frame [ 12 : - 1 ] . decode ( ' utf-8 ' ) )
else :
data = json . loads ( frame [ 12 : - 1 ] )
return self . __mk_msg__ ( status , service_id , data_id , data )
2020-01-26 16:14:16 +01:00
2021-01-11 07:36:18 +01:00
def __build_frame__ ( self , msg ) :
frame = struct . pack ( ' >III ' , msg . get_status ( ) , msg . get_service_id ( ) , msg . get_data_id ( ) )
2020-01-26 16:14:16 +01:00
if sys . version_info > = ( 3 , 0 ) :
2021-01-11 07:36:18 +01:00
frame + = bytes ( json . dumps ( msg . get_data ( ) ) , ' utf-8 ' )
2021-01-01 20:16:12 +01:00
frame + = self . __calc_chksum__ ( frame )
2020-01-26 16:14:16 +01:00
else :
2021-01-11 07:36:18 +01:00
frame + = json . dumps ( msg . get_data ( ) )
2021-01-01 20:16:12 +01:00
frame + = self . __calc_chksum__ ( frame )
return frame
2020-01-26 16:14:16 +01:00
def __calc_chksum__ ( self , raw_data ) :
2021-01-01 20:16:12 +01:00
chksum = 0
for b in raw_data :
if sys . version_info > = ( 3 , 0 ) :
chksum ^ = b
else :
chksum ^ = ord ( b )
if sys . version_info > = ( 3 , 0 ) :
return bytes ( [ chksum ] )
else :
return chr ( chksum )
2020-01-26 16:14:16 +01:00
def __check_frame_checksum__ ( self , frame ) :
2021-01-01 20:16:12 +01:00
return self . __calc_chksum__ ( frame [ : - 1 ] ) == frame [ - 1 : ]