1""" 2""" 3 4# Created on 2013.07.15 5# 6# Author: Giovanni Cannata 7# 8# Copyright 2013 - 2020 Giovanni Cannata 9# 10# This file is part of ldap3. 11# 12# ldap3 is free software: you can redistribute it and/or modify 13# it under the terms of the GNU Lesser General Public License as published 14# by the Free Software Foundation, either version 3 of the License, or 15# (at your option) any later version. 16# 17# ldap3 is distributed in the hope that it will be useful, 18# but WITHOUT ANY WARRANTY; without even the implied warranty of 19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20# GNU Lesser General Public License for more dectails. 21# 22# You should have received a copy of the GNU Lesser General Public License 23# along with ldap3 in the COPYING and COPYING.LESSER files. 24# If not, see <http://www.gnu.org/licenses/>. 25 26import socket 27try: # try to discover if unix sockets are available for LDAP over IPC (ldapi:// scheme) 28 # noinspection PyUnresolvedReferences 29 from socket import AF_UNIX 30 unix_socket_available = True 31except ImportError: 32 unix_socket_available = False 33from struct import pack 34from platform import system 35from random import choice 36 37from .. import SYNC, ANONYMOUS, get_config_parameter, BASE, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, NO_ATTRIBUTES 38from ..core.results import DO_NOT_RAISE_EXCEPTIONS, RESULT_REFERRAL 39from ..core.exceptions import LDAPOperationResult, LDAPSASLBindInProgressError, LDAPSocketOpenError, LDAPSessionTerminatedByServerError,\ 40 LDAPUnknownResponseError, LDAPUnknownRequestError, LDAPReferralError, communication_exception_factory, LDAPStartTLSError, \ 41 LDAPSocketSendError, LDAPExceptionError, LDAPControlError, LDAPResponseTimeoutError, LDAPTransactionError 42from ..utils.uri import parse_uri 43from ..protocol.rfc4511 import LDAPMessage, ProtocolOp, MessageID, SearchResultEntry 44from ..operation.add import add_response_to_dict, add_request_to_dict 45from ..operation.modify import modify_request_to_dict, modify_response_to_dict 46from ..operation.search import search_result_reference_response_to_dict, search_result_done_response_to_dict,\ 47 search_result_entry_response_to_dict, search_request_to_dict, search_result_entry_response_to_dict_fast,\ 48 search_result_reference_response_to_dict_fast, attributes_to_dict, attributes_to_dict_fast 49from ..operation.bind import bind_response_to_dict, bind_request_to_dict, sicily_bind_response_to_dict, bind_response_to_dict_fast, \ 50 sicily_bind_response_to_dict_fast 51from ..operation.compare import compare_response_to_dict, compare_request_to_dict 52from ..operation.extended import extended_request_to_dict, extended_response_to_dict, intermediate_response_to_dict, extended_response_to_dict_fast, intermediate_response_to_dict_fast 53from ..core.server import Server 54from ..operation.modifyDn import modify_dn_request_to_dict, modify_dn_response_to_dict 55from ..operation.delete import delete_response_to_dict, delete_request_to_dict 56from ..protocol.convert import prepare_changes_for_request, build_controls_list 57from ..operation.abandon import abandon_request_to_dict 58from ..core.tls import Tls 59from ..protocol.oid import Oids 60from ..protocol.rfc2696 import RealSearchControlValue 61from ..protocol.microsoft import DirSyncControlResponseValue 62from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, NETWORK, EXTENDED, format_ldap_message 63from ..utils.asn1 import encode, decoder, ldap_result_to_dict_fast, decode_sequence 64from ..utils.conv import to_unicode 65 66SESSION_TERMINATED_BY_SERVER = 'TERMINATED_BY_SERVER' 67TRANSACTION_ERROR = 'TRANSACTION_ERROR' 68RESPONSE_COMPLETE = 'RESPONSE_FROM_SERVER_COMPLETE' 69 70 71# noinspection PyProtectedMember 72class BaseStrategy(object): 73 """ 74 Base class for connection strategy 75 """ 76 77 def __init__(self, ldap_connection): 78 self.connection = ldap_connection 79 self._outstanding = None 80 self._referrals = [] 81 self.sync = None # indicates a synchronous connection 82 self.no_real_dsa = None # indicates a connection to a fake LDAP server 83 self.pooled = None # Indicates a connection with a connection pool 84 self.can_stream = None # indicates if a strategy keeps a stream of responses (i.e. LdifProducer can accumulate responses with a single header). Stream must be initialized and closed in _start_listen() and _stop_listen() 85 self.referral_cache = {} 86 self.thread_safe = False # Indicates that connection can be used in a multithread application 87 if log_enabled(BASIC): 88 log(BASIC, 'instantiated <%s>: <%s>', self.__class__.__name__, self) 89 90 def __str__(self): 91 s = [ 92 str(self.connection) if self.connection else 'None', 93 'sync' if self.sync else 'async', 94 'no real DSA' if self.no_real_dsa else 'real DSA', 95 'pooled' if self.pooled else 'not pooled', 96 'can stream output' if self.can_stream else 'cannot stream output', 97 ] 98 return ' - '.join(s) 99 100 def open(self, reset_usage=True, read_server_info=True): 101 """ 102 Open a socket to a server. Choose a server from the server pool if available 103 """ 104 if log_enabled(NETWORK): 105 log(NETWORK, 'opening connection for <%s>', self.connection) 106 if self.connection.lazy and not self.connection._executing_deferred: 107 self.connection._deferred_open = True 108 self.connection.closed = False 109 if log_enabled(NETWORK): 110 log(NETWORK, 'deferring open connection for <%s>', self.connection) 111 else: 112 if not self.connection.closed and not self.connection._executing_deferred: # try to close connection if still open 113 self.close() 114 115 self._outstanding = dict() 116 if self.connection.usage: 117 if reset_usage or not self.connection._usage.initial_connection_start_time: 118 self.connection._usage.start() 119 120 if self.connection.server_pool: 121 new_server = self.connection.server_pool.get_server(self.connection) # get a server from the server_pool if available 122 if self.connection.server != new_server: 123 self.connection.server = new_server 124 if self.connection.usage: 125 self.connection._usage.servers_from_pool += 1 126 127 exception_history = [] 128 if not self.no_real_dsa: # tries to connect to a real server 129 for candidate_address in self.connection.server.candidate_addresses(): 130 try: 131 if log_enabled(BASIC): 132 log(BASIC, 'try to open candidate address %s', candidate_address[:-2]) 133 self._open_socket(candidate_address, self.connection.server.ssl, unix_socket=self.connection.server.ipc) 134 self.connection.server.current_address = candidate_address 135 self.connection.server.update_availability(candidate_address, True) 136 break 137 except Exception as e: 138 self.connection.server.update_availability(candidate_address, False) 139 # exception_history.append((datetime.now(), exc_type, exc_value, candidate_address[4])) 140 exception_history.append((type(e)(str(e)), candidate_address[4])) 141 if not self.connection.server.current_address and exception_history: 142 if len(exception_history) == 1: # only one exception, reraise 143 if log_enabled(ERROR): 144 log(ERROR, '<%s> for <%s>', str(exception_history[0][0]) + ' ' + str((exception_history[0][1])), self.connection) 145 raise exception_history[0][0] 146 else: 147 if log_enabled(ERROR): 148 log(ERROR, 'unable to open socket for <%s>', self.connection) 149 raise LDAPSocketOpenError('unable to open socket', exception_history) 150 elif not self.connection.server.current_address: 151 if log_enabled(ERROR): 152 log(ERROR, 'invalid server address for <%s>', self.connection) 153 raise LDAPSocketOpenError('invalid server address') 154 155 self.connection._deferred_open = False 156 self._start_listen() 157 if log_enabled(NETWORK): 158 log(NETWORK, 'connection open for <%s>', self.connection) 159 160 def close(self): 161 """ 162 Close connection 163 """ 164 if log_enabled(NETWORK): 165 log(NETWORK, 'closing connection for <%s>', self.connection) 166 if self.connection.lazy and not self.connection._executing_deferred and (self.connection._deferred_bind or self.connection._deferred_open): 167 self.connection.listening = False 168 self.connection.closed = True 169 if log_enabled(NETWORK): 170 log(NETWORK, 'deferred connection closed for <%s>', self.connection) 171 else: 172 if not self.connection.closed: 173 self._stop_listen() 174 if not self. no_real_dsa: 175 self._close_socket() 176 if log_enabled(NETWORK): 177 log(NETWORK, 'connection closed for <%s>', self.connection) 178 179 self.connection.bound = False 180 self.connection.request = None 181 self.connection.response = None 182 self.connection.tls_started = False 183 self._outstanding = None 184 self._referrals = [] 185 186 if not self.connection.strategy.no_real_dsa: 187 self.connection.server.current_address = None 188 if self.connection.usage: 189 self.connection._usage.stop() 190 191 def _open_socket(self, address, use_ssl=False, unix_socket=False): 192 """ 193 Tries to open and connect a socket to a Server 194 raise LDAPExceptionError if unable to open or connect socket 195 """ 196 try: 197 self.connection.socket = socket.socket(*address[:3]) 198 except Exception as e: 199 self.connection.last_error = 'socket creation error: ' + str(e) 200 if log_enabled(ERROR): 201 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 202 # raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) 203 raise communication_exception_factory(LDAPSocketOpenError, type(e)(str(e)))(self.connection.last_error) 204 # Try to bind the socket locally before connecting to the remote address 205 # We go through our connection's source ports and try to bind our socket to our connection's source address 206 # with them. 207 # If no source address or ports were specified, this will have the same success/fail result as if we 208 # tried to connect to the remote server without binding locally first. 209 # This is actually a little bit better, as it lets us distinguish the case of "issue binding the socket 210 # locally" from "remote server is unavailable" with more clarity, though this will only really be an 211 # issue when no source address/port is specified if the system checking server availability is running 212 # as a very unprivileged user. 213 last_bind_exc = None 214 if unix_socket_available and self.connection.socket.family != socket.AF_UNIX: 215 socket_bind_succeeded = False 216 for source_port in self.connection.source_port_list: 217 try: 218 self.connection.socket.bind((self.connection.source_address, source_port)) 219 socket_bind_succeeded = True 220 break 221 except Exception as bind_ex: 222 last_bind_exc = bind_ex 223 # we'll always end up logging at error level if we cannot bind any ports to the address locally. 224 # but if some work and some don't you probably don't want the ones that don't at ERROR level 225 if log_enabled(NETWORK): 226 log(NETWORK, 'Unable to bind to local address <%s> with source port <%s> due to <%s>', 227 self.connection.source_address, source_port, bind_ex) 228 if not socket_bind_succeeded: 229 self.connection.last_error = 'socket connection error while locally binding: ' + str(last_bind_exc) 230 if log_enabled(ERROR): 231 log(ERROR, 'Unable to locally bind to local address <%s> with any of the source ports <%s> for connection <%s due to <%s>', 232 self.connection.source_address, self.connection.source_port_list, self.connection, last_bind_exc) 233 raise communication_exception_factory(LDAPSocketOpenError, type(last_bind_exc)(str(last_bind_exc)))(last_bind_exc) 234 235 try: # set socket timeout for opening connection 236 if self.connection.server.connect_timeout: 237 self.connection.socket.settimeout(self.connection.server.connect_timeout) 238 self.connection.socket.connect(address[4]) 239 except socket.error as e: 240 self.connection.last_error = 'socket connection error while opening: ' + str(e) 241 if log_enabled(ERROR): 242 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 243 # raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) 244 raise communication_exception_factory(LDAPSocketOpenError, type(e)(str(e)))(self.connection.last_error) 245 246 # Set connection recv timeout (must be set after connect, 247 # because socket.settimeout() affects both, connect() as 248 # well as recv(). Set it before tls.wrap_socket() because 249 # the recv timeout should take effect during the TLS 250 # handshake. 251 if self.connection.receive_timeout is not None: 252 try: # set receive timeout for the connection socket 253 self.connection.socket.settimeout(self.connection.receive_timeout) 254 if system().lower() == 'windows': 255 self.connection.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, int(1000 * self.connection.receive_timeout)) 256 else: 257 self.connection.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, pack('LL', self.connection.receive_timeout, 0)) 258 except socket.error as e: 259 self.connection.last_error = 'unable to set receive timeout for socket connection: ' + str(e) 260 261 # if exc: 262 # if log_enabled(ERROR): 263 # log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 264 # raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error) 265 if log_enabled(ERROR): 266 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 267 raise communication_exception_factory(LDAPSocketOpenError, type(e)(str(e)))(self.connection.last_error) 268 269 if use_ssl: 270 try: 271 self.connection.server.tls.wrap_socket(self.connection, do_handshake=True) 272 if self.connection.usage: 273 self.connection._usage.wrapped_sockets += 1 274 except Exception as e: 275 self.connection.last_error = 'socket ssl wrapping error: ' + str(e) 276 if log_enabled(ERROR): 277 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 278 raise communication_exception_factory(LDAPSocketOpenError, type(e)(str(e)))(self.connection.last_error) 279 if self.connection.usage: 280 self.connection._usage.open_sockets += 1 281 282 self.connection.closed = False 283 284 def _close_socket(self): 285 """ 286 Try to close a socket 287 don't raise exception if unable to close socket, assume socket is already closed 288 """ 289 290 try: 291 self.connection.socket.shutdown(socket.SHUT_RDWR) 292 except Exception: 293 pass 294 295 try: 296 self.connection.socket.close() 297 except Exception: 298 pass 299 300 self.connection.socket = None 301 self.connection.closed = True 302 303 if self.connection.usage: 304 self.connection._usage.closed_sockets += 1 305 306 def _stop_listen(self): 307 self.connection.listening = False 308 309 def send(self, message_type, request, controls=None): 310 """ 311 Send an LDAP message 312 Returns the message_id 313 """ 314 self.connection.request = None 315 if self.connection.listening: 316 if self.connection.sasl_in_progress and message_type not in ['bindRequest']: # as per RFC4511 (4.2.1) 317 self.connection.last_error = 'cannot send operation requests while SASL bind is in progress' 318 if log_enabled(ERROR): 319 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 320 raise LDAPSASLBindInProgressError(self.connection.last_error) 321 message_id = self.connection.server.next_message_id() 322 ldap_message = LDAPMessage() 323 ldap_message['messageID'] = MessageID(message_id) 324 ldap_message['protocolOp'] = ProtocolOp().setComponentByName(message_type, request) 325 message_controls = build_controls_list(controls) 326 if message_controls is not None: 327 ldap_message['controls'] = message_controls 328 self.connection.request = BaseStrategy.decode_request(message_type, request, controls) 329 self._outstanding[message_id] = self.connection.request 330 self.sending(ldap_message) 331 else: 332 self.connection.last_error = 'unable to send message, socket is not open' 333 if log_enabled(ERROR): 334 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 335 raise LDAPSocketOpenError(self.connection.last_error) 336 337 return message_id 338 339 def get_response(self, message_id, timeout=None, get_request=False): 340 """ 341 Get response LDAP messages 342 Responses are returned by the underlying connection strategy 343 Check if message_id LDAP message is still outstanding and wait for timeout to see if it appears in _get_response 344 Result is stored in connection.result 345 Responses without result is stored in connection.response 346 A tuple (responses, result) is returned 347 """ 348 if timeout is None: 349 timeout = get_config_parameter('RESPONSE_WAITING_TIMEOUT') 350 response = None 351 result = None 352 request = None 353 if self._outstanding and message_id in self._outstanding: 354 responses = self._get_response(message_id, timeout) 355 356 if not responses: 357 if log_enabled(ERROR): 358 log(ERROR, 'socket timeout, no response from server for <%s>', self.connection) 359 raise LDAPResponseTimeoutError('no response from server') 360 361 if responses == SESSION_TERMINATED_BY_SERVER: 362 try: # try to close the session but don't raise any error if server has already closed the session 363 self.close() 364 except (socket.error, LDAPExceptionError): 365 pass 366 self.connection.last_error = 'session terminated by server' 367 if log_enabled(ERROR): 368 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 369 raise LDAPSessionTerminatedByServerError(self.connection.last_error) 370 elif responses == TRANSACTION_ERROR: # Novell LDAP Transaction unsolicited notification 371 self.connection.last_error = 'transaction error' 372 if log_enabled(ERROR): 373 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 374 raise LDAPTransactionError(self.connection.last_error) 375 376 # if referral in response opens a new connection to resolve referrals if requested 377 378 if responses[-2]['result'] == RESULT_REFERRAL: 379 if self.connection.usage: 380 self.connection._usage.referrals_received += 1 381 if self.connection.auto_referrals: 382 ref_response, ref_result = self.do_operation_on_referral(self._outstanding[message_id], responses[-2]['referrals']) 383 if ref_response is not None: 384 responses = ref_response + [ref_result] 385 responses.append(RESPONSE_COMPLETE) 386 elif ref_result is not None: 387 responses = [ref_result, RESPONSE_COMPLETE] 388 389 self._referrals = [] 390 391 if responses: 392 result = responses[-2] 393 response = responses[:-2] 394 self.connection.result = None 395 self.connection.response = None 396 397 if self.connection.raise_exceptions and result and result['result'] not in DO_NOT_RAISE_EXCEPTIONS: 398 if log_enabled(PROTOCOL): 399 log(PROTOCOL, 'operation result <%s> for <%s>', result, self.connection) 400 self._outstanding.pop(message_id) 401 self.connection.result = result.copy() 402 raise LDAPOperationResult(result=result['result'], description=result['description'], dn=result['dn'], message=result['message'], response_type=result['type']) 403 404 # checks if any response has a range tag 405 # self._auto_range_searching is set as a flag to avoid recursive searches 406 if self.connection.auto_range and not hasattr(self, '_auto_range_searching') and any((True for resp in response if 'raw_attributes' in resp for name in resp['raw_attributes'] if ';range=' in name)): 407 self._auto_range_searching = result.copy() 408 temp_response = response[:] # copy 409 if self.do_search_on_auto_range(self._outstanding[message_id], response): 410 for resp in temp_response: 411 if resp['type'] == 'searchResEntry': 412 keys = [key for key in resp['raw_attributes'] if ';range=' in key] 413 for key in keys: 414 del resp['raw_attributes'][key] 415 del resp['attributes'][key] 416 response = temp_response 417 result = self._auto_range_searching 418 del self._auto_range_searching 419 420 if self.connection.empty_attributes: 421 for entry in response: 422 if entry['type'] == 'searchResEntry': 423 for attribute_type in self._outstanding[message_id]['attributes']: 424 if attribute_type not in entry['raw_attributes'] and attribute_type not in (ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, NO_ATTRIBUTES): 425 entry['raw_attributes'][attribute_type] = list() 426 entry['attributes'][attribute_type] = list() 427 if log_enabled(PROTOCOL): 428 log(PROTOCOL, 'attribute set to empty list for missing attribute <%s> in <%s>', attribute_type, self) 429 if not self.connection.auto_range: 430 attrs_to_remove = [] 431 # removes original empty attribute in case a range tag is returned 432 for attribute_type in entry['attributes']: 433 if ';range' in attribute_type.lower(): 434 orig_attr, _, _ = attribute_type.partition(';') 435 attrs_to_remove.append(orig_attr) 436 for attribute_type in attrs_to_remove: 437 if log_enabled(PROTOCOL): 438 log(PROTOCOL, 'attribute type <%s> removed in response because of same attribute returned as range by the server in <%s>', attribute_type, self) 439 del entry['raw_attributes'][attribute_type] 440 del entry['attributes'][attribute_type] 441 442 request = self._outstanding.pop(message_id) 443 else: 444 if log_enabled(ERROR): 445 log(ERROR, 'message id not in outstanding queue for <%s>', self.connection) 446 raise(LDAPResponseTimeoutError('message id not in outstanding queue')) 447 448 if get_request: 449 return response, result, request 450 else: 451 return response, result 452 453 @staticmethod 454 def compute_ldap_message_size(data): 455 """ 456 Compute LDAP Message size according to BER definite length rules 457 Returns -1 if too few data to compute message length 458 """ 459 if isinstance(data, str): # fix for Python 2, data is string not bytes 460 data = bytearray(data) # Python 2 bytearray is equivalent to Python 3 bytes 461 462 ret_value = -1 463 if len(data) > 2: 464 if data[1] <= 127: # BER definite length - short form. Highest bit of byte 1 is 0, message length is in the last 7 bits - Value can be up to 127 bytes long 465 ret_value = data[1] + 2 466 else: # BER definite length - long form. Highest bit of byte 1 is 1, last 7 bits counts the number of following octets containing the value length 467 bytes_length = data[1] - 128 468 if len(data) >= bytes_length + 2: 469 value_length = 0 470 cont = bytes_length 471 for byte in data[2:2 + bytes_length]: 472 cont -= 1 473 value_length += byte * (256 ** cont) 474 ret_value = value_length + 2 + bytes_length 475 476 return ret_value 477 478 def decode_response(self, ldap_message): 479 """ 480 Convert received LDAPMessage to a dict 481 """ 482 message_type = ldap_message.getComponentByName('protocolOp').getName() 483 component = ldap_message['protocolOp'].getComponent() 484 controls = ldap_message['controls'] if ldap_message['controls'].hasValue() else None 485 if message_type == 'bindResponse': 486 if not bytes(component['matchedDN']).startswith(b'NTLM'): # patch for microsoft ntlm authentication 487 result = bind_response_to_dict(component) 488 else: 489 result = sicily_bind_response_to_dict(component) 490 elif message_type == 'searchResEntry': 491 result = search_result_entry_response_to_dict(component, self.connection.server.schema, self.connection.server.custom_formatter, self.connection.check_names) 492 elif message_type == 'searchResDone': 493 result = search_result_done_response_to_dict(component) 494 elif message_type == 'searchResRef': 495 result = search_result_reference_response_to_dict(component) 496 elif message_type == 'modifyResponse': 497 result = modify_response_to_dict(component) 498 elif message_type == 'addResponse': 499 result = add_response_to_dict(component) 500 elif message_type == 'delResponse': 501 result = delete_response_to_dict(component) 502 elif message_type == 'modDNResponse': 503 result = modify_dn_response_to_dict(component) 504 elif message_type == 'compareResponse': 505 result = compare_response_to_dict(component) 506 elif message_type == 'extendedResp': 507 result = extended_response_to_dict(component) 508 elif message_type == 'intermediateResponse': 509 result = intermediate_response_to_dict(component) 510 else: 511 if log_enabled(ERROR): 512 log(ERROR, 'unknown response <%s> for <%s>', message_type, self.connection) 513 raise LDAPUnknownResponseError('unknown response') 514 result['type'] = message_type 515 if controls: 516 result['controls'] = dict() 517 for control in controls: 518 decoded_control = self.decode_control(control) 519 result['controls'][decoded_control[0]] = decoded_control[1] 520 return result 521 522 def decode_response_fast(self, ldap_message): 523 """ 524 Convert received LDAPMessage from fast ber decoder to a dict 525 """ 526 if ldap_message['protocolOp'] == 1: # bindResponse 527 if not ldap_message['payload'][1][3].startswith(b'NTLM'): # patch for microsoft ntlm authentication 528 result = bind_response_to_dict_fast(ldap_message['payload']) 529 else: 530 result = sicily_bind_response_to_dict_fast(ldap_message['payload']) 531 result['type'] = 'bindResponse' 532 elif ldap_message['protocolOp'] == 4: # searchResEntry' 533 result = search_result_entry_response_to_dict_fast(ldap_message['payload'], self.connection.server.schema, self.connection.server.custom_formatter, self.connection.check_names) 534 result['type'] = 'searchResEntry' 535 elif ldap_message['protocolOp'] == 5: # searchResDone 536 result = ldap_result_to_dict_fast(ldap_message['payload']) 537 result['type'] = 'searchResDone' 538 elif ldap_message['protocolOp'] == 19: # searchResRef 539 result = search_result_reference_response_to_dict_fast(ldap_message['payload']) 540 result['type'] = 'searchResRef' 541 elif ldap_message['protocolOp'] == 7: # modifyResponse 542 result = ldap_result_to_dict_fast(ldap_message['payload']) 543 result['type'] = 'modifyResponse' 544 elif ldap_message['protocolOp'] == 9: # addResponse 545 result = ldap_result_to_dict_fast(ldap_message['payload']) 546 result['type'] = 'addResponse' 547 elif ldap_message['protocolOp'] == 11: # delResponse 548 result = ldap_result_to_dict_fast(ldap_message['payload']) 549 result['type'] = 'delResponse' 550 elif ldap_message['protocolOp'] == 13: # modDNResponse 551 result = ldap_result_to_dict_fast(ldap_message['payload']) 552 result['type'] = 'modDNResponse' 553 elif ldap_message['protocolOp'] == 15: # compareResponse 554 result = ldap_result_to_dict_fast(ldap_message['payload']) 555 result['type'] = 'compareResponse' 556 elif ldap_message['protocolOp'] == 24: # extendedResp 557 result = extended_response_to_dict_fast(ldap_message['payload']) 558 result['type'] = 'extendedResp' 559 elif ldap_message['protocolOp'] == 25: # intermediateResponse 560 result = intermediate_response_to_dict_fast(ldap_message['payload']) 561 result['type'] = 'intermediateResponse' 562 else: 563 if log_enabled(ERROR): 564 log(ERROR, 'unknown response <%s> for <%s>', ldap_message['protocolOp'], self.connection) 565 raise LDAPUnknownResponseError('unknown response') 566 if ldap_message['controls']: 567 result['controls'] = dict() 568 for control in ldap_message['controls']: 569 decoded_control = self.decode_control_fast(control[3]) 570 result['controls'][decoded_control[0]] = decoded_control[1] 571 return result 572 573 @staticmethod 574 def decode_control(control): 575 """ 576 decode control, return a 2-element tuple where the first element is the control oid 577 and the second element is a dictionary with description (from Oids), criticality and decoded control value 578 """ 579 control_type = str(control['controlType']) 580 criticality = bool(control['criticality']) 581 control_value = bytes(control['controlValue']) 582 unprocessed = None 583 if control_type == '1.2.840.113556.1.4.319': # simple paged search as per RFC2696 584 control_resp, unprocessed = decoder.decode(control_value, asn1Spec=RealSearchControlValue()) 585 control_value = dict() 586 control_value['size'] = int(control_resp['size']) 587 control_value['cookie'] = bytes(control_resp['cookie']) 588 elif control_type == '1.2.840.113556.1.4.841': # DirSync AD 589 control_resp, unprocessed = decoder.decode(control_value, asn1Spec=DirSyncControlResponseValue()) 590 control_value = dict() 591 control_value['more_results'] = bool(control_resp['MoreResults']) # more_result if nonzero 592 control_value['cookie'] = bytes(control_resp['CookieServer']) 593 elif control_type == '1.3.6.1.1.13.1' or control_type == '1.3.6.1.1.13.2': # Pre-Read control, Post-Read Control as per RFC 4527 594 control_resp, unprocessed = decoder.decode(control_value, asn1Spec=SearchResultEntry()) 595 control_value = dict() 596 control_value['result'] = attributes_to_dict(control_resp['attributes']) 597 if unprocessed: 598 if log_enabled(ERROR): 599 log(ERROR, 'unprocessed control response in substrate') 600 raise LDAPControlError('unprocessed control response in substrate') 601 return control_type, {'description': Oids.get(control_type, ''), 'criticality': criticality, 'value': control_value} 602 603 @staticmethod 604 def decode_control_fast(control, from_server=True): 605 """ 606 decode control, return a 2-element tuple where the first element is the control oid 607 and the second element is a dictionary with description (from Oids), criticality and decoded control value 608 """ 609 control_type = str(to_unicode(control[0][3], from_server=from_server)) 610 criticality = False 611 control_value = None 612 for r in control[1:]: 613 if r[2] == 4: # controlValue 614 control_value = r[3] 615 else: 616 criticality = False if r[3] == 0 else True # criticality (booleand default to False) 617 if control_type == '1.2.840.113556.1.4.319': # simple paged search as per RFC2696 618 control_resp = decode_sequence(control_value, 0, len(control_value)) 619 control_value = dict() 620 control_value['size'] = int(control_resp[0][3][0][3]) 621 control_value['cookie'] = bytes(control_resp[0][3][1][3]) 622 elif control_type == '1.2.840.113556.1.4.841': # DirSync AD 623 control_resp = decode_sequence(control_value, 0, len(control_value)) 624 control_value = dict() 625 control_value['more_results'] = True if control_resp[0][3][0][3] else False # more_result if nonzero 626 control_value['cookie'] = control_resp[0][3][2][3] 627 elif control_type == '1.3.6.1.1.13.1' or control_type == '1.3.6.1.1.13.2': # Pre-Read control, Post-Read Control as per RFC 4527 628 control_resp = decode_sequence(control_value, 0, len(control_value)) 629 control_value = dict() 630 control_value['result'] = attributes_to_dict_fast(control_resp[0][3][1][3]) 631 return control_type, {'description': Oids.get(control_type, ''), 'criticality': criticality, 'value': control_value} 632 633 @staticmethod 634 def decode_request(message_type, component, controls=None): 635 # message_type = ldap_message.getComponentByName('protocolOp').getName() 636 # component = ldap_message['protocolOp'].getComponent() 637 if message_type == 'bindRequest': 638 result = bind_request_to_dict(component) 639 elif message_type == 'unbindRequest': 640 result = dict() 641 elif message_type == 'addRequest': 642 result = add_request_to_dict(component) 643 elif message_type == 'compareRequest': 644 result = compare_request_to_dict(component) 645 elif message_type == 'delRequest': 646 result = delete_request_to_dict(component) 647 elif message_type == 'extendedReq': 648 result = extended_request_to_dict(component) 649 elif message_type == 'modifyRequest': 650 result = modify_request_to_dict(component) 651 elif message_type == 'modDNRequest': 652 result = modify_dn_request_to_dict(component) 653 elif message_type == 'searchRequest': 654 result = search_request_to_dict(component) 655 elif message_type == 'abandonRequest': 656 result = abandon_request_to_dict(component) 657 else: 658 if log_enabled(ERROR): 659 log(ERROR, 'unknown request <%s>', message_type) 660 raise LDAPUnknownRequestError('unknown request') 661 result['type'] = message_type 662 result['controls'] = controls 663 664 return result 665 666 def valid_referral_list(self, referrals): 667 referral_list = [] 668 for referral in referrals: 669 candidate_referral = parse_uri(referral) 670 if candidate_referral: 671 for ref_host in self.connection.server.allowed_referral_hosts: 672 if ref_host[0] == candidate_referral['host'] or ref_host[0] == '*': 673 if candidate_referral['host'] not in self._referrals: 674 candidate_referral['anonymousBindOnly'] = not ref_host[1] 675 referral_list.append(candidate_referral) 676 break 677 678 return referral_list 679 680 def do_next_range_search(self, request, response, attr_name): 681 done = False 682 current_response = response 683 while not done: 684 attr_type, _, returned_range = attr_name.partition(';range=') 685 _, _, high_range = returned_range.partition('-') 686 response['raw_attributes'][attr_type] += current_response['raw_attributes'][attr_name] 687 response['attributes'][attr_type] += current_response['attributes'][attr_name] 688 if high_range != '*': 689 if log_enabled(PROTOCOL): 690 log(PROTOCOL, 'performing next search on auto-range <%s> via <%s>', str(int(high_range) + 1), self.connection) 691 requested_range = attr_type + ';range=' + str(int(high_range) + 1) + '-*' 692 result = self.connection.search(search_base=response['dn'], 693 search_filter='(objectclass=*)', 694 search_scope=BASE, 695 dereference_aliases=request['dereferenceAlias'], 696 attributes=[attr_type + ';range=' + str(int(high_range) + 1) + '-*']) 697 if self.connection.strategy.thread_safe: 698 status, result, _response, _ = result 699 else: 700 status = result 701 result = self.connection.result 702 _response = self.connection.response 703 704 if self.connection.strategy.sync: 705 if status: 706 current_response = _response[0] 707 else: 708 done = True 709 else: 710 current_response, _ = self.get_response(status) 711 current_response = current_response[0] 712 713 if not done: 714 if requested_range in current_response['raw_attributes'] and len(current_response['raw_attributes'][requested_range]) == 0: 715 del current_response['raw_attributes'][requested_range] 716 del current_response['attributes'][requested_range] 717 attr_name = list(filter(lambda a: ';range=' in a, current_response['raw_attributes'].keys()))[0] 718 continue 719 720 done = True 721 722 def do_search_on_auto_range(self, request, response): 723 for resp in [r for r in response if r['type'] == 'searchResEntry']: 724 for attr_name in list(resp['raw_attributes'].keys()): # generate list to avoid changing of dict size error 725 if ';range=' in attr_name: 726 attr_type, _, range_values = attr_name.partition(';range=') 727 if range_values in ('1-1', '0-0'): # DirSync returns these values for adding and removing members 728 return False 729 if attr_type not in resp['raw_attributes'] or resp['raw_attributes'][attr_type] is None: 730 resp['raw_attributes'][attr_type] = list() 731 if attr_type not in resp['attributes'] or resp['attributes'][attr_type] is None: 732 resp['attributes'][attr_type] = list() 733 self.do_next_range_search(request, resp, attr_name) 734 return True 735 736 def create_referral_connection(self, referrals): 737 referral_connection = None 738 selected_referral = None 739 cachekey = None 740 valid_referral_list = self.valid_referral_list(referrals) 741 if valid_referral_list: 742 preferred_referral_list = [referral for referral in valid_referral_list if 743 referral['ssl'] == self.connection.server.ssl] 744 selected_referral = choice(preferred_referral_list) if preferred_referral_list else choice( 745 valid_referral_list) 746 747 cachekey = (selected_referral['host'], selected_referral['port'] or self.connection.server.port, selected_referral['ssl']) 748 if self.connection.use_referral_cache and cachekey in self.referral_cache: 749 referral_connection = self.referral_cache[cachekey] 750 else: 751 referral_server = Server(host=selected_referral['host'], 752 port=selected_referral['port'] or self.connection.server.port, 753 use_ssl=selected_referral['ssl'], 754 get_info=self.connection.server.get_info, 755 formatter=self.connection.server.custom_formatter, 756 connect_timeout=self.connection.server.connect_timeout, 757 mode=self.connection.server.mode, 758 allowed_referral_hosts=self.connection.server.allowed_referral_hosts, 759 tls=Tls(local_private_key_file=self.connection.server.tls.private_key_file, 760 local_certificate_file=self.connection.server.tls.certificate_file, 761 validate=self.connection.server.tls.validate, 762 version=self.connection.server.tls.version, 763 ca_certs_file=self.connection.server.tls.ca_certs_file) if 764 selected_referral['ssl'] else None) 765 766 from ..core.connection import Connection 767 768 referral_connection = Connection(server=referral_server, 769 user=self.connection.user if not selected_referral['anonymousBindOnly'] else None, 770 password=self.connection.password if not selected_referral['anonymousBindOnly'] else None, 771 version=self.connection.version, 772 authentication=self.connection.authentication if not selected_referral['anonymousBindOnly'] else ANONYMOUS, 773 client_strategy=SYNC, 774 auto_referrals=True, 775 read_only=self.connection.read_only, 776 check_names=self.connection.check_names, 777 raise_exceptions=self.connection.raise_exceptions, 778 fast_decoder=self.connection.fast_decoder, 779 receive_timeout=self.connection.receive_timeout, 780 sasl_mechanism=self.connection.sasl_mechanism, 781 sasl_credentials=self.connection.sasl_credentials) 782 783 if self.connection.usage: 784 self.connection._usage.referrals_connections += 1 785 786 referral_connection.open() 787 referral_connection.strategy._referrals = self._referrals 788 if self.connection.tls_started and not referral_server.ssl: # if the original server was in start_tls mode and the referral server is not in ssl then start_tls on the referral connection 789 if not referral_connection.start_tls(): 790 error = 'start_tls in referral not successful' + (' - ' + referral_connection.last_error if referral_connection.last_error else '') 791 if log_enabled(ERROR): 792 log(ERROR, '%s for <%s>', error, self) 793 self.unbind() 794 raise LDAPStartTLSError(error) 795 796 if self.connection.bound: 797 referral_connection.bind() 798 799 if self.connection.usage: 800 self.connection._usage.referrals_followed += 1 801 802 return selected_referral, referral_connection, cachekey 803 804 def do_operation_on_referral(self, request, referrals): 805 if log_enabled(PROTOCOL): 806 log(PROTOCOL, 'following referral for <%s>', self.connection) 807 selected_referral, referral_connection, cachekey = self.create_referral_connection(referrals) 808 if selected_referral: 809 if request['type'] == 'searchRequest': 810 referral_connection.search(selected_referral['base'] or request['base'], 811 selected_referral['filter'] or request['filter'], 812 selected_referral['scope'] or request['scope'], 813 request['dereferenceAlias'], 814 selected_referral['attributes'] or request['attributes'], 815 request['sizeLimit'], 816 request['timeLimit'], 817 request['typesOnly'], 818 controls=request['controls']) 819 elif request['type'] == 'addRequest': 820 referral_connection.add(selected_referral['base'] or request['entry'], 821 None, 822 request['attributes'], 823 controls=request['controls']) 824 elif request['type'] == 'compareRequest': 825 referral_connection.compare(selected_referral['base'] or request['entry'], 826 request['attribute'], 827 request['value'], 828 controls=request['controls']) 829 elif request['type'] == 'delRequest': 830 referral_connection.delete(selected_referral['base'] or request['entry'], 831 controls=request['controls']) 832 elif request['type'] == 'extendedReq': 833 referral_connection.extended(request['name'], 834 request['value'], 835 controls=request['controls'], 836 no_encode=True 837 ) 838 elif request['type'] == 'modifyRequest': 839 referral_connection.modify(selected_referral['base'] or request['entry'], 840 prepare_changes_for_request(request['changes']), 841 controls=request['controls']) 842 elif request['type'] == 'modDNRequest': 843 referral_connection.modify_dn(selected_referral['base'] or request['entry'], 844 request['newRdn'], 845 request['deleteOldRdn'], 846 request['newSuperior'], 847 controls=request['controls']) 848 else: 849 self.connection.last_error = 'referral operation not permitted' 850 if log_enabled(ERROR): 851 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 852 raise LDAPReferralError(self.connection.last_error) 853 854 response = referral_connection.response 855 result = referral_connection.result 856 if self.connection.use_referral_cache: 857 self.referral_cache[cachekey] = referral_connection 858 else: 859 referral_connection.unbind() 860 else: 861 response = None 862 result = None 863 864 return response, result 865 866 def sending(self, ldap_message): 867 if log_enabled(NETWORK): 868 log(NETWORK, 'sending 1 ldap message for <%s>', self.connection) 869 try: 870 encoded_message = encode(ldap_message) 871 self.connection.socket.sendall(encoded_message) 872 if log_enabled(EXTENDED): 873 log(EXTENDED, 'ldap message sent via <%s>:%s', self.connection, format_ldap_message(ldap_message, '>>')) 874 if log_enabled(NETWORK): 875 log(NETWORK, 'sent %d bytes via <%s>', len(encoded_message), self.connection) 876 except socket.error as e: 877 self.connection.last_error = 'socket sending error' + str(e) 878 encoded_message = None 879 if log_enabled(ERROR): 880 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 881 # raise communication_exception_factory(LDAPSocketSendError, exc)(self.connection.last_error) 882 raise communication_exception_factory(LDAPSocketSendError, type(e)(str(e)))(self.connection.last_error) 883 if self.connection.usage: 884 self.connection._usage.update_transmitted_message(self.connection.request, len(encoded_message)) 885 886 def _start_listen(self): 887 # overridden on strategy class 888 raise NotImplementedError 889 890 def _get_response(self, message_id, timeout): 891 # overridden in strategy class 892 raise NotImplementedError 893 894 def receiving(self): 895 # overridden in strategy class 896 raise NotImplementedError 897 898 def post_send_single_response(self, message_id): 899 # overridden in strategy class 900 raise NotImplementedError 901 902 def post_send_search(self, message_id): 903 # overridden in strategy class 904 raise NotImplementedError 905 906 def get_stream(self): 907 raise NotImplementedError 908 909 def set_stream(self, value): 910 raise NotImplementedError 911 912 def unbind_referral_cache(self): 913 while len(self.referral_cache) > 0: 914 cachekey, referral_connection = self.referral_cache.popitem() 915 referral_connection.unbind() 916