Python Library Socket Protocol
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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.struct_json_protocol`
  13. * :class:`socket_protocol.pure_json_protocol`
  14. **Unittest:**
  15. See also the :download:`unittest <../../socket_protocol/_testresults_/unittest.pdf>` documentation.
  16. """
  17. __DEPENDENCIES__ = ['stringtools']
  18. import stringtools
  19. import binascii
  20. import hashlib
  21. import json
  22. import logging
  23. import os
  24. import struct
  25. import sys
  26. import time
  27. logger_name = 'SOCKET_PROTOCOL'
  28. logger = logging.getLogger(logger_name)
  29. __DESCRIPTION__ = """The Module {\\tt %s} is designed to pack and unpack data for serial transportation.
  30. For more Information read the sphinx documentation.""" % __name__.replace('_', '\_')
  31. """The Module Description"""
  32. __INTERPRETER__ = (2, 3)
  33. """The Tested Interpreter-Versions"""
  34. class RegistrationError(BaseException):
  35. pass
  36. class callback_storage(dict):
  37. def __init__(self):
  38. dict.__init__(self)
  39. def get(self, service_id, data_id):
  40. if service_id is not None and data_id is not None:
  41. try:
  42. return self[service_id][data_id]
  43. except KeyError:
  44. pass # nothing to append
  45. if data_id is not None:
  46. try:
  47. return self[None][data_id]
  48. except KeyError:
  49. pass # nothing to append
  50. if service_id is not None:
  51. try:
  52. return self[service_id][None]
  53. except KeyError:
  54. pass # nothing to append
  55. try:
  56. return self[None][None]
  57. except KeyError:
  58. pass # nothing to append
  59. return None
  60. def add(self, service_id, data_id, callback):
  61. if self.get(service_id, data_id) is not None:
  62. raise RegistrationError("Callback for service_id (%s) and data_id (%s) already exists" % (repr(service_id), repr(data_id)))
  63. if service_id not in self:
  64. self[service_id] = {}
  65. self[service_id][data_id] = callback
  66. class data_storage(dict):
  67. KEY_STATUS = 'status'
  68. KEY_SERVICE_ID = 'service_id'
  69. KEY_DATA_ID = 'data_id'
  70. KEY_DATA = 'data'
  71. def __init__(self, *args, **kwargs):
  72. dict.__init__(self, *args, **kwargs)
  73. def get_status(self, default=None):
  74. return self.get(self.KEY_STATUS, default)
  75. def get_service_id(self, default=None):
  76. return self.get(self.KEY_SERVICE_ID, default)
  77. def get_data_id(self, default=None):
  78. return self.get(self.KEY_DATA_ID, default)
  79. def get_data(self, default=None):
  80. return self.get(self.KEY_DATA, default)
  81. class struct_json_protocol(object):
  82. """
  83. :param comm_instance: a communication instance supportin at least these functions: :func:`register_callback`, :func:`register_disconnect_callback`, :func:`send`.
  84. :type comm_instance: instance
  85. :param secret: A secret (e.g. created by ``binascii.hexlify(os.urandom(24))``).
  86. :type secret: str
  87. This communication protocol supports to transfer a Service-ID, Data-ID and Data. The transmitted data is shorter than :class:`pure_json_protocol`.
  88. .. note::
  89. This class is here for compatibility reasons. Usage of :class:`pure_json_protocol` is recommended.
  90. **Example:**
  91. Server:
  92. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__struct_json_protocol_server.py
  93. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__struct_json_protocol_server.log
  94. Client:
  95. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__struct_json_protocol_client.py
  96. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__struct_json_protocol_client.log
  97. """
  98. LOG_PREFIX = 'SJP:'
  99. SID_AUTH_SEED_REQUEST = 1
  100. SID_AUTH_KEY_REQUEST = 2
  101. SID_AUTH_KEY_CHECK_REQUEST = 3
  102. SID_AUTH_KEY_CHECK_RESPONSE = 4
  103. SID_READ_REQUEST = 10
  104. SID_READ_RESPONSE = 11
  105. SID_WRITE_REQUEST = 20
  106. SID_WRITE_RESPONSE = 21
  107. SID_EXECUTE_REQUEST = 30
  108. SID_EXECUTE_RESPONSE = 31
  109. SID_RESPONSE_DICT = {SID_AUTH_SEED_REQUEST: SID_AUTH_KEY_REQUEST,
  110. SID_AUTH_KEY_REQUEST: SID_AUTH_KEY_CHECK_REQUEST,
  111. SID_AUTH_KEY_CHECK_REQUEST: SID_AUTH_KEY_CHECK_RESPONSE,
  112. SID_READ_REQUEST: SID_READ_RESPONSE,
  113. SID_WRITE_REQUEST: SID_WRITE_RESPONSE,
  114. SID_EXECUTE_REQUEST: SID_EXECUTE_RESPONSE}
  115. SID_AUTH_LIST = [SID_AUTH_SEED_REQUEST, SID_AUTH_KEY_REQUEST, SID_AUTH_KEY_CHECK_REQUEST, SID_AUTH_KEY_CHECK_RESPONSE]
  116. STATUS_OKAY = 0
  117. STATUS_BUFFERING_UNHANDLED_REQUEST = 1
  118. STATUS_AUTH_REQUIRED = 2
  119. STATUS_SERVICE_OR_DATA_UNKNOWN = 3
  120. STATUS_CHECKSUM_ERROR = 4
  121. STATUS_OPERATION_NOT_PERMITTED = 5
  122. STATUS_NAMES = {STATUS_OKAY: 'Okay',
  123. STATUS_BUFFERING_UNHANDLED_REQUEST: 'Request has no callback. Data buffered.',
  124. STATUS_AUTH_REQUIRED: 'Authentification required',
  125. STATUS_SERVICE_OR_DATA_UNKNOWN: 'Service or Data unknown',
  126. STATUS_CHECKSUM_ERROR: 'Checksum Error',
  127. STATUS_OPERATION_NOT_PERMITTED: 'Operation not permitted'}
  128. AUTH_STATE_UNKNOWN_CLIENT = 0
  129. AUTH_STATE_SEED_REQUESTED = 1
  130. AUTH_STATE_SEED_TRANSFERRED = 2
  131. AUTH_STATE_KEY_TRANSFERRED = 3
  132. AUTH_STATE_TRUSTED_CLIENT = 4
  133. AUTH_STATUS_NAMES = {AUTH_STATE_UNKNOWN_CLIENT: 'Unknown Client',
  134. AUTH_STATE_SEED_REQUESTED: 'Seed was requested',
  135. AUTH_STATE_SEED_TRANSFERRED: 'Seed has been sent',
  136. AUTH_STATE_KEY_TRANSFERRED: 'Key has been sent',
  137. AUTH_STATE_TRUSTED_CLIENT: 'Trusted Client'}
  138. def __init__(self, comm_instance, secret=None):
  139. self.__secret__ = secret
  140. self.__clean_receive_buffer__()
  141. self.__callbacks__ = callback_storage()
  142. self.__callbacks__.add(self.SID_AUTH_SEED_REQUEST, 0, self.__authentificate_create_seed__)
  143. self.__callbacks__.add(self.SID_AUTH_KEY_REQUEST, 0, self.__authentificate_create_key__)
  144. self.__callbacks__.add(self.SID_AUTH_KEY_CHECK_REQUEST, 0, self.__authentificate_check_key__)
  145. self.__callbacks__.add(self.SID_AUTH_KEY_CHECK_RESPONSE, 0, self.__authentificate_process_feedback__)
  146. self.__authentification_state_reset__()
  147. self.__seed__ = None
  148. self.__comm_inst__ = comm_instance
  149. self.__comm_inst__.register_callback(self.__data_available_callback__)
  150. self.__comm_inst__.register_connect_callback(self.__clean_receive_buffer__)
  151. self.__comm_inst__.register_disconnect_callback(self.__authentification_state_reset__)
  152. def __authentification_state_reset__(self):
  153. logger.info("%s Resetting authentification state to AUTH_STATE_UNKNOWN_CLIENT", self.LOG_PREFIX)
  154. self.__authentification_state__ = self.AUTH_STATE_UNKNOWN_CLIENT
  155. def __analyse_frame__(self, frame):
  156. status, service_id, data_id = struct.unpack('>III', frame[0:12])
  157. if sys.version_info >= (3, 0):
  158. data = json.loads(frame[12:-1].decode('utf-8'))
  159. else:
  160. data = json.loads(frame[12:-1])
  161. return self.__mk_msg__(status, service_id, data_id, data)
  162. def __build_frame__(self, service_id, data_id, data, status=STATUS_OKAY):
  163. frame = struct.pack('>III', status, service_id, data_id)
  164. if sys.version_info >= (3, 0):
  165. frame += bytes(json.dumps(data), 'utf-8')
  166. frame += self.__calc_chksum__(frame)
  167. else:
  168. frame += json.dumps(data)
  169. frame += self.__calc_chksum__(frame)
  170. return frame
  171. def __calc_chksum__(self, raw_data):
  172. chksum = 0
  173. for b in raw_data:
  174. if sys.version_info >= (3, 0):
  175. chksum ^= b
  176. else:
  177. chksum ^= ord(b)
  178. if sys.version_info >= (3, 0):
  179. return bytes([chksum])
  180. else:
  181. return chr(chksum)
  182. def __check_frame_checksum__(self, frame):
  183. return self.__calc_chksum__(frame[:-1]) == frame[-1:]
  184. def __data_available_callback__(self, comm_inst):
  185. frame = comm_inst.receive()
  186. if not self.__check_frame_checksum__(frame):
  187. logger.warning("%s Received message has a wrong checksum and will be ignored: %s.", self.LOG_PREFIX, stringtools.hexlify(frame))
  188. else:
  189. msg = self.__analyse_frame__(frame)
  190. logger.info(
  191. '%s RX <- status: %s, service_id: %s, data_id: %s, data: "%s"',
  192. self.LOG_PREFIX,
  193. repr(msg.get_status()),
  194. repr(msg.get_service_id()),
  195. repr(msg.get_data_id()),
  196. repr(msg.get_data())
  197. )
  198. callback = self.__callbacks__.get(msg.get_service_id(), msg.get_data_id())
  199. if msg.get_service_id() in self.SID_RESPONSE_DICT.keys():
  200. #
  201. # REQUEST RECEIVED
  202. #
  203. if self.__secret__ is not None and not self.check_authentification_state() and msg.get_service_id() not in self.SID_AUTH_LIST:
  204. status = self.STATUS_AUTH_REQUIRED
  205. data = None
  206. logger.warning("%s Received message needs authentification: %s. Sending negative response.", self.LOG_PREFIX, self.AUTH_STATUS_NAMES.get(self.__authentification_state__, 'Unknown authentification status!'))
  207. elif callback is None:
  208. logger.warning("%s Received message with no registered callback. Sending negative response.", self.LOG_PREFIX)
  209. status = self.STATUS_BUFFERING_UNHANDLED_REQUEST
  210. data = None
  211. else:
  212. try:
  213. logger.debug("%s Executing callback %s to process received data", self.LOG_PREFIX, callback.__name__)
  214. status, data = callback(msg)
  215. except TypeError:
  216. 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())))
  217. self.send(self.SID_RESPONSE_DICT[msg.get_service_id()], msg.get_data_id(), data, status=status)
  218. else:
  219. #
  220. # RESPONSE RECEIVED
  221. #
  222. if msg.get_status() not in [self.STATUS_OKAY]:
  223. logger.warning("%s Received message has a peculiar status: %s", self.LOG_PREFIX, self.STATUS_NAMES.get(msg.get_status(), 'Unknown status response!'))
  224. if callback is None:
  225. status = self.STATUS_OKAY
  226. data = None
  227. self.__buffer_received_data__(msg)
  228. else:
  229. try:
  230. logger.debug("%s Executing callback %s to process received data", self.LOG_PREFIX, callback.__name__)
  231. status, data = callback(msg)
  232. except TypeError:
  233. 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())))
  234. def __buffer_received_data__(self, msg):
  235. if not msg.get_service_id() in self.__msg_buffer__:
  236. self.__msg_buffer__[msg.get_service_id()] = {}
  237. if not msg.get_data_id() in self.__msg_buffer__[msg.get_service_id()]:
  238. self.__msg_buffer__[msg.get_service_id()][msg.get_data_id()] = []
  239. self.__msg_buffer__[msg.get_service_id()][msg.get_data_id()].append(msg)
  240. logger.debug("%s Message data is stored in buffer and is now ready to be retrieved by receive method", self.LOG_PREFIX)
  241. def __clean_receive_buffer__(self):
  242. logger.debug("%s Cleaning up receive-buffer", self.LOG_PREFIX)
  243. self.__msg_buffer__ = {}
  244. def receive(self, service_id, data_id, timeout=1):
  245. data = None
  246. cnt = 0
  247. while data is None and cnt < timeout * 10:
  248. try:
  249. data = self.__msg_buffer__.get(service_id, {}).get(data_id, []).pop(0)
  250. except IndexError:
  251. data = None
  252. cnt += 1
  253. time.sleep(0.1)
  254. if data is None and cnt >= timeout * 10:
  255. 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))
  256. return data
  257. def __mk_msg__(self, status, service_id, data_id, data):
  258. 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})
  259. def send(self, service_id, data_id, data, status=STATUS_OKAY, timeout=2, log_lvl=logging.INFO):
  260. """
  261. :param service_id: The Service-ID for the message. See class definitions starting with ``SERVICE_``.
  262. :type service_id: int
  263. :param data_id: The Data-ID for the message.
  264. :type data_id: int
  265. :param data: The data to be transfered. The data needs to be json compatible.
  266. :type data: str
  267. :param status: The Status for the message. All requests should have ``STATUS_OKAY``.
  268. :type status: int
  269. :param timeout: The timeout for sending data (e.g. time to establish new connection).
  270. :type timeout: float
  271. :param rx_log_lvl: The log level to log outgoing TX-data
  272. :type rx_log_lvl: int
  273. :return: True if data had been sent, otherwise False.
  274. :rtype: bool
  275. This methods sends out a message with the given content.
  276. """
  277. 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))
  278. return self.__comm_inst__.send(self.__build_frame__(service_id, data_id, data, status), timeout=timeout, log_lvl=logging.DEBUG)
  279. def register_callback(self, service_id, data_id, callback):
  280. """
  281. :param service_id: The Service-ID for the message. See class definitions starting with ``SID_``.
  282. :type service_id: int
  283. :param data_id: The Data-ID for the message.
  284. :type data_id: int
  285. :returns: True, if registration was successfull; False, if registration failed (e.g. existance of a callback for this configuration)
  286. :rtype: bool
  287. This method registers a callback for the given parameters. Givin ``None`` means, that all Service-IDs or all Data-IDs are used.
  288. If a message hitting these parameters has been received, the callback will be executed.
  289. .. note:: The :func:`callback` is priorised in the following order:
  290. * Callbacks with defined Service-ID and Data-ID.
  291. * Callbacks with a defined Data-ID.
  292. * Callbacks with a defined Service-ID.
  293. * Unspecific Callbacks
  294. .. note:: The :func:`callback` is executed with these arguments:
  295. :param msg: A :class:`dict` containing all message information.
  296. :returns: status (see class definition starting with ``STATUS_``), response_data (JSON compatible object)
  297. """
  298. self.__callbacks__.add(service_id, data_id, callback)
  299. def authentificate(self, timeout=2):
  300. """
  301. :param timeout: The timeout for the authentification (requesting seed, sending key and getting authentification_feedback).
  302. :type timeout: float
  303. :returns: True, if authentification was successfull; False, if not.
  304. :rtype: bool
  305. This method authetificates the client at the server.
  306. .. note:: An authentification will only processed, if a secret had been given on initialisation.
  307. .. note:: Client and Server needs to use the same secret.
  308. """
  309. if self.__secret__ is not None:
  310. self.__authentification_state__ = self.AUTH_STATE_SEED_REQUESTED
  311. logger.info("%s Requesting seed for authentification", self.LOG_PREFIX)
  312. self.send(self.SID_AUTH_SEED_REQUEST, 0, None)
  313. cnt = 0
  314. while cnt < timeout * 10:
  315. time.sleep(0.1)
  316. if self.__authentification_state__ == self.AUTH_STATE_TRUSTED_CLIENT:
  317. return True
  318. elif self.__authentification_state__ == self.AUTH_STATE_UNKNOWN_CLIENT:
  319. break
  320. cnt += 1
  321. return False
  322. def check_authentification_state(self):
  323. """
  324. :return: True, if authentification state is okay, otherwise False
  325. :rtype: bool
  326. """
  327. return self.__authentification_state__ == self.AUTH_STATE_TRUSTED_CLIENT
  328. def __authentificate_salt_and_hash__(self, seed):
  329. if sys.version_info >= (3, 0):
  330. return hashlib.sha512(bytes(seed, 'utf-8') + self.__secret__).hexdigest()
  331. else:
  332. return hashlib.sha512(seed.encode('utf-8') + self.__secret__.encode('utf-8')).hexdigest()
  333. def __authentificate_create_seed__(self, msg):
  334. logger.info("%s Got seed request, sending seed for authentification", self.LOG_PREFIX)
  335. self.__authentification_state__ = self.AUTH_STATE_SEED_TRANSFERRED
  336. if sys.version_info >= (3, 0):
  337. self.__seed__ = binascii.hexlify(os.urandom(32)).decode('utf-8')
  338. else:
  339. self.__seed__ = binascii.hexlify(os.urandom(32))
  340. return self.STATUS_OKAY, self.__seed__
  341. def __authentificate_create_key__(self, msg):
  342. logger.info("%s Got seed, sending key for authentification", self.LOG_PREFIX)
  343. self.__authentification_state__ = self.AUTH_STATE_KEY_TRANSFERRED
  344. seed = msg.get_data()
  345. key = self.__authentificate_salt_and_hash__(seed)
  346. return self.STATUS_OKAY, key
  347. def __authentificate_check_key__(self, msg):
  348. key = msg.get_data()
  349. if key == self.__authentificate_salt_and_hash__(self.__seed__):
  350. self.__authentification_state__ = self.AUTH_STATE_TRUSTED_CLIENT
  351. logger.info("%s Got correct key, sending positive authentification feedback", self.LOG_PREFIX)
  352. return self.STATUS_OKAY, True
  353. else:
  354. self.__authentification_state__ = self.AUTH_STATE_UNKNOWN_CLIENT
  355. logger.info("%s Got incorrect key, sending negative authentification feedback", self.LOG_PREFIX)
  356. return self.STATUS_OKAY, False
  357. def __authentificate_process_feedback__(self, msg):
  358. feedback = msg.get_data()
  359. if feedback:
  360. self.__authentification_state__ = self.AUTH_STATE_TRUSTED_CLIENT
  361. logger.info("%s Got positive authentification feedback", self.LOG_PREFIX)
  362. else:
  363. self.__authentification_state__ = self.AUTH_STATE_UNKNOWN_CLIENT
  364. logger.warning("%s Got negative authentification feedback", self.LOG_PREFIX)
  365. return self.STATUS_OKAY, None
  366. class pure_json_protocol(struct_json_protocol):
  367. """
  368. :param comm_instance: a communication instance supportin at least these functions: :func:`register_callback`, :func:`register_disconnect_callback`, :func:`send`.
  369. :type comm_instance: instance
  370. :param secret: A secret (e.g. created by ``binascii.hexlify(os.urandom(24))``).
  371. :type secret: str
  372. This communication protocol supports to transfer a Service-ID, Data-ID and Data.
  373. **Example:**
  374. Server:
  375. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__pure_json_protocol_server.py
  376. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__pure_json_protocol_server.log
  377. Client:
  378. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__pure_json_protocol_client.py
  379. .. literalinclude:: ../../socket_protocol/_examples_/socket_protocol__pure_json_protocol_client.log
  380. """
  381. def __init__(self, comm_instance, secret=None):
  382. struct_json_protocol.__init__(self, comm_instance, secret)
  383. def __build_frame__(self, service_id, data_id, data, status=struct_json_protocol.STATUS_OKAY):
  384. data_frame = json.dumps(self.__mk_msg__(status, service_id, data_id, data))
  385. if sys.version_info >= (3, 0):
  386. data_frame = bytes(data_frame, 'utf-8')
  387. checksum = self.__calc_chksum__(data_frame)
  388. return data_frame + checksum
  389. def __analyse_frame__(self, frame):
  390. if sys.version_info >= (3, 0):
  391. return data_storage(json.loads(frame[:-4].decode('utf-8')))
  392. else:
  393. return data_storage(json.loads(frame[:-4]))
  394. def __calc_chksum__(self, raw_data):
  395. return struct.pack('>I', binascii.crc32(raw_data) & 0xffffffff)
  396. def __check_frame_checksum__(self, frame):
  397. return self.__calc_chksum__(frame[:-4]) == frame[-4:]