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.
 
 
 
 

252 line
14 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. import socket
  25. from .. import SEQUENCE_TYPES, get_config_parameter, DIGEST_MD5
  26. from ..core.exceptions import LDAPSocketReceiveError, communication_exception_factory, LDAPExceptionError, LDAPExtensionError, LDAPOperationResult, LDAPSignatureVerificationFailedError
  27. from ..strategy.base import BaseStrategy, SESSION_TERMINATED_BY_SERVER, RESPONSE_COMPLETE, TRANSACTION_ERROR
  28. from ..protocol.rfc4511 import LDAPMessage
  29. from ..utils.log import log, log_enabled, ERROR, NETWORK, EXTENDED, format_ldap_message
  30. from ..utils.asn1 import decoder, decode_message_fast
  31. from ..protocol.sasl.digestMd5 import md5_hmac
  32. LDAP_MESSAGE_TEMPLATE = LDAPMessage()
  33. # noinspection PyProtectedMember
  34. class SyncStrategy(BaseStrategy):
  35. """
  36. This strategy is synchronous. You send the request and get the response
  37. Requests return a boolean value to indicate the result of the requested Operation
  38. Connection.response will contain the whole LDAP response for the messageId requested in a dict form
  39. Connection.request will contain the result LDAP message in a dict form
  40. """
  41. def __init__(self, ldap_connection):
  42. BaseStrategy.__init__(self, ldap_connection)
  43. self.sync = True
  44. self.no_real_dsa = False
  45. self.pooled = False
  46. self.can_stream = False
  47. self.socket_size = get_config_parameter('SOCKET_SIZE')
  48. def open(self, reset_usage=True, read_server_info=True):
  49. BaseStrategy.open(self, reset_usage, read_server_info)
  50. if read_server_info and not self.connection._deferred_open:
  51. try:
  52. self.connection.refresh_server_info()
  53. except LDAPOperationResult: # catch errors from server if raise_exception = True
  54. self.connection.server._dsa_info = None
  55. self.connection.server._schema_info = None
  56. def _start_listen(self):
  57. if not self.connection.listening and not self.connection.closed:
  58. self.connection.listening = True
  59. def receiving(self):
  60. """
  61. Receives data over the socket
  62. Checks if the socket is closed
  63. """
  64. messages = []
  65. receiving = True
  66. unprocessed = b''
  67. data = b''
  68. get_more_data = True
  69. # exc = None # not needed here GC
  70. sasl_total_bytes_recieved = 0
  71. sasl_received_data = b'' # used to verify the signature
  72. sasl_next_packet = b''
  73. # sasl_signature = b'' # not needed here? GC
  74. # sasl_sec_num = b'' # used to verify the signature # not needed here, reformatted to lowercase GC
  75. sasl_buffer_length = -1 # added, not initialized? GC
  76. while receiving:
  77. if get_more_data:
  78. try:
  79. data = self.connection.socket.recv(self.socket_size)
  80. except (OSError, socket.error, AttributeError) as e:
  81. self.connection.last_error = 'error receiving data: ' + str(e)
  82. try: # try to close the connection before raising exception
  83. self.close()
  84. except (socket.error, LDAPExceptionError):
  85. pass
  86. if log_enabled(ERROR):
  87. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  88. # raise communication_exception_factory(LDAPSocketReceiveError, exc)(self.connection.last_error)
  89. raise communication_exception_factory(LDAPSocketReceiveError, type(e)(str(e)))(self.connection.last_error)
  90. # If we are using DIGEST-MD5 and LDAP signing is set : verify & remove the signature from the message
  91. if self.connection.sasl_mechanism == DIGEST_MD5 and self.connection._digest_md5_kis and not self.connection.sasl_in_progress:
  92. data = sasl_next_packet + data
  93. if sasl_received_data == b'' or sasl_next_packet:
  94. # Remove the sizeOf(encoded_message + signature + 0x0001 + secNum) from data.
  95. sasl_buffer_length = int.from_bytes(data[0:4], "big")
  96. data = data[4:]
  97. sasl_next_packet = b''
  98. sasl_total_bytes_recieved += len(data)
  99. sasl_received_data += data
  100. if sasl_total_bytes_recieved >= sasl_buffer_length:
  101. # 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
  102. # This means that the end of one SASL packet/beginning of one other....could be located in the middle of data
  103. # We are using "sasl_received_data" instead of "data" & "unprocessed" for this reason
  104. # structure of messages when LDAP signing is enabled : sizeOf(encoded_message + signature + 0x0001 + secNum) + encoded_message + signature + 0x0001 + secNum
  105. sasl_signature = sasl_received_data[sasl_buffer_length - 16:sasl_buffer_length - 6]
  106. sasl_sec_num = sasl_received_data[sasl_buffer_length - 4:sasl_buffer_length]
  107. 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.
  108. sasl_received_data = sasl_received_data[:sasl_buffer_length - 16] # remove signature + 0x0001 + secNum + the next packet if any, from sasl_received_data
  109. kis = self.connection._digest_md5_kis # renamed to lowercase GC
  110. calculated_signature = bytes.fromhex(md5_hmac(kis, sasl_sec_num + sasl_received_data)[0:20])
  111. if sasl_signature != calculated_signature:
  112. 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() + ".")
  113. sasl_total_bytes_recieved = 0
  114. unprocessed += sasl_received_data
  115. sasl_received_data = b''
  116. else:
  117. unprocessed += data
  118. if len(data) > 0:
  119. length = BaseStrategy.compute_ldap_message_size(unprocessed)
  120. if length == -1: # too few data to decode message length
  121. get_more_data = True
  122. continue
  123. if len(unprocessed) < length:
  124. get_more_data = True
  125. else:
  126. if log_enabled(NETWORK):
  127. log(NETWORK, 'received %d bytes via <%s>', len(unprocessed[:length]), self.connection)
  128. messages.append(unprocessed[:length])
  129. unprocessed = unprocessed[length:]
  130. get_more_data = False
  131. if len(unprocessed) == 0:
  132. receiving = False
  133. else:
  134. receiving = False
  135. if log_enabled(NETWORK):
  136. log(NETWORK, 'received %d ldap messages via <%s>', len(messages), self.connection)
  137. return messages
  138. def post_send_single_response(self, message_id):
  139. """
  140. Executed after an Operation Request (except Search)
  141. Returns the result message or None
  142. """
  143. responses, result = self.get_response(message_id)
  144. self.connection.result = result
  145. if result['type'] == 'intermediateResponse': # checks that all responses are intermediates (there should be only one)
  146. for response in responses:
  147. if response['type'] != 'intermediateResponse':
  148. self.connection.last_error = 'multiple messages received error'
  149. if log_enabled(ERROR):
  150. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  151. raise LDAPSocketReceiveError(self.connection.last_error)
  152. responses.append(result)
  153. return responses
  154. def post_send_search(self, message_id):
  155. """
  156. Executed after a search request
  157. Returns the result message and store in connection.response the objects found
  158. """
  159. responses, result = self.get_response(message_id)
  160. self.connection.result = result
  161. if isinstance(responses, SEQUENCE_TYPES):
  162. self.connection.response = responses[:] # copy search result entries
  163. return responses
  164. self.connection.last_error = 'error receiving response'
  165. if log_enabled(ERROR):
  166. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  167. raise LDAPSocketReceiveError(self.connection.last_error)
  168. def _get_response(self, message_id, timeout):
  169. """
  170. Performs the capture of LDAP response for SyncStrategy
  171. """
  172. ldap_responses = []
  173. response_complete = False
  174. while not response_complete:
  175. responses = self.receiving()
  176. if responses:
  177. for response in responses:
  178. if len(response) > 0:
  179. if self.connection.usage:
  180. self.connection._usage.update_received_message(len(response))
  181. if self.connection.fast_decoder:
  182. ldap_resp = decode_message_fast(response)
  183. dict_response = self.decode_response_fast(ldap_resp)
  184. else:
  185. ldap_resp, _ = decoder.decode(response, asn1Spec=LDAP_MESSAGE_TEMPLATE) # unprocessed unused because receiving() waits for the whole message
  186. dict_response = self.decode_response(ldap_resp)
  187. if log_enabled(EXTENDED):
  188. log(EXTENDED, 'ldap message received via <%s>:%s', self.connection, format_ldap_message(ldap_resp, '<<'))
  189. if int(ldap_resp['messageID']) == message_id:
  190. ldap_responses.append(dict_response)
  191. if dict_response['type'] not in ['searchResEntry', 'searchResRef', 'intermediateResponse']:
  192. response_complete = True
  193. elif int(ldap_resp['messageID']) == 0: # 0 is reserved for 'Unsolicited Notification' from server as per RFC4511 (paragraph 4.4)
  194. if dict_response['responseName'] == '1.3.6.1.4.1.1466.20036': # Notice of Disconnection as per RFC4511 (paragraph 4.4.1)
  195. return SESSION_TERMINATED_BY_SERVER
  196. elif dict_response['responseName'] == '2.16.840.1.113719.1.27.103.4': # Novell LDAP transaction error unsolicited notification
  197. return TRANSACTION_ERROR
  198. else:
  199. self.connection.last_error = 'unknown unsolicited notification from server'
  200. if log_enabled(ERROR):
  201. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  202. raise LDAPSocketReceiveError(self.connection.last_error)
  203. elif int(ldap_resp['messageID']) != message_id and dict_response['type'] == 'extendedResp':
  204. self.connection.last_error = 'multiple extended responses to a single extended request'
  205. if log_enabled(ERROR):
  206. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  207. raise LDAPExtensionError(self.connection.last_error)
  208. # pass # ignore message with invalid messageId when receiving multiple extendedResp. This is not allowed by RFC4511 but some LDAP server do it
  209. else:
  210. self.connection.last_error = 'invalid messageId received'
  211. if log_enabled(ERROR):
  212. log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  213. raise LDAPSocketReceiveError(self.connection.last_error)
  214. # response = unprocessed
  215. # if response: # if this statement is removed unprocessed data will be processed as another message
  216. # self.connection.last_error = 'unprocessed substrate error'
  217. # if log_enabled(ERROR):
  218. # log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
  219. # raise LDAPSocketReceiveError(self.connection.last_error)
  220. else:
  221. return SESSION_TERMINATED_BY_SERVER
  222. ldap_responses.append(RESPONSE_COMPLETE)
  223. return ldap_responses
  224. def set_stream(self, value):
  225. raise NotImplementedError
  226. def get_stream(self):
  227. raise NotImplementedError