Development of an internal social media platform with personalised dashboards for students
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.

asynchronous.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. """
  2. """
  3. # Created on 2013.07.15
  4. #
  5. # Author: Giovanni Cannata
  6. #
  7. # Copyright 2013 - 2018 Giovanni Cannata
  8. #
  9. # This file is part of ldap3.
  10. #
  11. # ldap3 is free software: you can redistribute it and/or modify
  12. # it under the terms of the GNU Lesser General Public License as published
  13. # by the Free Software Foundation, either version 3 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # ldap3 is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU Lesser General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU Lesser General Public License
  22. # along with ldap3 in the COPYING and COPYING.LESSER files.
  23. # If not, see <http://www.gnu.org/licenses/>.
  24. from threading import Thread, Lock
  25. import socket
  26. from .. import get_config_parameter
  27. from ..core.exceptions import LDAPSSLConfigurationError, LDAPStartTLSError, LDAPOperationResult
  28. from ..strategy.base import BaseStrategy, RESPONSE_COMPLETE
  29. from ..protocol.rfc4511 import LDAPMessage
  30. from ..utils.log import log, log_enabled, format_ldap_message, ERROR, NETWORK, EXTENDED
  31. from ..utils.asn1 import decoder, decode_message_fast
  32. # noinspection PyProtectedMember
  33. class AsyncStrategy(BaseStrategy):
  34. """
  35. This strategy is asynchronous. You send the request and get the messageId of the request sent
  36. Receiving data from socket is managed in a separated thread in a blocking mode
  37. Requests return an int value to indicate the messageId of the requested Operation
  38. You get the response with get_response, it has a timeout to wait for response to appear
  39. Connection.response will contain the whole LDAP response for the messageId requested in a dict form
  40. Connection.request will contain the result LDAP message in a dict form
  41. Response appear in strategy._responses dictionary
  42. """
  43. # noinspection PyProtectedMember
  44. class ReceiverSocketThread(Thread):
  45. """
  46. The thread that actually manage the receiver socket
  47. """
  48. def __init__(self, ldap_connection):
  49. Thread.__init__(self)
  50. self.connection = ldap_connection
  51. self.socket_size = get_config_parameter('SOCKET_SIZE')
  52. def run(self):
  53. """
  54. Wait for data on socket, compute the length of the message and wait for enough bytes to decode the message
  55. Message are appended to strategy._responses
  56. """
  57. unprocessed = b''
  58. get_more_data = True
  59. listen = True
  60. data = b''
  61. while listen:
  62. if get_more_data:
  63. try:
  64. data = self.connection.socket.recv(self.socket_size)
  65. except (OSError, socket.error, AttributeError):
  66. if self.connection.receive_timeout: # a receive timeout has been detected - keep kistening on the socket
  67. continue
  68. except Exception as e:
  69. if log_enabled(ERROR):
  70. log(ERROR, '<%s> for <%s>', str(e), self.connection)
  71. raise # unexpected exception - re-raise
  72. if len(data) > 0:
  73. unprocessed += data
  74. data = b''
  75. else:
  76. listen = False
  77. length = BaseStrategy.compute_ldap_message_size(unprocessed)
  78. if length == -1 or len(unprocessed) < length:
  79. get_more_data = True
  80. elif len(unprocessed) >= length: # add message to message list
  81. if self.connection.usage:
  82. self.connection._usage.update_received_message(length)
  83. if log_enabled(NETWORK):
  84. log(NETWORK, 'received %d bytes via <%s>', length, self.connection)
  85. if self.connection.fast_decoder:
  86. ldap_resp = decode_message_fast(unprocessed[:length])
  87. dict_response = self.connection.strategy.decode_response_fast(ldap_resp)
  88. else:
  89. ldap_resp = decoder.decode(unprocessed[:length], asn1Spec=LDAPMessage())[0]
  90. dict_response = self.connection.strategy.decode_response(ldap_resp)
  91. message_id = int(ldap_resp['messageID'])
  92. if log_enabled(NETWORK):
  93. log(NETWORK, 'received 1 ldap message via <%s>', self.connection)
  94. if log_enabled(EXTENDED):
  95. log(EXTENDED, 'ldap message received via <%s>:%s', self.connection, format_ldap_message(ldap_resp, '<<'))
  96. if dict_response['type'] == 'extendedResp' and (dict_response['responseName'] == '1.3.6.1.4.1.1466.20037' or hasattr(self.connection, '_awaiting_for_async_start_tls')):
  97. if dict_response['result'] == 0: # StartTls in progress
  98. if self.connection.server.tls:
  99. self.connection.server.tls._start_tls(self.connection)
  100. else:
  101. self.connection.last_error = 'no Tls object defined in Server'
  102. if log_enabled(ERROR):
  103. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  104. raise LDAPSSLConfigurationError(self.connection.last_error)
  105. else:
  106. self.connection.last_error = 'asynchronous StartTls failed'
  107. if log_enabled(ERROR):
  108. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  109. raise LDAPStartTLSError(self.connection.last_error)
  110. del self.connection._awaiting_for_async_start_tls
  111. if message_id != 0: # 0 is reserved for 'Unsolicited Notification' from server as per RFC4511 (paragraph 4.4)
  112. with self.connection.strategy.async_lock:
  113. if message_id in self.connection.strategy._responses:
  114. self.connection.strategy._responses[message_id].append(dict_response)
  115. else:
  116. self.connection.strategy._responses[message_id] = [dict_response]
  117. if dict_response['type'] not in ['searchResEntry', 'searchResRef', 'intermediateResponse']:
  118. self.connection.strategy._responses[message_id].append(RESPONSE_COMPLETE)
  119. if self.connection.strategy.can_stream: # for AsyncStreamStrategy, used for PersistentSearch
  120. self.connection.strategy.accumulate_stream(message_id, dict_response)
  121. unprocessed = unprocessed[length:]
  122. get_more_data = False if unprocessed else True
  123. listen = True if self.connection.listening or unprocessed else False
  124. else: # Unsolicited Notification
  125. if dict_response['responseName'] == '1.3.6.1.4.1.1466.20036': # Notice of Disconnection as per RFC4511 (paragraph 4.4.1)
  126. listen = False
  127. else:
  128. self.connection.last_error = 'unknown unsolicited notification from server'
  129. if log_enabled(ERROR):
  130. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  131. raise LDAPStartTLSError(self.connection.last_error)
  132. self.connection.strategy.close()
  133. def __init__(self, ldap_connection):
  134. BaseStrategy.__init__(self, ldap_connection)
  135. self.sync = False
  136. self.no_real_dsa = False
  137. self.pooled = False
  138. self._responses = None
  139. self._requests = None
  140. self.can_stream = False
  141. self.receiver = None
  142. self.async_lock = Lock()
  143. def open(self, reset_usage=True, read_server_info=True):
  144. """
  145. Open connection and start listen on the socket in a different thread
  146. """
  147. with self.connection.connection_lock:
  148. self._responses = dict()
  149. self._requests = dict()
  150. BaseStrategy.open(self, reset_usage, read_server_info)
  151. if read_server_info:
  152. try:
  153. self.connection.refresh_server_info()
  154. except LDAPOperationResult: # catch errors from server if raise_exception = True
  155. self.connection.server._dsa_info = None
  156. self.connection.server._schema_info = None
  157. def close(self):
  158. """
  159. Close connection and stop socket thread
  160. """
  161. with self.connection.connection_lock:
  162. BaseStrategy.close(self)
  163. def post_send_search(self, message_id):
  164. """
  165. Clears connection.response and returns messageId
  166. """
  167. self.connection.response = None
  168. self.connection.request = None
  169. self.connection.result = None
  170. return message_id
  171. def post_send_single_response(self, message_id):
  172. """
  173. Clears connection.response and returns messageId.
  174. """
  175. self.connection.response = None
  176. self.connection.request = None
  177. self.connection.result = None
  178. return message_id
  179. def _start_listen(self):
  180. """
  181. Start thread in daemon mode
  182. """
  183. if not self.connection.listening:
  184. self.receiver = AsyncStrategy.ReceiverSocketThread(self.connection)
  185. self.connection.listening = True
  186. self.receiver.daemon = True
  187. self.receiver.start()
  188. def _get_response(self, message_id):
  189. """
  190. Performs the capture of LDAP response for this strategy
  191. Checks lock to avoid race condition with receiver thread
  192. """
  193. with self.async_lock:
  194. responses = self._responses.pop(message_id) if message_id in self._responses and self._responses[message_id][-1] == RESPONSE_COMPLETE else None
  195. return responses
  196. def receiving(self):
  197. raise NotImplementedError
  198. def get_stream(self):
  199. raise NotImplementedError
  200. def set_stream(self, value):
  201. raise NotImplementedError