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