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.
 
 
 
 

293 lines
15 KiB

  1. """
  2. """
  3. # Created on 2013.07.15
  4. #
  5. # Author: Giovanni Cannata
  6. #
  7. # Copyright 2013 - 2020 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, Event
  25. import socket
  26. from .. import get_config_parameter, DIGEST_MD5
  27. from ..core.exceptions import LDAPSSLConfigurationError, LDAPStartTLSError, LDAPOperationResult, LDAPSignatureVerificationFailedError
  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. from ..protocol.sasl.digestMd5 import md5_hmac
  33. # noinspection PyProtectedMember
  34. class AsyncStrategy(BaseStrategy):
  35. """
  36. This strategy is asynchronous. You send the request and get the messageId of the request sent
  37. Receiving data from socket is managed in a separated thread in a blocking mode
  38. Requests return an int value to indicate the messageId of the requested Operation
  39. You get the response with get_response, it has a timeout to wait for response to appear
  40. Connection.response will contain the whole LDAP response for the messageId requested in a dict form
  41. Connection.request will contain the result LDAP message in a dict form
  42. Response appear in strategy._responses dictionary
  43. """
  44. # noinspection PyProtectedMember
  45. class ReceiverSocketThread(Thread):
  46. """
  47. The thread that actually manage the receiver socket
  48. """
  49. def __init__(self, ldap_connection):
  50. Thread.__init__(self)
  51. self.connection = ldap_connection
  52. self.socket_size = get_config_parameter('SOCKET_SIZE')
  53. def run(self):
  54. """
  55. Waits for data on socket, computes the length of the message and waits for enough bytes to decode the message
  56. Message are appended to strategy._responses
  57. """
  58. unprocessed = b''
  59. get_more_data = True
  60. listen = True
  61. data = b''
  62. sasl_total_bytes_recieved = 0
  63. sasl_received_data = b'' # used to verify the signature, typo GC
  64. sasl_next_packet = b''
  65. sasl_buffer_length = -1
  66. # sasl_signature = b'' # not needed here GC
  67. # sasl_sec_num = b'' # used to verify the signature, not needed here GC
  68. while listen:
  69. if get_more_data:
  70. try:
  71. data = self.connection.socket.recv(self.socket_size)
  72. except (OSError, socket.error, AttributeError):
  73. if self.connection.receive_timeout: # a receive timeout has been detected - keep kistening on the socket
  74. continue
  75. except Exception as e:
  76. if log_enabled(ERROR):
  77. log(ERROR, '<%s> for <%s>', str(e), self.connection)
  78. raise # unexpected exception - re-raise
  79. if len(data) > 0:
  80. # If we are using DIGEST-MD5 and LDAP signing is set : verify & remove the signature from the message
  81. if self.connection.sasl_mechanism == DIGEST_MD5 and self.connection._digest_md5_kis and not self.connection.sasl_in_progress:
  82. data = sasl_next_packet + data
  83. if sasl_received_data == b'' or sasl_next_packet:
  84. # Remove the sizeOf(encoded_message + signature + 0x0001 + secNum) from data.
  85. sasl_buffer_length = int.from_bytes(data[0:4], "big")
  86. data = data[4:]
  87. sasl_next_packet = b''
  88. sasl_total_bytes_recieved += len(data)
  89. sasl_received_data += data
  90. if sasl_total_bytes_recieved >= sasl_buffer_length:
  91. # When the LDAP response is splitted accross multiple TCP packets, the SASL buffer length is equal to the MTU of each packet..Which is usually not equal to self.socket_size
  92. # This means that the end of one SASL packet/beginning of one other....could be located in the middle of data
  93. # We are using "sasl_received_data" instead of "data" & "unprocessed" for this reason
  94. # structure of messages when LDAP signing is enabled : sizeOf(encoded_message + signature + 0x0001 + secNum) + encoded_message + signature + 0x0001 + secNum
  95. sasl_signature = sasl_received_data[sasl_buffer_length - 16:sasl_buffer_length - 6]
  96. sasl_sec_num = sasl_received_data[sasl_buffer_length - 4:sasl_buffer_length]
  97. sasl_next_packet = sasl_received_data[sasl_buffer_length:] # the last "data" variable may contain another sasl packet. We'll process it at the next iteration.
  98. sasl_received_data = sasl_received_data[:sasl_buffer_length - 16] # remove signature + 0x0001 + secNum + the next packet if any, from sasl_received_data
  99. kis = self.connection._digest_md5_kis # renamed to lowercase GC
  100. calculated_signature = bytes.fromhex(md5_hmac(kis, sasl_sec_num + sasl_received_data)[0:20])
  101. if sasl_signature != calculated_signature:
  102. raise LDAPSignatureVerificationFailedError("Signature verification failed for the recieved LDAP message number " + str(int.from_bytes(sasl_sec_num, 'big')) + ". Expected signature " + calculated_signature.hex() + " but got " + sasl_signature.hex() + ".")
  103. sasl_total_bytes_recieved = 0
  104. unprocessed += sasl_received_data
  105. sasl_received_data = b''
  106. else:
  107. unprocessed += data
  108. data = b''
  109. else:
  110. listen = False
  111. length = BaseStrategy.compute_ldap_message_size(unprocessed)
  112. if length == -1 or len(unprocessed) < length:
  113. get_more_data = True
  114. elif len(unprocessed) >= length: # add message to message list
  115. if self.connection.usage:
  116. self.connection._usage.update_received_message(length)
  117. if log_enabled(NETWORK):
  118. log(NETWORK, 'received %d bytes via <%s>', length, self.connection)
  119. if self.connection.fast_decoder:
  120. ldap_resp = decode_message_fast(unprocessed[:length])
  121. dict_response = self.connection.strategy.decode_response_fast(ldap_resp)
  122. else:
  123. ldap_resp = decoder.decode(unprocessed[:length], asn1Spec=LDAPMessage())[0]
  124. dict_response = self.connection.strategy.decode_response(ldap_resp)
  125. message_id = int(ldap_resp['messageID'])
  126. if log_enabled(NETWORK):
  127. log(NETWORK, 'received 1 ldap message via <%s>', self.connection)
  128. if log_enabled(EXTENDED):
  129. log(EXTENDED, 'ldap message received via <%s>:%s', self.connection, format_ldap_message(ldap_resp, '<<'))
  130. 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')):
  131. if dict_response['result'] == 0: # StartTls in progress
  132. if self.connection.server.tls:
  133. self.connection.server.tls._start_tls(self.connection)
  134. else:
  135. self.connection.last_error = 'no Tls object defined in Server'
  136. if log_enabled(ERROR):
  137. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  138. raise LDAPSSLConfigurationError(self.connection.last_error)
  139. else:
  140. self.connection.last_error = 'asynchronous StartTls failed'
  141. if log_enabled(ERROR):
  142. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  143. raise LDAPStartTLSError(self.connection.last_error)
  144. del self.connection._awaiting_for_async_start_tls
  145. if message_id != 0: # 0 is reserved for 'Unsolicited Notification' from server as per RFC4511 (paragraph 4.4)
  146. with self.connection.strategy.async_lock:
  147. if message_id in self.connection.strategy._responses:
  148. self.connection.strategy._responses[message_id].append(dict_response)
  149. else:
  150. self.connection.strategy._responses[message_id] = [dict_response]
  151. if dict_response['type'] not in ['searchResEntry', 'searchResRef', 'intermediateResponse']:
  152. self.connection.strategy._responses[message_id].append(RESPONSE_COMPLETE)
  153. self.connection.strategy.set_event_for_message(message_id)
  154. if self.connection.strategy.can_stream: # for AsyncStreamStrategy, used for PersistentSearch
  155. self.connection.strategy.accumulate_stream(message_id, dict_response)
  156. unprocessed = unprocessed[length:]
  157. get_more_data = False if unprocessed else True
  158. listen = True if self.connection.listening or unprocessed else False
  159. else: # Unsolicited Notification
  160. if dict_response['responseName'] == '1.3.6.1.4.1.1466.20036': # Notice of Disconnection as per RFC4511 (paragraph 4.4.1)
  161. listen = False
  162. else:
  163. self.connection.last_error = 'unknown unsolicited notification from server'
  164. if log_enabled(ERROR):
  165. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  166. raise LDAPStartTLSError(self.connection.last_error)
  167. self.connection.strategy.close()
  168. def __init__(self, ldap_connection):
  169. BaseStrategy.__init__(self, ldap_connection)
  170. self.sync = False
  171. self.no_real_dsa = False
  172. self.pooled = False
  173. self._responses = None
  174. self._requests = None
  175. self.can_stream = False
  176. self.receiver = None
  177. self.async_lock = Lock()
  178. self.event_lock = Lock()
  179. self._events = {}
  180. def open(self, reset_usage=True, read_server_info=True):
  181. """
  182. Open connection and start listen on the socket in a different thread
  183. """
  184. with self.connection.connection_lock:
  185. self._responses = dict()
  186. self._requests = dict()
  187. BaseStrategy.open(self, reset_usage, read_server_info)
  188. if read_server_info:
  189. try:
  190. self.connection.refresh_server_info()
  191. except LDAPOperationResult: # catch errors from server if raise_exception = True
  192. self.connection.server._dsa_info = None
  193. self.connection.server._schema_info = None
  194. def close(self):
  195. """
  196. Close connection and stop socket thread
  197. """
  198. with self.connection.connection_lock:
  199. BaseStrategy.close(self)
  200. def _add_event_for_message(self, message_id):
  201. with self.event_lock:
  202. # Should have the check here because the receiver thread may has created it
  203. if message_id not in self._events:
  204. self._events[message_id] = Event()
  205. def set_event_for_message(self, message_id):
  206. with self.event_lock:
  207. # The receiver thread may receive the response before the sender set the event for the message_id,
  208. # so we have to check if the event exists
  209. if message_id not in self._events:
  210. self._events[message_id] = Event()
  211. self._events[message_id].set()
  212. def _get_event_for_message(self, message_id):
  213. with self.event_lock:
  214. if message_id not in self._events:
  215. raise RuntimeError('Event for message[{}] should have been created before accessing'.format(message_id))
  216. return self._events[message_id]
  217. def post_send_search(self, message_id):
  218. """
  219. Clears connection.response and returns messageId
  220. """
  221. self.connection.response = None
  222. self.connection.request = None
  223. self.connection.result = None
  224. self._add_event_for_message(message_id)
  225. return message_id
  226. def post_send_single_response(self, message_id):
  227. """
  228. Clears connection.response and returns messageId.
  229. """
  230. self.connection.response = None
  231. self.connection.request = None
  232. self.connection.result = None
  233. self._add_event_for_message(message_id)
  234. return message_id
  235. def _start_listen(self):
  236. """
  237. Start thread in daemon mode
  238. """
  239. if not self.connection.listening:
  240. self.receiver = AsyncStrategy.ReceiverSocketThread(self.connection)
  241. self.connection.listening = True
  242. self.receiver.daemon = True
  243. self.receiver.start()
  244. def _get_response(self, message_id, timeout):
  245. """
  246. Performs the capture of LDAP response for this strategy
  247. The response is only complete after the event been set
  248. """
  249. event = self._get_event_for_message(message_id)
  250. flag = event.wait(timeout)
  251. if not flag:
  252. # timeout
  253. return None
  254. # In this stage we could ensure the response is already there
  255. self._events.pop(message_id)
  256. with self.async_lock:
  257. return self._responses.pop(message_id)
  258. def receiving(self):
  259. raise NotImplementedError
  260. def get_stream(self):
  261. raise NotImplementedError
  262. def set_stream(self, value):
  263. raise NotImplementedError