Python Library Socket Protocol
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

__init__.py 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. """
  5. socket_protocol (Socket Protocol)
  6. =================================
  7. **Author:**
  8. * Dirk Alders <sudo-dirk@mount-mockery.de>
  9. **Description:**
  10. This Module supports point to point communication for client-server issues.
  11. **Submodules:**
  12. * :class:`socket_protocol.data_storage`
  13. * :class:`socket_protocol.pure_json_protocol`
  14. * :class:`socket_protocol.struct_json_protocol`
  15. **Unittest:**
  16. See also the :download:`unittest <socket_protocol/_testresults_/unittest.pdf>` documentation.
  17. **Module Documentation:**
  18. """
  19. __DEPENDENCIES__ = ['stringtools']
  20. import stringtools
  21. import binascii
  22. import hashlib
  23. import json
  24. import logging
  25. import os
  26. import struct
  27. import sys
  28. import time
  29. try:
  30. from config import APP_NAME as ROOT_LOGGER_NAME
  31. except ImportError:
  32. ROOT_LOGGER_NAME = 'root'
  33. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  34. __DESCRIPTION__ = """The Module {\\tt %s} is designed for point to point communication for client-server issues.
  35. For more Information read the sphinx documentation.""" % __name__.replace('_', '\_')
  36. """The Module Description"""
  37. __INTERPRETER__ = (3, )
  38. """The Tested Interpreter-Versions"""
  39. SID_AUTH_REQUEST = 0
  40. """SID for authentification request"""
  41. SID_AUTH_RESPONSE = 1
  42. """SID for authentification response"""
  43. DID_AUTH_SEED = 0
  44. """DID for authentification (seed)"""
  45. DID_AUTH_KEY = 1
  46. """DID for authentification (key)"""
  47. SID_CHANNEL_NAME_REQUEST = 8
  48. """SID for channel name exchange request """
  49. SID_CHANNEL_NAME_RESPONSE = 9
  50. """SID for channel name exchange response"""
  51. DID_CHANNEL_NAME = 0
  52. """DID for channel name """
  53. SID_READ_REQUEST = 10
  54. """SID for a read data request"""
  55. SID_READ_RESPONSE = 11
  56. """SID for read data response"""
  57. SID_WRITE_REQUEST = 20
  58. """SID for a write data request"""
  59. SID_WRITE_RESPONSE = 21
  60. """SID for a write data response"""
  61. SID_EXECUTE_REQUEST = 30
  62. """SID for a execute request"""
  63. SID_EXECUTE_RESPONSE = 31
  64. """SID for a execute response"""
  65. STATUS_OKAY = 0
  66. """Status for 'okay'"""
  67. STATUS_BUFFERING_UNHANDLED_REQUEST = 1
  68. """Status for 'unhandled request'"""
  69. STATUS_CALLBACK_ERROR = 2
  70. """Status for 'callback errors'"""
  71. STATUS_AUTH_REQUIRED = 3
  72. """Status for 'authentification is required'"""
  73. STATUS_SERVICE_OR_DATA_UNKNOWN = 4
  74. """Status for 'service or data unknown'"""
  75. STATUS_CHECKSUM_ERROR = 5
  76. """Status for 'checksum error'"""
  77. STATUS_OPERATION_NOT_PERMITTED = 6
  78. """Status for 'operation not permitted'"""
  79. STATUS_LOG_LVL = {
  80. STATUS_OKAY: logging.INFO,
  81. STATUS_BUFFERING_UNHANDLED_REQUEST: logging.WARNING,
  82. STATUS_CALLBACK_ERROR: logging.ERROR,
  83. STATUS_AUTH_REQUIRED: logging.WARNING,
  84. STATUS_SERVICE_OR_DATA_UNKNOWN: logging.ERROR,
  85. STATUS_CHECKSUM_ERROR: logging.ERROR,
  86. STATUS_OPERATION_NOT_PERMITTED: logging.WARNING,
  87. }
  88. """Status depending log level for messages"""
  89. AUTH_STATE_UNTRUSTED_CONNECTION = 0
  90. """Authentification Status for an 'Untrusted Connection'"""
  91. AUTH_STATE_SEED_REQUESTED = 1
  92. """Authentification Status for 'Seed was requested'"""
  93. AUTH_STATE_SEED_TRANSFERRED = 2
  94. """Authentification Status for 'Seed has been sent'"""
  95. AUTH_STATE_KEY_TRANSFERRED = 3
  96. """Authentification Status for 'Key has been sent'"""
  97. AUTH_STATE_TRUSTED_CONNECTION = 4
  98. """Authentification Status for a 'Trusted Connection'"""
  99. AUTH_STATE__NAMES = {AUTH_STATE_UNTRUSTED_CONNECTION: 'Untrusted Connection',
  100. AUTH_STATE_SEED_REQUESTED: 'Seed was requested',
  101. AUTH_STATE_SEED_TRANSFERRED: 'Seed has been sent',
  102. AUTH_STATE_KEY_TRANSFERRED: 'Key has been sent',
  103. AUTH_STATE_TRUSTED_CONNECTION: 'Trusted Connection'}
  104. """Authentification Status names for previous defined authentification states"""
  105. class RequestSidExistsError(Exception):
  106. pass
  107. class ResponseSidExistsError(Exception):
  108. pass
  109. class _callback_storage(dict):
  110. DEFAULT_CHANNEL_NAME = 'all_others'
  111. def __init__(self, channel_name, log_prefix):
  112. self.init_channel_name(channel_name)
  113. self.__log_prefix__ = log_prefix
  114. dict.__init__(self)
  115. def init_channel_name(self, channel_name):
  116. if channel_name is None:
  117. self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__ + '.' + self.DEFAULT_CHANNEL_NAME)
  118. else:
  119. self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__ + '.' + channel_name)
  120. def get(self, service_id, data_id):
  121. if dict.get(self, service_id, {}).get(data_id, None) is not None:
  122. return self[service_id][data_id]
  123. elif dict.get(self, service_id, {}).get(None, None) is not None:
  124. return self[service_id][None]
  125. elif dict.get(self, None, {}).get(data_id, None) is not None:
  126. return self[None][data_id]
  127. elif dict.get(self, None, {}).get(None, None) is not None:
  128. return self[None][None]
  129. else:
  130. return (None, None, None)
  131. def add(self, service_id, data_id, callback, *args, **kwargs):
  132. cb_data = self.get(service_id, data_id)
  133. if dict.get(self, service_id, {}).get(data_id, None) is not None:
  134. if callback is None:
  135. 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))
  136. del(self[service_id][data_id])
  137. return
  138. else:
  139. 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__))
  140. else:
  141. 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))
  142. if service_id not in self:
  143. self[service_id] = {}
  144. self[service_id][data_id] = (callback, args, kwargs)
  145. class data_storage(dict):
  146. """
  147. This is a storage object for socket_protocol messages.
  148. :param status: The message status.
  149. :type status: int
  150. :param service_id: The Service-ID.
  151. :type service_id: int
  152. :param data_id: The Data-ID.
  153. :type data_id: int
  154. :param data: The transfered data.
  155. :type data: any
  156. """
  157. KEY_STATUS = 'status'
  158. KEY_SERVICE_ID = 'service_id'
  159. KEY_DATA_ID = 'data_id'
  160. KEY_DATA = 'data'
  161. ALL_KEYS = [KEY_DATA, KEY_DATA_ID, KEY_SERVICE_ID, KEY_STATUS]
  162. def __init__(self, *args, **kwargs):
  163. dict.__init__(self, *args, **kwargs)
  164. for key in self.ALL_KEYS:
  165. if key not in self:
  166. self[key] = None
  167. def get_status(self, default=None):
  168. """
  169. This Method returns the message status.
  170. :param default: The default value, if no data is available.
  171. """
  172. return self.get(self.KEY_STATUS, default)
  173. def get_service_id(self, default=None):
  174. """
  175. This Method returns the message Service-ID.
  176. :param default: The default value, if no data is available.
  177. """
  178. return self.get(self.KEY_SERVICE_ID, default)
  179. def get_data_id(self, default=None):
  180. """
  181. This Method returns the message Data-ID.
  182. :param default: The default value, if no data is available.
  183. """
  184. return self.get(self.KEY_DATA_ID, default)
  185. def get_data(self, default=None):
  186. """
  187. This Method returns the message data.
  188. :param default: The default value, if no data is available.
  189. """
  190. return self.get(self.KEY_DATA, default)
  191. class pure_json_protocol(object):
  192. """
  193. This `class` supports to transfer a message and it's data.
  194. :param comm_instance: A communication instance.
  195. :type comm_instance: instance
  196. :param secret: An optinal secret (e.g. created by ``binascii.hexlify(os.urandom(24))``).
  197. :type secret: str
  198. :param auto_auth: An optional parameter to enable (True) automatic authentification, otherwise you need to do it manually, if needed.
  199. :type auto_auth: bool
  200. :param channel_name: An optional parameter to set a channel name for logging of the communication.
  201. :type channel_name: str
  202. .. hint::
  203. * 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`, ...)
  204. * The Data-ID is designed to identify the requests / responses using the same Service_ID.
  205. .. note:: The :class:`comm_instance` needs to have at least the following interface:
  206. * A Method :func:`comm_instance.init_channel_name` to set the channel name.
  207. * A Constant :const:`comm_instance.IS_CLIENT` to identify that the :class:`comm_instance` is a client (True) or a server (False).
  208. * A Method :func:`comm_instance.is_connected` to identify if the instance is connected (True) or not (False).
  209. * A Method :func:`comm_instance.reconnect` to initiate a reconnect.
  210. * A Method :func:`comm_instance.register_callback` to register a data available callback.
  211. * A Method :func:`comm_instance.register_connect_callback` to register a connect callback.
  212. * A Method :func:`comm_instance.register_disconnect_callback` to register a disconnect callback.
  213. * A Method :func:`comm_instance.send` to send data via the :class:`comm_instance`.
  214. .. 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.
  215. .. note:: The :const:`channel_name`-exchange will be initiated by the client directly after the the connection is established.
  216. * 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.
  217. **Example:**
  218. .. literalinclude:: socket_protocol/_examples_/socket_protocol_client.py
  219. and
  220. .. literalinclude:: socket_protocol/_examples_/socket_protocol_server.py
  221. Will result to the following output:
  222. .. literalinclude:: socket_protocol/_examples_/socket_protocol_client.log
  223. """
  224. DEFAULT_CHANNEL_NAME = 'all_others'
  225. def __init__(self, comm_instance, secret=None, auto_auth=False, channel_name=None):
  226. self.__comm_inst__ = comm_instance
  227. self.__secret__ = secret
  228. self.__auto_auth__ = auto_auth
  229. #
  230. self.__auth_whitelist__ = {}
  231. self.__sid_response_dict__ = {}
  232. self.__sid_name_dict__ = {}
  233. self.__did_name_dict__ = {}
  234. #
  235. self.__callbacks__ = _callback_storage(channel_name, self.__log_prefix__)
  236. self.__init_channel_name__(channel_name)
  237. #
  238. self.__status_name_dict = {}
  239. self.add_status(STATUS_OKAY, 'okay')
  240. self.add_status(STATUS_BUFFERING_UNHANDLED_REQUEST, 'no callback for service, data buffered')
  241. self.add_status(STATUS_CALLBACK_ERROR, 'callback error')
  242. self.add_status(STATUS_AUTH_REQUIRED, 'authentification required')
  243. self.add_status(STATUS_SERVICE_OR_DATA_UNKNOWN, 'service or data unknown')
  244. self.add_status(STATUS_CHECKSUM_ERROR, 'checksum error')
  245. self.add_status(STATUS_OPERATION_NOT_PERMITTED, 'operation not permitted')
  246. #
  247. self.__clean_receive_buffer__()
  248. self.add_service(SID_AUTH_REQUEST, SID_AUTH_RESPONSE, 'authentification request', 'authentification response')
  249. self.add_data((SID_AUTH_REQUEST, SID_AUTH_RESPONSE), DID_AUTH_SEED, 'seed')
  250. self.add_data(SID_AUTH_REQUEST, DID_AUTH_KEY, 'key')
  251. self.add_data(SID_AUTH_RESPONSE, DID_AUTH_KEY, 'key')
  252. self.add_msg_to_auth_whitelist_(SID_AUTH_REQUEST, DID_AUTH_SEED)
  253. self.add_msg_to_auth_whitelist_(SID_AUTH_RESPONSE, DID_AUTH_SEED)
  254. self.add_msg_to_auth_whitelist_(SID_AUTH_REQUEST, DID_AUTH_KEY)
  255. self.add_msg_to_auth_whitelist_(SID_AUTH_RESPONSE, DID_AUTH_KEY)
  256. self.__callbacks__.add(SID_AUTH_REQUEST, DID_AUTH_SEED, self.__authentificate_create_seed__)
  257. self.__callbacks__.add(SID_AUTH_RESPONSE, DID_AUTH_SEED, self.__authentificate_create_key__)
  258. self.__callbacks__.add(SID_AUTH_REQUEST, DID_AUTH_KEY, self.__authentificate_check_key__)
  259. self.__callbacks__.add(SID_AUTH_RESPONSE, DID_AUTH_KEY, self.__authentificate_process_feedback__)
  260. self.__authentification_state_reset__()
  261. self.add_service(SID_CHANNEL_NAME_REQUEST, SID_CHANNEL_NAME_RESPONSE, 'channel name request', 'channel name response')
  262. self.add_data((SID_CHANNEL_NAME_REQUEST, SID_CHANNEL_NAME_RESPONSE), DID_CHANNEL_NAME, 'name')
  263. self.add_msg_to_auth_whitelist_(SID_CHANNEL_NAME_REQUEST, DID_CHANNEL_NAME)
  264. self.add_msg_to_auth_whitelist_(SID_CHANNEL_NAME_RESPONSE, DID_CHANNEL_NAME)
  265. self.__callbacks__.add(SID_CHANNEL_NAME_REQUEST, DID_CHANNEL_NAME, self.__channel_name_request__)
  266. self.__callbacks__.add(SID_CHANNEL_NAME_RESPONSE, DID_CHANNEL_NAME, self.__channel_name_response__)
  267. self.add_service(SID_READ_REQUEST, SID_READ_RESPONSE, 'read data request', 'read data response')
  268. self.add_service(SID_WRITE_REQUEST, SID_WRITE_RESPONSE, 'write data request', 'write data response')
  269. self.add_service(SID_EXECUTE_REQUEST, SID_EXECUTE_RESPONSE, 'execute request', 'execute response')
  270. self.__seed__ = None
  271. self.__comm_inst__.register_callback(self.__data_available_callback__)
  272. self.__comm_inst__.register_connect_callback(self.__connection_established__)
  273. self.__comm_inst__.register_disconnect_callback(self.__authentification_state_reset__)
  274. self.logger.info('%s Initialisation finished.', self.__log_prefix__())
  275. def __analyse_frame__(self, frame):
  276. if sys.version_info >= (3, 0):
  277. return data_storage(json.loads(frame[:-4].decode('utf-8')))
  278. else:
  279. return data_storage(json.loads(frame[:-4]))
  280. def __authentificate_check_key__(self, msg):
  281. key = msg.get_data()
  282. if key == self.__authentificate_salt_and_hash__(self.__seed__):
  283. self.__authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION
  284. return STATUS_OKAY, True
  285. else:
  286. self.__authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
  287. return STATUS_OKAY, False
  288. def __authentificate_create_key__(self, msg):
  289. self.__authentification_state__ = AUTH_STATE_KEY_TRANSFERRED
  290. seed = msg.get_data()
  291. key = self.__authentificate_salt_and_hash__(seed)
  292. self.send(SID_AUTH_REQUEST, DID_AUTH_KEY, key)
  293. def __authentificate_create_seed__(self, msg):
  294. self.__authentification_state__ = AUTH_STATE_SEED_TRANSFERRED
  295. if sys.version_info >= (3, 0):
  296. self.__seed__ = binascii.hexlify(os.urandom(32)).decode('utf-8')
  297. else:
  298. self.__seed__ = binascii.hexlify(os.urandom(32))
  299. return STATUS_OKAY, self.__seed__
  300. def __authentificate_process_feedback__(self, msg):
  301. feedback = msg.get_data()
  302. if feedback:
  303. self.__authentification_state__ = AUTH_STATE_TRUSTED_CONNECTION
  304. self.logger.info("%s Got positive authentification feedback", self.__log_prefix__())
  305. else:
  306. self.__authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
  307. self.logger.warning("%s Got negative authentification feedback", self.__log_prefix__())
  308. return STATUS_OKAY, None
  309. def __authentificate_salt_and_hash__(self, seed):
  310. if sys.version_info >= (3, 0):
  311. return hashlib.sha512(bytes(seed, 'utf-8') + self.__secret__).hexdigest()
  312. else:
  313. return hashlib.sha512(seed.encode('utf-8') + self.__secret__.encode('utf-8')).hexdigest()
  314. def __authentification_state_reset__(self):
  315. self.logger.info("%s Resetting authentification state to AUTH_STATE_UNTRUSTED_CONNECTION", self.__log_prefix__())
  316. self.__authentification_state__ = AUTH_STATE_UNTRUSTED_CONNECTION
  317. def __authentification_required__(self, service_id, data_id):
  318. return data_id not in self.__auth_whitelist__.get(service_id, [])
  319. def __buffer_received_data__(self, msg):
  320. if not msg.get_service_id() in self.__msg_buffer__:
  321. self.__msg_buffer__[msg.get_service_id()] = {}
  322. if not msg.get_data_id() in self.__msg_buffer__[msg.get_service_id()]:
  323. self.__msg_buffer__[msg.get_service_id()][msg.get_data_id()] = []
  324. self.__msg_buffer__[msg.get_service_id()][msg.get_data_id()].append(msg)
  325. self.logger.debug("%s Message data is stored in buffer and is now ready to be retrieved by receive method", self.__log_prefix__())
  326. def __build_frame__(self, msg):
  327. data_frame = json.dumps(self.__mk_msg__(msg.get_status(), msg.get_service_id(), msg.get_data_id(), msg.get_data()))
  328. if sys.version_info >= (3, 0):
  329. data_frame = bytes(data_frame, 'utf-8')
  330. checksum = self.__calc_chksum__(data_frame)
  331. return data_frame + checksum
  332. def __calc_chksum__(self, raw_data):
  333. return struct.pack('>I', binascii.crc32(raw_data) & 0xffffffff)
  334. @property
  335. def __channel_name__(self):
  336. cn = self.logger.name.split('.')[-1]
  337. if cn != self.DEFAULT_CHANNEL_NAME:
  338. return cn
  339. def __channel_name_response__(self, msg):
  340. data = msg.get_data()
  341. if self.__channel_name__ is None and data is not None:
  342. self.__init_channel_name__(data)
  343. self.logger.info('%s channel name is now %s', self.__log_prefix__(), repr(self.__channel_name__))
  344. return STATUS_OKAY, None
  345. def __channel_name_request__(self, msg):
  346. data = msg.get_data()
  347. if data is None:
  348. return STATUS_OKAY, self.__channel_name__
  349. else:
  350. prev_channel_name = self.__channel_name__
  351. self.__init_channel_name__(data)
  352. if prev_channel_name is not None and prev_channel_name != data:
  353. self.logger.warning('%s overwriting user defined channel name from %s to %s', self.__log_prefix__(), repr(prev_channel_name), repr(data))
  354. elif prev_channel_name is None:
  355. self.logger.info('%s channel name is now %s', self.__log_prefix__(), repr(self.__channel_name__))
  356. return STATUS_OKAY, None
  357. def __check_frame_checksum__(self, frame):
  358. return self.__calc_chksum__(frame[:-4]) == frame[-4:]
  359. def __clean_receive_buffer__(self):
  360. self.logger.debug("%s Cleaning up receive-buffer", self.__log_prefix__())
  361. self.__msg_buffer__ = {}
  362. def __connection_established__(self):
  363. self.__clean_receive_buffer__()
  364. if self.__comm_inst__.IS_CLIENT:
  365. self.send(SID_CHANNEL_NAME_REQUEST, 0, self.__channel_name__)
  366. if self.__auto_auth__ and self.__comm_inst__.IS_CLIENT and self.__secret__ is not None:
  367. self.authentificate()
  368. def __log_msg__(self, msg, rx_tx_prefix):
  369. self.logger.log(
  370. self.__status_log_lvl__(msg.get_status()),
  371. '%s %s %s, %s, data: "%s"',
  372. self.__log_prefix__(),
  373. rx_tx_prefix,
  374. self.__get_message_name__(msg.get_service_id(), msg.get_data_id()),
  375. self.__get_status_name__(msg.get_status()),
  376. repr(msg.get_data())
  377. )
  378. def __data_available_callback__(self, comm_inst):
  379. frame = comm_inst.receive()
  380. msg = self.__analyse_frame__(frame)
  381. if not self.__check_frame_checksum__(frame):
  382. # Wrong Checksum
  383. self.logger.log(self.__status_log_lvl__(STATUS_CHECKSUM_ERROR), "%s Received message has an invalid checksum. Message will be ignored.", self.__log_prefix__())
  384. return # No response needed
  385. elif not self.check_authentification_state() and self.__authentification_required__(msg.get_service_id(), msg.get_data_id()):
  386. # Authentification required
  387. self.__log_msg__(msg, 'RX <-')
  388. if msg.get_service_id() in self.__sid_response_dict__.keys():
  389. self.logger.log(self.__status_log_lvl__(STATUS_AUTH_REQUIRED), "%s Authentification is required. Just sending negative response.", self.__log_prefix__())
  390. status = STATUS_AUTH_REQUIRED
  391. data = None
  392. else:
  393. self.logger.log(self.__status_log_lvl__(STATUS_AUTH_REQUIRED), "%s Authentification is required. Incomming message will be ignored.", self.__log_prefix__())
  394. return # No response needed
  395. else:
  396. # Valid message
  397. self.__log_msg__(msg, 'RX <-')
  398. callback, args, kwargs = self.__callbacks__.get(msg.get_service_id(), msg.get_data_id())
  399. if msg.get_service_id() in self.__sid_response_dict__.keys():
  400. #
  401. # REQUEST RECEIVED
  402. #
  403. if callback is None:
  404. self.logger.warning("%s Incomming message with no registered callback. Sending negative response.", self.__log_prefix__())
  405. status = STATUS_BUFFERING_UNHANDLED_REQUEST
  406. data = None
  407. else:
  408. self.logger.debug("%s Executing callback %s to process received data", self.__log_prefix__(), callback.__name__)
  409. try:
  410. status, data = callback(msg, *args, **kwargs)
  411. except Exception as e:
  412. self.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())))
  413. status = STATUS_CALLBACK_ERROR
  414. data = None
  415. else:
  416. #
  417. # RESPONSE RECEIVED
  418. #
  419. if callback is None:
  420. self.__buffer_received_data__(msg)
  421. else:
  422. self.logger.debug("%s Executing callback %s to process received data", self.__log_prefix__(), callback.__name__)
  423. try:
  424. callback(msg, *args, **kwargs)
  425. except Exception as e:
  426. self.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())))
  427. return # No response needed
  428. self.send(self.__sid_response_dict__[msg.get_service_id()], msg.get_data_id(), data, status=status)
  429. def __get_message_name__(self, service_id, data_id):
  430. return 'service: %s, data_id: %s' % (
  431. self.__sid_name_dict__.get(service_id, repr(service_id)),
  432. self.__did_name_dict__.get(service_id, {}).get(data_id, repr(data_id)),
  433. )
  434. def __get_status_name__(self, status):
  435. return 'status: %s' % (self.__status_name_dict.get(status, 'unknown status: %s' % repr(status)))
  436. def __init_channel_name__(self, channel_name):
  437. self.__comm_inst__.init_channel_name(channel_name)
  438. self.__callbacks__.init_channel_name(channel_name)
  439. if channel_name is None:
  440. self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__ + '.' + self.DEFAULT_CHANNEL_NAME)
  441. else:
  442. self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__ + '.' + channel_name)
  443. def __log_prefix__(self):
  444. return 'prot-client:' if self.__comm_inst__.IS_CLIENT else 'prot-server:'
  445. def __mk_msg__(self, status, service_id, data_id, data):
  446. 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})
  447. def __status_log_lvl__(self, status):
  448. return STATUS_LOG_LVL.get(status, logging.CRITICAL)
  449. def add_data(self, service_id, data_id, name):
  450. """
  451. Method to add a name for a specific message.
  452. :param service_id: The Service-ID of the message. See class definitions starting with ``SID_``.
  453. :type service_id: int or list of ints
  454. :param data_id: The Data-ID of the message.
  455. :type data_id: int
  456. :param name: The Name for the transfered message.
  457. :type name: str
  458. """
  459. try:
  460. iter(service_id)
  461. except Exception:
  462. service_id = (service_id, )
  463. for sid in service_id:
  464. if sid not in self.__did_name_dict__:
  465. self.__did_name_dict__[sid] = {}
  466. self.__did_name_dict__[sid][data_id] = name
  467. def add_msg_to_auth_whitelist_(self, service_id, data_id):
  468. """
  469. Method to add a specific message to the list, where no authentification is required.
  470. :param service_id: The Service-ID of the message. See class definitions starting with ``SID_``.
  471. :type service_id: int
  472. :param data_id: The Data-ID of the message.
  473. :type data_id: int
  474. """
  475. if service_id not in self.__auth_whitelist__:
  476. self.__auth_whitelist__[service_id] = []
  477. self.__auth_whitelist__[service_id].append(data_id)
  478. self.logger.debug('%s Adding Message (%s) to the authentification whitelist', self.__log_prefix__(), self.__get_message_name__(service_id, data_id))
  479. def add_service(self, req_sid, resp_sid, req_name=None, resp_name=None):
  480. """
  481. Method to add a Service defined by Request- and Response Serivce-ID.
  482. :param req_sid: The Request Service-ID.
  483. :type req_sid: int
  484. :param resp_sid: The Response Service-ID.
  485. :type resp_sid: int
  486. """
  487. if req_sid in self.__sid_response_dict__:
  488. self.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)
  489. raise RequestSidExistsError("Request for this Service is already registered")
  490. elif resp_sid in self.__sid_response_dict__.values():
  491. self.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)
  492. raise ResponseSidExistsError("Response for this Service is already registered")
  493. else:
  494. self.__sid_response_dict__[req_sid] = resp_sid
  495. if req_name is not None:
  496. self.__sid_name_dict__[req_sid] = req_name
  497. if resp_name is not None:
  498. self.__sid_name_dict__[resp_sid] = resp_name
  499. self.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))
  500. def add_status(self, status, name):
  501. """
  502. Method to add a name for a status.
  503. :param status: The Status. See class definitions starting with ``STATUS_``.
  504. :type status: int
  505. :param name: The Name for the Status.
  506. :type name: str
  507. """
  508. self.__status_name_dict[status] = name
  509. def authentificate(self, timeout=2):
  510. """
  511. This method authetificates the client at the server.
  512. :param timeout: The timeout for the authentification (requesting seed, sending key and getting authentification_feedback).
  513. :type timeout: float
  514. :returns: True, if authentification was successfull; False, if not.
  515. :rtype: bool
  516. .. note:: An authentification will only processed, if a secret had been given on initialisation.
  517. .. note:: Client and Server needs to use the same secret.
  518. """
  519. if self.__secret__ is not None:
  520. self.__authentification_state__ = AUTH_STATE_SEED_REQUESTED
  521. self.send(SID_AUTH_REQUEST, DID_AUTH_SEED, None)
  522. cnt = 0
  523. while cnt < timeout * 10:
  524. time.sleep(0.1)
  525. if self.__authentification_state__ == AUTH_STATE_TRUSTED_CONNECTION:
  526. return True
  527. elif self.__authentification_state__ == AUTH_STATE_UNTRUSTED_CONNECTION:
  528. break
  529. cnt += 1
  530. return False
  531. def check_authentification_state(self):
  532. """
  533. This Method return the Authitification State as boolean value.
  534. :return: True, if authentification state is okay, otherwise False
  535. :rtype: bool
  536. """
  537. return self.__secret__ is None or self.__authentification_state__ == AUTH_STATE_TRUSTED_CONNECTION
  538. def connection_established(self):
  539. """
  540. This Method returns the Connection state including authentification as a boolean value.
  541. :return: True, if the connection is established (incl. authentification, if a secret has been given)
  542. :rtype: bool
  543. """
  544. return self.is_connected() and (self.__secret__ is None or self.check_authentification_state())
  545. def is_connected(self):
  546. """
  547. This Methods returns Connection state of the Communication Instance :func:`comm_instance.is_connected`.
  548. :return: True if the :class:`comm_instance` is connected, otherwise False..
  549. :rtype: bool
  550. """
  551. return self.__comm_inst__.is_connected()
  552. def receive(self, service_id, data_id, timeout=1):
  553. """
  554. This Method returns a message object for a defined message or None, if this message is not available after the given timout.
  555. :param service_id: The Service-ID for the message. See class definitions starting with ``SID_``.
  556. :type service_id: int
  557. :param data_id: The Data-ID for the message.
  558. :type data_id: int
  559. :param timeout: The timeout for receiving.
  560. :type timeout: float
  561. :returns: The received data storage object or None, if no data was received.
  562. :rtype: data_storage
  563. """
  564. data = None
  565. cnt = 0
  566. while data is None and cnt < timeout * 10:
  567. try:
  568. data = self.__msg_buffer__.get(service_id, {}).get(data_id, []).pop(0)
  569. except IndexError:
  570. data = None
  571. cnt += 1
  572. time.sleep(0.1)
  573. if data is None and cnt >= timeout * 10:
  574. 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))
  575. return data
  576. def reconnect(self):
  577. """
  578. This methods initiates a reconnect by calling :func:`comm_instance.reconnect`.
  579. """
  580. return self.__comm_inst__.reconnect()
  581. def register_callback(self, service_id, data_id, callback, *args, **kwargs):
  582. """
  583. This method registers a callback for the given parameters. Giving ``None`` means, that all Service-IDs or all Data-IDs are used.
  584. If a message hitting these parameters has been received, the callback will be executed.
  585. :param service_id: The Service-ID for the message. See class definitions starting with ``SID_``.
  586. :type service_id: int
  587. :param data_id: The Data-ID for the message.
  588. :type data_id: int
  589. .. note:: The :func:`callback` is priorised in the following order:
  590. * Callbacks with defined Service-ID and Data-ID.
  591. * Callbacks with a defined Service-ID and all Data-IDs.
  592. * Callbacks with a defined Data-ID and all Service-IDs.
  593. * Unspecific Callbacks.
  594. .. note:: The :func:`callback` is executed with these arguments:
  595. **Parameters given at the callback call:**
  596. * The first Arguments is the received message as :class:`data_storage` object.
  597. * Further arguments given at registration.
  598. * Further keyword arguments given at registration.
  599. **Return value of the callback:**
  600. If the Callback is a Request Callback for a registered Service, the return value has to be a tuple or list with
  601. * :const:`response_status`: The response status (see class definitions starting with :const:`STA_*`.
  602. * :const:`response_data`: A JSON iterable object to be used as data for the response.
  603. .. note:: Only registered services will respond via the callbacks return values with the same data_id.
  604. """
  605. self.__callbacks__.add(service_id, data_id, callback, *args, **kwargs)
  606. def send(self, service_id, data_id, data, status=STATUS_OKAY, timeout=2):
  607. """
  608. This methods sends out a message with the given content.
  609. :param service_id: The Service-ID for the message. See class definitions starting with ``SERVICE_``.
  610. :type service_id: int
  611. :param data_id: The Data-ID for the message.
  612. :type data_id: int
  613. :param data: The data to be transfered. The data needs to be json compatible.
  614. :type data: str
  615. :param status: The Status for the message. All requests should have ``STATUS_OKAY``.
  616. :type status: int
  617. :param timeout: The timeout for sending data (e.g. time to establish new connection).
  618. :type timeout: float
  619. :return: True if data had been sent, otherwise False.
  620. :rtype: bool
  621. """
  622. 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):
  623. msg = data_storage(service_id=service_id, data_id=data_id, data=data, status=status)
  624. self.__log_msg__(msg, 'TX ->')
  625. return self.__comm_inst__.send(self.__build_frame__(msg), timeout=timeout)
  626. else:
  627. # Authentification required
  628. 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))
  629. return False
  630. class struct_json_protocol(pure_json_protocol):
  631. """
  632. 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.
  633. See also parent :py:class:`pure_json_protocol`.
  634. .. note::
  635. This class is depreceated and here for compatibility reasons (to support old clients or servers). Usage of :class:`pure_json_protocol` is recommended.
  636. """
  637. def __init__(self, *args, **kwargs):
  638. pure_json_protocol.__init__(self, *args, **kwargs)
  639. def __analyse_frame__(self, frame):
  640. status, service_id, data_id = struct.unpack('>III', frame[0:12])
  641. if sys.version_info >= (3, 0):
  642. data = json.loads(frame[12:-1].decode('utf-8'))
  643. else:
  644. data = json.loads(frame[12:-1])
  645. return self.__mk_msg__(status, service_id, data_id, data)
  646. def __build_frame__(self, msg):
  647. frame = struct.pack('>III', msg.get_status(), msg.get_service_id(), msg.get_data_id())
  648. if sys.version_info >= (3, 0):
  649. frame += bytes(json.dumps(msg.get_data()), 'utf-8')
  650. frame += self.__calc_chksum__(frame)
  651. else:
  652. frame += json.dumps(msg.get_data())
  653. frame += self.__calc_chksum__(frame)
  654. return frame
  655. def __calc_chksum__(self, raw_data):
  656. chksum = 0
  657. for b in raw_data:
  658. if sys.version_info >= (3, 0):
  659. chksum ^= b
  660. else:
  661. chksum ^= ord(b)
  662. if sys.version_info >= (3, 0):
  663. return bytes([chksum])
  664. else:
  665. return chr(chksum)
  666. def __check_frame_checksum__(self, frame):
  667. return self.__calc_chksum__(frame[:-1]) == frame[-1:]