786 lines
35 KiB
Python
786 lines
35 KiB
Python
#!/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:**
|
|
|
|
* :class:`socket_protocol.data_storage`
|
|
* :class:`socket_protocol.pure_json_protocol`
|
|
* :class:`socket_protocol.struct_json_protocol`
|
|
|
|
**Unittest:**
|
|
|
|
See also the :download:`unittest <socket_protocol/_testresults_/unittest.pdf>` documentation.
|
|
|
|
**Module Documentation:**
|
|
|
|
"""
|
|
__DEPENDENCIES__ = ['stringtools']
|
|
|
|
import stringtools
|
|
|
|
import binascii
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import struct
|
|
import sys
|
|
import time
|
|
|
|
|
|
try:
|
|
from config import APP_NAME as ROOT_LOGGER_NAME
|
|
except ImportError:
|
|
ROOT_LOGGER_NAME = 'root'
|
|
logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
|
|
|
|
|
|
__DESCRIPTION__ = """The Module {\\tt %s} is designed for point to point communication for client-server issues.
|
|
For more Information read the sphinx documentation.""" % __name__.replace('_', '\_')
|
|
"""The Module Description"""
|
|
__INTERPRETER__ = (2, 3)
|
|
"""The Tested Interpreter-Versions"""
|
|
|
|
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'"""
|
|
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"""
|
|
|
|
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
|
|
|
|
|
|
class _callback_storage(dict):
|
|
DEFAULT_CHANNEL_NAME = 'all_others'
|
|
|
|
def __init__(self, channel_name, log_prefix):
|
|
self.init_channel_name(channel_name)
|
|
self.__log_prefix__ = log_prefix
|
|
dict.__init__(self)
|
|
|
|
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)
|
|
|
|
def get(self, service_id, data_id):
|
|
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:
|
|
return self[None][None]
|
|
else:
|
|
return (None, None, None)
|
|
|
|
def add(self, service_id, data_id, callback, *args, **kwargs):
|
|
cb_data = self.get(service_id, data_id)
|
|
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))
|
|
if service_id not in self:
|
|
self[service_id] = {}
|
|
self[service_id][data_id] = (callback, args, kwargs)
|
|
|
|
|
|
class data_storage(dict):
|
|
"""
|
|
This is a storage object for socket_protocol messages.
|
|
|
|
: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
|
|
"""
|
|
|
|
KEY_STATUS = 'status'
|
|
KEY_SERVICE_ID = 'service_id'
|
|
KEY_DATA_ID = 'data_id'
|
|
KEY_DATA = 'data'
|
|
ALL_KEYS = [KEY_DATA, KEY_DATA_ID, KEY_SERVICE_ID, KEY_STATUS]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
dict.__init__(self, *args, **kwargs)
|
|
for key in self.ALL_KEYS:
|
|
if key not in self:
|
|
self[key] = None
|
|
|
|
def get_status(self, default=None):
|
|
"""
|
|
This Method returns the message status.
|
|
|
|
:param default: The default value, if no data is available.
|
|
"""
|
|
return self.get(self.KEY_STATUS, default)
|
|
|
|
def get_service_id(self, default=None):
|
|
"""
|
|
This Method returns the message Service-ID.
|
|
|
|
:param default: The default value, if no data is available.
|
|
"""
|
|
return self.get(self.KEY_SERVICE_ID, default)
|
|
|
|
def get_data_id(self, default=None):
|
|
"""
|
|
This Method returns the message Data-ID.
|
|
|
|
:param default: The default value, if no data is available.
|
|
"""
|
|
return self.get(self.KEY_DATA_ID, default)
|
|
|
|
def get_data(self, default=None):
|
|
"""
|
|
This Method returns the message data.
|
|
|
|
:param default: The default value, if no data is available.
|
|
"""
|
|
return self.get(self.KEY_DATA, default)
|
|
|
|
|
|
class pure_json_protocol(object):
|
|
"""
|
|
This `class` supports to transfer a message and it's data.
|
|
|
|
:param comm_instance: A communication instance.
|
|
:type comm_instance: instance
|
|
:param secret: An optinal secret (e.g. created by ``binascii.hexlify(os.urandom(24))``).
|
|
:type secret: str
|
|
:param auto_auth: An optional parameter to enable (True) automatic authentification, otherwise you need to do it manually, if needed.
|
|
:type auto_auth: bool
|
|
:param channel_name: An optional parameter to set a channel name for logging of the communication.
|
|
:type channel_name: str
|
|
|
|
.. hint::
|
|
|
|
* 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`, ...)
|
|
* The Data-ID is designed to identify the requests / responses using the same Service_ID.
|
|
|
|
.. note:: The :class:`comm_instance` needs to have at least the following interface:
|
|
|
|
* A Method :func:`comm_instance.init_channel_name` to set the channel name.
|
|
* 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`.
|
|
|
|
.. 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.
|
|
|
|
.. note:: The :const:`channel_name`-exchange will be initiated by the client directly after the the connection is established.
|
|
|
|
* 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.
|
|
"""
|
|
DEFAULT_CHANNEL_NAME = 'all_others'
|
|
|
|
def __init__(self, comm_instance, secret=None, auto_auth=False, channel_name=None):
|
|
self.__comm_inst__ = comm_instance
|
|
self.__secret__ = secret
|
|
self.__auto_auth__ = auto_auth
|
|
#
|
|
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')
|
|
self.add_status(STATUS_BUFFERING_UNHANDLED_REQUEST, 'no callback for service, data buffered')
|
|
self.add_status(STATUS_CALLBACK_ERROR, 'callback error')
|
|
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__)
|
|
self.__init_channel_name__(channel_name)
|
|
#
|
|
self.__clean_receive_buffer__()
|
|
|
|
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__)
|
|
self.__authentification_state_reset__()
|
|
|
|
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')
|
|
|
|
self.__seed__ = None
|
|
self.__comm_inst__.register_callback(self.__data_available_callback__)
|
|
self.__comm_inst__.register_connect_callback(self.__connection_established__)
|
|
self.__comm_inst__.register_disconnect_callback(self.__authentification_state_reset__)
|
|
logger.info('%s Initialisation finished.', self.__log_prefix__())
|
|
|
|
def __analyse_frame__(self, frame):
|
|
if sys.version_info >= (3, 0):
|
|
return data_storage(json.loads(frame[:-4].decode('utf-8')))
|
|
else:
|
|
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__):
|
|
self.__authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION
|
|
return STATUS_OKAY, True
|
|
else:
|
|
self.__authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
|
|
return STATUS_OKAY, False
|
|
|
|
def __authentificate_create_key__(self, msg):
|
|
self.__authentification_state__ = AUTH_STATE_KEY_TRANSFERRED
|
|
seed = msg.get_data()
|
|
key = self.__authentificate_salt_and_hash__(seed)
|
|
self.send(SID_AUTH_REQUEST, DID_AUTH_KEY, key)
|
|
|
|
def __authentificate_create_seed__(self, msg):
|
|
self.__authentification_state__ = AUTH_STATE_SEED_TRANSFERRED
|
|
if sys.version_info >= (3, 0):
|
|
self.__seed__ = binascii.hexlify(os.urandom(32)).decode('utf-8')
|
|
else:
|
|
self.__seed__ = binascii.hexlify(os.urandom(32))
|
|
return STATUS_OKAY, self.__seed__
|
|
|
|
def __authentificate_process_feedback__(self, msg):
|
|
feedback = msg.get_data()
|
|
if feedback:
|
|
self.__authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION
|
|
self.logger.info("%s Got positive authentification feedback", self.__log_prefix__())
|
|
else:
|
|
self.__authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
|
|
self.logger.warning("%s Got negative authentification feedback", self.__log_prefix__())
|
|
return STATUS_OKAY, None
|
|
|
|
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):
|
|
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, [])
|
|
|
|
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__())
|
|
|
|
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()))
|
|
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)
|
|
|
|
@property
|
|
def __channel_name__(self):
|
|
cn = self.logger.name.split('.')[-1]
|
|
if cn != self.DEFAULT_CHANNEL_NAME:
|
|
return cn
|
|
|
|
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__))
|
|
return STATUS_OKAY, None
|
|
|
|
def __channel_name_request__(self, msg):
|
|
data = msg.get_data()
|
|
if data is None:
|
|
return STATUS_OKAY, self.__channel_name__
|
|
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__))
|
|
return STATUS_OKAY, None
|
|
|
|
def __check_frame_checksum__(self, frame):
|
|
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__ = {}
|
|
|
|
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()
|
|
|
|
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())
|
|
)
|
|
|
|
def __data_available_callback__(self, comm_inst):
|
|
frame = comm_inst.receive()
|
|
msg = self.__analyse_frame__(frame)
|
|
if not self.__check_frame_checksum__(frame):
|
|
# Wrong Checksum
|
|
self.logger.log(self.__status_log_lvl__(STATUS_CHECKSUM_ERROR), "%s Received message has an invalid checksum. Message will be ignored.", self.__log_prefix__())
|
|
return # No response needed
|
|
elif not self.check_authentification_state() and self.__authentification_required__(msg.get_service_id(), msg.get_data_id()):
|
|
# Authentification required
|
|
self.__log_msg__(msg, 'RX <-')
|
|
if msg.get_service_id() in self.__sid_response_dict__.keys():
|
|
self.logger.log(self.__status_log_lvl__(STATUS_AUTH_REQUIRED), "%s Authentification is required. Just sending negative response.", self.__log_prefix__())
|
|
status = STATUS_AUTH_REQUIRED
|
|
data = None
|
|
else:
|
|
self.logger.log(self.__status_log_lvl__(STATUS_AUTH_REQUIRED), "%s Authentification is required. Incomming message will be ignored.", self.__log_prefix__())
|
|
return # No response needed
|
|
else:
|
|
# Valid message
|
|
self.__log_msg__(msg, 'RX <-')
|
|
callback, args, kwargs = self.__callbacks__.get(msg.get_service_id(), msg.get_data_id())
|
|
if msg.get_service_id() in self.__sid_response_dict__.keys():
|
|
#
|
|
# REQUEST RECEIVED
|
|
#
|
|
if callback is None:
|
|
self.logger.warning("%s Incomming message with no registered callback. Sending negative response.", self.__log_prefix__())
|
|
status = STATUS_BUFFERING_UNHANDLED_REQUEST
|
|
data = None
|
|
else:
|
|
self.logger.debug("%s Executing callback %s to process received data", self.__log_prefix__(), callback.__name__)
|
|
try:
|
|
status, data = callback(msg, *args, **kwargs)
|
|
except Exception as e:
|
|
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())))
|
|
status = STATUS_CALLBACK_ERROR
|
|
data = None
|
|
else:
|
|
#
|
|
# RESPONSE RECEIVED
|
|
#
|
|
if callback is None:
|
|
self.__buffer_received_data__(msg)
|
|
else:
|
|
self.logger.debug("%s Executing callback %s to process received data", self.__log_prefix__(), callback.__name__)
|
|
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())))
|
|
return # No response needed
|
|
self.send(self.__sid_response_dict__[msg.get_service_id()], msg.get_data_id(), data, status=status)
|
|
|
|
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)))
|
|
|
|
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)
|
|
|
|
def __log_prefix__(self):
|
|
return 'prot-client:' if self.__comm_inst__.IS_CLIENT else 'prot-server:'
|
|
|
|
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})
|
|
|
|
def __status_log_lvl__(self, status):
|
|
return STATUS_LOG_LVL.get(status, logging.CRITICAL)
|
|
|
|
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
|
|
|
|
def authentificate(self, timeout=2):
|
|
"""
|
|
This method authetificates the client at the server.
|
|
|
|
: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:
|
|
self.__authentification_state__ = AUTH_STATE_SEED_REQUESTED
|
|
self.send(SID_AUTH_REQUEST, DID_AUTH_SEED, None)
|
|
cnt = 0
|
|
while cnt < timeout * 10:
|
|
time.sleep(0.1)
|
|
if self.__authentification_state__ == AUTH_STATE_TRUSTED_CONNECTION:
|
|
return True
|
|
elif self.__authentification_state__ == AUTH_STATE_UNTRUSTED_CONNECTION:
|
|
break
|
|
cnt += 1
|
|
return False
|
|
|
|
def check_authentification_state(self):
|
|
"""
|
|
This Method return the Authitification State as boolean value.
|
|
|
|
:return: True, if authentification state is okay, otherwise False
|
|
:rtype: bool
|
|
"""
|
|
return self.__secret__ is None or self.__authentification_state__ == AUTH_STATE_TRUSTED_CONNECTION
|
|
|
|
def connection_established(self):
|
|
"""
|
|
This Method returns the Connection state including authentification as a boolean value.
|
|
|
|
: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())
|
|
|
|
def is_connected(self):
|
|
"""
|
|
This Methods returns Connection state of the Communication Instance :func:`comm_instance.is_connected`.
|
|
|
|
:return: True if the :class:`comm_instance` is connected, otherwise False..
|
|
:rtype: bool
|
|
"""
|
|
return self.__comm_inst__.is_connected()
|
|
|
|
def receive(self, service_id, data_id, timeout=1):
|
|
"""
|
|
This Method returns a message object for a defined message or None, if this message is not available after the given timout.
|
|
|
|
: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 (%ss): Requested data (service_id: %s; data_id: %s) not in buffer.', self.__log_prefix__(), repr(timeout), repr(service_id), repr(data_id))
|
|
return data
|
|
|
|
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):
|
|
"""
|
|
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.
|
|
|
|
: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
|
|
|
|
.. note:: The :func:`callback` is priorised in the following order:
|
|
|
|
* Callbacks with defined Service-ID and Data-ID.
|
|
* Callbacks with a defined Service-ID and all Data-IDs.
|
|
* Callbacks with a defined Data-ID and all Service-IDs.
|
|
* Unspecific Callbacks.
|
|
|
|
.. note:: The :func:`callback` is executed with these arguments:
|
|
|
|
**Parameters given at the callback call:**
|
|
|
|
* 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_*`.
|
|
* :const:`response_data`: A JSON iterable object to be used as data for the response.
|
|
|
|
.. note:: Only registered services will respond via the callbacks return values with the same data_id.
|
|
"""
|
|
self.__callbacks__.add(service_id, data_id, callback, *args, **kwargs)
|
|
|
|
def send(self, service_id, data_id, data, status=STATUS_OKAY, timeout=2):
|
|
"""
|
|
This methods sends out a message with the given content.
|
|
|
|
: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
|
|
"""
|
|
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):
|
|
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)
|
|
else:
|
|
# Authentification required
|
|
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))
|
|
return False
|
|
|
|
|
|
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.
|
|
See also parent :py:class:`pure_json_protocol`.
|
|
|
|
.. note::
|
|
This class is depreceated and here for compatibility reasons (to support old clients or servers). Usage of :class:`pure_json_protocol` is recommended.
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
pure_json_protocol.__init__(self, *args, **kwargs)
|
|
|
|
def __analyse_frame__(self, frame):
|
|
status, service_id, data_id = struct.unpack('>III', frame[0:12])
|
|
if sys.version_info >= (3, 0):
|
|
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)
|
|
|
|
def __build_frame__(self, msg):
|
|
frame = struct.pack('>III', msg.get_status(), msg.get_service_id(), msg.get_data_id())
|
|
if sys.version_info >= (3, 0):
|
|
frame += bytes(json.dumps(msg.get_data()), 'utf-8')
|
|
frame += self.__calc_chksum__(frame)
|
|
else:
|
|
frame += json.dumps(msg.get_data())
|
|
frame += self.__calc_chksum__(frame)
|
|
return frame
|
|
|
|
def __calc_chksum__(self, raw_data):
|
|
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)
|
|
|
|
def __check_frame_checksum__(self, frame):
|
|
return self.__calc_chksum__(frame[:-1]) == frame[-1:]
|