diff --git a/__init__.py b/__init__.py index 6e68e0c..6be8114 100644 --- a/__init__.py +++ b/__init__.py @@ -15,12 +15,13 @@ socket_protocol (Socket Protocol) **Submodules:** +* :class:`socket_protocol.data_storage` * :class:`socket_protocol.pure_json_protocol` * :class:`socket_protocol.struct_json_protocol` **Unittest:** - See also the :download:`unittest <../pylibs/socket_protocol/_testresults_/unittest.pdf>` documentation. + See also the :download:`unittest ` documentation. **Module Documentation:** @@ -52,12 +53,80 @@ For more Information read the sphinx documentation.""" % __name__.replace('_', ' __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'""" + +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): + 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): @@ -67,31 +136,28 @@ class _callback_storage(dict): self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__ + '.' + channel_name) def get(self, service_id, data_id): - if service_id is not None and data_id is not None: - try: - return self[service_id][data_id] - except KeyError: - pass # nothing to append - if data_id is not None: - try: - return self[None][data_id] - except KeyError: - pass # nothing to append - if service_id is not None: - try: - return self[service_id][None] - except KeyError: - pass # nothing to append - try: + 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] - except KeyError: - pass # nothing to append - return (None, 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 cb_data != (None, None, None): - self.logger.warning("Overwriting existing callback %s for service_id (%s) and data_id (%s) to %s!", repr(cb_data[0].__name__), repr(service_id), repr(data_id), repr(callback.__name__)) + 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) @@ -99,6 +165,8 @@ class _callback_storage(dict): 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. @@ -107,70 +175,74 @@ class data_storage(dict): :type data_id: int :param data: The transfered data. :type data: any - - This is a storage object for socket_protocol messages. """ 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): """ - :param default: The default value, if no data is available. - 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): """ - :param default: The default value, if no data is available. - 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): """ - :param default: The default value, if no data is available. - 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): """ - :param default: The default value, if no data is available. - 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 (True) to enable automatic authentification, otherwise you need to do it manually, if needed. + :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:: This `class` supports to transfer a Service-ID, Data-ID and Data. + .. hint:: - * The Service-ID is designed to identify the type of the communication (e.g. READ_REQUEST, WRITE_REQUEST, READ_RESPONSE, WRITE_RESPONSE, ...) + * 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 if needed. + * 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. @@ -187,107 +259,60 @@ class pure_json_protocol(object): """ DEFAULT_CHANNEL_NAME = 'all_others' - SID_AUTH_SEED_REQUEST = 1 - """SID for requesting a seed for authentification""" - SID_AUTH_KEY_REQUEST = 2 - """SID for requesting a key for the given seed""" - SID_AUTH_KEY_CHECK_REQUEST = 3 - """SID for request for checking a key""" - SID_AUTH_KEY_CHECK_RESPONSE = 4 - """SID for the authentification response""" - SID_CHANNEL_NAME_REQUEST = 5 - """SID for requesting a channel name exchange""" - SID_CHANNEL_NAME_RESPONSE = 6 - """SID for the channel name response""" - 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""" - - SID__RESPONSE_DICT = {SID_AUTH_SEED_REQUEST: SID_AUTH_KEY_REQUEST, - SID_AUTH_KEY_REQUEST: SID_AUTH_KEY_CHECK_REQUEST, - SID_AUTH_KEY_CHECK_REQUEST: SID_AUTH_KEY_CHECK_RESPONSE, - SID_CHANNEL_NAME_REQUEST: SID_CHANNEL_NAME_RESPONSE, - SID_READ_REQUEST: SID_READ_RESPONSE, - SID_WRITE_REQUEST: SID_WRITE_RESPONSE, - SID_EXECUTE_REQUEST: SID_EXECUTE_RESPONSE} - """Dictionary to get the SID for the response by the key which is the SID for the request""" - - SID__NO_AUTH_LIST = [ - SID_AUTH_SEED_REQUEST, - SID_AUTH_KEY_REQUEST, - SID_AUTH_KEY_CHECK_REQUEST, - SID_AUTH_KEY_CHECK_RESPONSE, - SID_CHANNEL_NAME_REQUEST, - SID_CHANNEL_NAME_RESPONSE - ] - """List of SIDs without need of an authentification""" - - STATUS_OKAY = 0 - """Status for 'okay'""" - STATUS_BUFFERING_UNHANDLED_REQUEST = 1 - """Status for 'unhandled request'""" - STATUS_AUTH_REQUIRED = 2 - """Status for 'authentification is required'""" - STATUS_SERVICE_OR_DATA_UNKNOWN = 3 - """Status for 'service or data unknown'""" - STATUS_CHECKSUM_ERROR = 4 - """Status for 'checksum error'""" - STATUS_OPERATION_NOT_PERMITTED = 5 - """Status for 'operation not permitted'""" - STATUS__NAMES = {STATUS_OKAY: 'Okay', - STATUS_BUFFERING_UNHANDLED_REQUEST: 'Request has no callback. Data buffered.', - STATUS_AUTH_REQUIRED: 'Authentification required', - STATUS_SERVICE_OR_DATA_UNKNOWN: 'Service or Data unknown', - STATUS_CHECKSUM_ERROR: 'Checksum Error', - STATUS_OPERATION_NOT_PERMITTED: 'Operation not permitted'} - """Status names for previous defined states""" - - AUTH_STATE_UNKNOWN_CLIENT = 0 - """Authentification Status for 'Unknown Client'""" - 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_CLIENT = 4 - """Authentification Status for 'Trusted Connection'""" - AUTH_STATE__NAMES = {AUTH_STATE_UNKNOWN_CLIENT: 'Unknown Client', - 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_CLIENT: 'Trusted Connection'} - """Authentification Status names for previous defined authentification states""" - 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.__callbacks__ = _callback_storage(channel_name) + 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.__callbacks__.add(self.SID_AUTH_SEED_REQUEST, 0, self.__authentificate_create_seed__) - self.__callbacks__.add(self.SID_AUTH_KEY_REQUEST, 0, self.__authentificate_create_key__) - self.__callbacks__.add(self.SID_AUTH_KEY_CHECK_REQUEST, 0, self.__authentificate_check_key__) - self.__callbacks__.add(self.SID_AUTH_KEY_CHECK_RESPONSE, 0, self.__authentificate_process_feedback__) - self.__callbacks__.add(self.SID_CHANNEL_NAME_REQUEST, 0, self.__channel_name_request__) - self.__callbacks__.add(self.SID_CHANNEL_NAME_RESPONSE, 0, self.__channel_name_response__) + + 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): @@ -298,39 +323,35 @@ class pure_json_protocol(object): def __authentificate_check_key__(self, msg): key = msg.get_data() if key == self.__authentificate_salt_and_hash__(self.__seed__): - self.__authentification_state__ = self.AUTH_STATE_TRUSTED_CLIENT - self.logger.info("%s Got correct key, sending positive authentification feedback", self.__log_prefix__()) - return self.STATUS_OKAY, True + self.__authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION + return STATUS_OKAY, True else: - self.__authentification_state__ = self.AUTH_STATE_UNKNOWN_CLIENT - self.logger.info("%s Got incorrect key, sending negative authentification feedback", self.__log_prefix__()) - return self.STATUS_OKAY, False + self.__authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION + return STATUS_OKAY, False def __authentificate_create_key__(self, msg): - self.logger.info("%s Got seed, sending key for authentification", self.__log_prefix__()) - self.__authentification_state__ = self.AUTH_STATE_KEY_TRANSFERRED + self.__authentification_state__ = AUTH_STATE_KEY_TRANSFERRED seed = msg.get_data() key = self.__authentificate_salt_and_hash__(seed) - return self.STATUS_OKAY, key + self.send(SID_AUTH_REQUEST, DID_AUTH_KEY, key) def __authentificate_create_seed__(self, msg): - self.logger.info("%s Got seed request, sending seed for authentification", self.__log_prefix__()) - self.__authentification_state__ = self.AUTH_STATE_SEED_TRANSFERRED + 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 self.STATUS_OKAY, self.__seed__ + return STATUS_OKAY, self.__seed__ def __authentificate_process_feedback__(self, msg): feedback = msg.get_data() if feedback: - self.__authentification_state__ = self.AUTH_STATE_TRUSTED_CLIENT + self.__authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION self.logger.info("%s Got positive authentification feedback", self.__log_prefix__()) else: - self.__authentification_state__ = self.AUTH_STATE_UNKNOWN_CLIENT + self.__authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION self.logger.warning("%s Got negative authentification feedback", self.__log_prefix__()) - return self.STATUS_OKAY, None + return STATUS_OKAY, None def __authentificate_salt_and_hash__(self, seed): if sys.version_info >= (3, 0): @@ -339,8 +360,11 @@ class pure_json_protocol(object): 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_UNKNOWN_CLIENT", self.__log_prefix__()) - self.__authentification_state__ = self.AUTH_STATE_UNKNOWN_CLIENT + 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__: @@ -371,12 +395,12 @@ class pure_json_protocol(object): 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 self.STATUS_OKAY, None + return STATUS_OKAY, None def __channel_name_request__(self, msg): data = msg.get_data() if data is None: - return self.STATUS_OKAY, self.__channel_name__ + return STATUS_OKAY, self.__channel_name__ else: prev_channel_name = self.__channel_name__ self.__init_channel_name__(data) @@ -384,7 +408,7 @@ class pure_json_protocol(object): 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 self.STATUS_OKAY, None + return STATUS_OKAY, None def __check_frame_checksum__(self, frame): return self.__calc_chksum__(frame[:-4]) == frame[-4:] @@ -393,63 +417,77 @@ class pure_json_protocol(object): 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 __data_available_callback__(self, comm_inst): frame = comm_inst.receive() + msg = self.__analyse_frame__(frame) if not self.__check_frame_checksum__(frame): - self.logger.warning("%s Received message has a wrong checksum and will be ignored: %s.", self.__log_prefix__(), stringtools.hexlify(frame)) + # Wrong Checksum + self.logger.warning("%s RX <- Received message has a wrong 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 + if msg.get_service_id() in self.__sid_response_dict__.keys(): + self.logger.warning("%s RX <- Authentification is required. Just sending negative response.", self.__log_prefix__()) + status = STATUS_AUTH_REQUIRED + data = None + else: + self.logger.warning("%s RX <- Authentification is required. Message will be ignored.", self.__log_prefix__()) + return # No response needed else: - msg = self.__analyse_frame__(frame) + # Valid message self.logger.info( - '%s RX <- status: %s, service_id: %s, data_id: %s, data: "%s"', + '%s RX <- %s, %s, data: "%s"', self.__log_prefix__(), - repr(msg.get_status()), - repr(msg.get_service_id()), - repr(msg.get_data_id()), + self.__get_message_name__(msg.get_service_id(), msg.get_data_id()), + self.__get_status_name__(msg.get_status()), repr(msg.get_data()) ) + if msg.get_status() not in [STATUS_OKAY]: + self.logger.warning("%s RX <- Message has a peculiar status: %s", self.__log_prefix__(), self.__get_status_name__(msg.get_status())) 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(): + if msg.get_service_id() in self.__sid_response_dict__.keys(): # # REQUEST RECEIVED # - if self.__secret__ is not None and not self.check_authentification_state() and msg.get_service_id() not in self.SID__NO_AUTH_LIST: - status = self.STATUS_AUTH_REQUIRED - data = None - self.logger.warning("%s Received message needs authentification: %s. Sending negative response.", self.__log_prefix__(), self.AUTH_STATE__NAMES.get(self.__authentification_state__, 'Unknown authentification status!')) - elif callback is None: - self.logger.warning("%s Received message with no registered callback. Sending negative response.", self.__log_prefix__()) - status = self.STATUS_BUFFERING_UNHANDLED_REQUEST + if callback is None: + self.logger.warning("%s RX <- Message with no registered callback. Sending negative response.", self.__log_prefix__()) + status = STATUS_BUFFERING_UNHANDLED_REQUEST data = None else: try: - self.logger.debug("%s Executing callback %s to process received data", self.__log_prefix__(), callback.__name__) + self.logger.debug("%s RX <- Executing callback %s to process received data", self.__log_prefix__(), callback.__name__) status, data = callback(msg, *args, **kwargs) - except TypeError: - raise TypeError('Check return value of callback function {callback_name} for service_id {service_id} and data_id {data_id}'.format(callback_name=callback.__name__, service_id=repr(msg.get_service_id()), data_id=repr(msg.get_data_id()))) - self.send(self.SID__RESPONSE_DICT[msg.get_service_id()], msg.get_data_id(), data, status=status) + except Exception: + logger.error('{lp} RX <- Exception raised. Check callback {callback_name} and it\'s return values for service_id {service_id} and data_id {data_id}'.format(lp=self.__log_prefix__(), callback_name=callback.__name__, service_id=repr(msg.get_service_id()), data_id=repr(msg.get_data_id()))) + status = STATUS_CALLBACK_ERROR + data = None else: # # RESPONSE RECEIVED # - if msg.get_status() not in [self.STATUS_OKAY]: - self.logger.warning("%s Received message has a peculiar status: %s", self.__log_prefix__(), self.STATUS__NAMES.get(msg.get_status(), 'Unknown status response!')) if callback is None: - status = self.STATUS_OKAY - data = None self.__buffer_received_data__(msg) else: - try: - self.logger.debug("%s Executing callback %s to process received data", self.__log_prefix__(), callback.__name__) - status, data = callback(msg, *args, **kwargs) - except TypeError: - raise TypeError('Check return value of callback function {callback_name} for service_id {service_id} and data_id {data_id}'.format(callback_name=callback.__name__, service_id=repr(msg.get_service_id()), data_id=repr(msg.get_data_id()))) + self.logger.debug("%s Executing callback %s to process received data", self.__log_prefix__(), callback.__name__) + callback(msg, *args, **kwargs) + return # No response needed + self.send(self.__sid_response_dict__[msg.get_service_id()], msg.get_data_id(), data, status=status) - def __connection_established__(self): - self.__clean_receive_buffer__() - if not self.__comm_inst__.IS_CLIENT: - self.send(self.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 __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) @@ -460,47 +498,119 @@ class pure_json_protocol(object): self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__ + '.' + channel_name) def __log_prefix__(self): - return ' SP client:' if self.__comm_inst__.IS_CLIENT else ' SP server:' + return 'SP client:' if self.__comm_inst__.IS_CLIENT else 'SP 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 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 - This method authetificates the client at the server. - .. 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__ = self.AUTH_STATE_SEED_REQUESTED - self.logger.info("%s Requesting seed for authentification", self.__log_prefix__()) - self.send(self.SID_AUTH_SEED_REQUEST, 0, 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__ == self.AUTH_STATE_TRUSTED_CLIENT: + if self.__authentification_state__ == AUTH_STATE_TRUSTED_CONNECTION: return True - elif self.__authentification_state__ == self.AUTH_STATE_UNKNOWN_CLIENT: + 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.__authentification_state__ == self.AUTH_STATE_TRUSTED_CLIENT + 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 """ @@ -508,15 +618,17 @@ class pure_json_protocol(object): 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 - - This methods returns the return value of :func:`comm_instance.is_connected`. """ 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. @@ -547,37 +659,44 @@ class pure_json_protocol(object): 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 - :returns: True, if registration was successfull; False, if registration failed (e.g. existance of a callback for this configuration) - :rtype: bool - - This method registers a callback for the given parameters. Givin ``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. .. note:: The :func:`callback` is priorised in the following order: * Callbacks with defined Service-ID and Data-ID. - * Callbacks with a defined Data-ID. - * Callbacks with a defined Service-ID. - * Unspecific Callbacks + * 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: - :param msg: A :class:`data_storage` object containing all message information. - :returns: (:const:`status`, :const:`response_data`) + **Parameters given at the callback call:** - * :const:`status`: A status as defined as a constant of this class :const:`STA_*` to be used as status for the response. + * 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 callbacks defined in :const:`pure_json_protocol.SID__RESPONSE_DICT` will send a response, using a Service-ID given in the dict and the same Data-ID to the client. + .. 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, log_lvl=logging.INFO): + 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. @@ -588,15 +707,22 @@ class pure_json_protocol(object): :type status: int :param timeout: The timeout for sending data (e.g. time to establish new connection). :type timeout: float - :param rx_log_lvl: The log level to log outgoing TX-data - :type rx_log_lvl: int :return: True if data had been sent, otherwise False. :rtype: bool - - This methods sends out a message with the given content. """ - self.logger.log(log_lvl, '%s TX -> status: %d, service_id: %d, data_id: %d, data: "%s"', self.__log_prefix__(), status, service_id, data_id, repr(data)) - return self.__comm_inst__.send(self.__build_frame__(service_id, data_id, data, status), timeout=timeout, log_lvl=logging.DEBUG) + 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): + self.logger.info( + '%s TX <- %s, %s, data: "%s"', + self.__log_prefix__(), + self.__get_message_name__(service_id, data_id), + self.__get_status_name__(status), + repr(data) + ) + return self.__comm_inst__.send(self.__build_frame__(service_id, data_id, data, status), timeout=timeout, log_lvl=logging.DEBUG) + else: + # Authentification required + self.logger.warning("%s TX -> Authentification is required. 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): @@ -617,7 +743,7 @@ class struct_json_protocol(pure_json_protocol): data = json.loads(frame[12:-1]) return self.__mk_msg__(status, service_id, data_id, data) - def __build_frame__(self, service_id, data_id, data, status=pure_json_protocol.STATUS_OKAY): + def __build_frame__(self, service_id, data_id, data, status=STATUS_OKAY): frame = struct.pack('>III', status, service_id, data_id) if sys.version_info >= (3, 0): frame += bytes(json.dumps(data), 'utf-8') diff --git a/_docs_/_downloads/37503cb17b21b2c78bb8b07730976f24/unittest.pdf b/_docs_/_downloads/37503cb17b21b2c78bb8b07730976f24/unittest.pdf new file mode 100644 index 0000000..4ab710a Binary files /dev/null and b/_docs_/_downloads/37503cb17b21b2c78bb8b07730976f24/unittest.pdf differ diff --git a/_docs_/_downloads/f482679fb1771f4d05403bb87fd0cc34/unittest.pdf b/_docs_/_downloads/f482679fb1771f4d05403bb87fd0cc34/unittest.pdf deleted file mode 100644 index d04f054..0000000 Binary files a/_docs_/_downloads/f482679fb1771f4d05403bb87fd0cc34/unittest.pdf and /dev/null differ diff --git a/_docs_/genindex.html b/_docs_/genindex.html index 497820b..312f218 100644 --- a/_docs_/genindex.html +++ b/_docs_/genindex.html @@ -161,19 +161,27 @@

A

- +
@@ -251,51 +271,43 @@