1"""
2"""
3
4# Created on 2014.05.31
5#
6# Author: Giovanni Cannata
7#
8# Copyright 2014 - 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 details.
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/>.
25from copy import deepcopy, copy
26from os import linesep
27from threading import RLock, Lock
28from functools import reduce
29import json
30
31from .. import ANONYMOUS, SIMPLE, SASL, MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, get_config_parameter, DEREF_ALWAYS, \
32    SUBTREE, ASYNC, SYNC, NO_ATTRIBUTES, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, MODIFY_INCREMENT, LDIF, ASYNC_STREAM, \
33    RESTARTABLE, ROUND_ROBIN, REUSABLE, AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_TLS_BEFORE_BIND, SAFE_SYNC, \
34    AUTO_BIND_TLS_AFTER_BIND, AUTO_BIND_NO_TLS, STRING_TYPES, SEQUENCE_TYPES, MOCK_SYNC, MOCK_ASYNC, NTLM, EXTERNAL,\
35    DIGEST_MD5, GSSAPI, PLAIN, DSA, SCHEMA, ALL
36
37from .results import RESULT_SUCCESS, RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE
38from ..extend import ExtendedOperationsRoot
39from .pooling import ServerPool
40from .server import Server
41from ..operation.abandon import abandon_operation, abandon_request_to_dict
42from ..operation.add import add_operation, add_request_to_dict
43from ..operation.bind import bind_operation, bind_request_to_dict
44from ..operation.compare import compare_operation, compare_request_to_dict
45from ..operation.delete import delete_operation, delete_request_to_dict
46from ..operation.extended import extended_operation, extended_request_to_dict
47from ..operation.modify import modify_operation, modify_request_to_dict
48from ..operation.modifyDn import modify_dn_operation, modify_dn_request_to_dict
49from ..operation.search import search_operation, search_request_to_dict
50from ..protocol.rfc2849 import operation_to_ldif, add_ldif_header
51from ..protocol.sasl.digestMd5 import sasl_digest_md5
52from ..protocol.sasl.external import sasl_external
53from ..protocol.sasl.plain import sasl_plain
54from ..strategy.sync import SyncStrategy
55from ..strategy.safeSync import SafeSyncStrategy
56from ..strategy.mockAsync import MockAsyncStrategy
57from ..strategy.asynchronous import AsyncStrategy
58from ..strategy.reusable import ReusableStrategy
59from ..strategy.restartable import RestartableStrategy
60from ..strategy.ldifProducer import LdifProducerStrategy
61from ..strategy.mockSync import MockSyncStrategy
62from ..strategy.asyncStream import AsyncStreamStrategy
63from ..operation.unbind import unbind_operation
64from ..protocol.rfc2696 import paged_search_control
65from .usage import ConnectionUsage
66from .tls import Tls
67from .exceptions import LDAPUnknownStrategyError, LDAPBindError, LDAPUnknownAuthenticationMethodError, \
68    LDAPSASLMechanismNotSupportedError, LDAPObjectClassError, LDAPConnectionIsReadOnlyError, LDAPChangeError, LDAPExceptionError, \
69    LDAPObjectError, LDAPSocketReceiveError, LDAPAttributeError, LDAPInvalidValueError, LDAPInvalidPortError, LDAPStartTLSError
70
71from ..utils.conv import escape_bytes, prepare_for_stream, check_json_dict, format_json, to_unicode
72from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED, get_library_log_hide_sensitive_data
73from ..utils.dn import safe_dn
74from ..utils.port_validators import check_port_and_port_list
75
76
77SASL_AVAILABLE_MECHANISMS = [EXTERNAL,
78                             DIGEST_MD5,
79                             GSSAPI,
80                             PLAIN]
81
82CLIENT_STRATEGIES = [SYNC,
83                     SAFE_SYNC,
84                     ASYNC,
85                     LDIF,
86                     RESTARTABLE,
87                     REUSABLE,
88                     MOCK_SYNC,
89                     MOCK_ASYNC,
90                     ASYNC_STREAM]
91
92
93def _format_socket_endpoint(endpoint):
94    if endpoint and len(endpoint) == 2:  # IPv4
95        return str(endpoint[0]) + ':' + str(endpoint[1])
96    elif endpoint and len(endpoint) == 4:  # IPv6
97        return '[' + str(endpoint[0]) + ']:' + str(endpoint[1])
98
99    try:
100        return str(endpoint)
101    except Exception:
102        return '?'
103
104
105def _format_socket_endpoints(sock):
106    if sock:
107        try:
108            local = sock.getsockname()
109        except Exception:
110            local = (None, None, None, None)
111        try:
112            remote = sock.getpeername()
113        except Exception:
114            remote = (None, None, None, None)
115
116        return '<local: ' + _format_socket_endpoint(local) + ' - remote: ' + _format_socket_endpoint(remote) + '>'
117    return '<no socket>'
118
119
120class Connection(object):
121    """Main ldap connection class.
122
123    Controls, if used, must be a list of tuples. Each tuple must have 3
124    elements, the control OID, a boolean meaning if the control is
125    critical, a value.
126
127    If the boolean is set to True the server must honor the control or
128    refuse the operation
129
130    Mixing controls must be defined in controls specification (as per
131    RFC 4511)
132
133    :param server: the Server object to connect to
134    :type server: Server, str
135    :param user: the user name for simple authentication
136    :type user: str
137    :param password: the password for simple authentication
138    :type password: str
139    :param auto_bind: specify if the bind will be performed automatically when defining the Connection object
140    :type auto_bind: int, can be one of AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND, AUTO_BIND_TLS_AFTER_BIND as specified in ldap3
141    :param version: LDAP version, default to 3
142    :type version: int
143    :param authentication: type of authentication
144    :type authentication: int, can be one of AUTH_ANONYMOUS, AUTH_SIMPLE or AUTH_SASL, as specified in ldap3
145    :param client_strategy: communication strategy used in the Connection
146    :type client_strategy: can be one of STRATEGY_SYNC, STRATEGY_ASYNC_THREADED, STRATEGY_LDIF_PRODUCER, STRATEGY_SYNC_RESTARTABLE, STRATEGY_REUSABLE_THREADED as specified in ldap3
147    :param auto_referrals: specify if the connection object must automatically follow referrals
148    :type auto_referrals: bool
149    :param sasl_mechanism: mechanism for SASL authentication, can be one of 'EXTERNAL', 'DIGEST-MD5', 'GSSAPI', 'PLAIN'
150    :type sasl_mechanism: str
151    :param sasl_credentials: credentials for SASL mechanism
152    :type sasl_credentials: tuple
153    :param check_names: if True the library will check names of attributes and object classes against the schema. Also values found in entries will be formatted as indicated by the schema
154    :type check_names: bool
155    :param collect_usage: collect usage metrics in the usage attribute
156    :type collect_usage: bool
157    :param read_only: disable operations that modify data in the LDAP server
158    :type read_only: bool
159    :param lazy: open and bind the connection only when an actual operation is performed
160    :type lazy: bool
161    :param raise_exceptions: raise exceptions when operations are not successful, if False operations return False if not successful but not raise exceptions
162    :type raise_exceptions: bool
163    :param pool_name: pool name for pooled strategies
164    :type pool_name: str
165    :param pool_size: pool size for pooled strategies
166    :type pool_size: int
167    :param pool_lifetime: pool lifetime for pooled strategies
168    :type pool_lifetime: int
169    :param cred_store: credential store for gssapi
170    :type cred_store: dict
171    :param use_referral_cache: keep referral connections open and reuse them
172    :type use_referral_cache: bool
173    :param auto_escape: automatic escaping of filter values
174    :type auto_escape: bool
175    :param auto_encode: automatic encoding of attribute values
176    :type auto_encode: bool
177    :param source_address: the ip address or hostname to use as the source when opening the connection to the server
178    :type source_address: str
179    :param source_port: the source port to use when opening the connection to the server. Cannot be specified with source_port_list
180    :type source_port: int
181    :param source_port_list: a list of source ports to choose from when opening the connection to the server. Cannot be specified with source_port
182    :type source_port_list: list
183    """
184    def __init__(self,
185                 server,
186                 user=None,
187                 password=None,
188                 auto_bind=AUTO_BIND_DEFAULT,
189                 version=3,
190                 authentication=None,
191                 client_strategy=SYNC,
192                 auto_referrals=True,
193                 auto_range=True,
194                 sasl_mechanism=None,
195                 sasl_credentials=None,
196                 check_names=True,
197                 collect_usage=False,
198                 read_only=False,
199                 lazy=False,
200                 raise_exceptions=False,
201                 pool_name=None,
202                 pool_size=None,
203                 pool_lifetime=None,
204                 cred_store=None,
205                 fast_decoder=True,
206                 receive_timeout=None,
207                 return_empty_attributes=True,
208                 use_referral_cache=False,
209                 auto_escape=True,
210                 auto_encode=True,
211                 pool_keepalive=None,
212                 source_address=None,
213                 source_port=None,
214                 source_port_list=None):
215
216        conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME')
217        self.connection_lock = RLock()  # re-entrant lock to ensure that operations in the Connection object are executed atomically in the same thread
218        with self.connection_lock:
219            if client_strategy not in CLIENT_STRATEGIES:
220                self.last_error = 'unknown client connection strategy'
221                if log_enabled(ERROR):
222                    log(ERROR, '%s for <%s>', self.last_error, self)
223                raise LDAPUnknownStrategyError(self.last_error)
224
225            self.strategy_type = client_strategy
226            self.user = user
227            self.password = password
228
229            if not authentication and self.user:
230                self.authentication = SIMPLE
231            elif not authentication:
232                self.authentication = ANONYMOUS
233            elif authentication in [SIMPLE, ANONYMOUS, SASL, NTLM]:
234                self.authentication = authentication
235            else:
236                self.last_error = 'unknown authentication method'
237                if log_enabled(ERROR):
238                    log(ERROR, '%s for <%s>', self.last_error, self)
239                raise LDAPUnknownAuthenticationMethodError(self.last_error)
240
241            self.version = version
242            self.auto_referrals = True if auto_referrals else False
243            self.request = None
244            self.response = None
245            self.result = None
246            self.bound = False
247            self.listening = False
248            self.closed = True
249            self.last_error = None
250            if auto_bind is False:  # compatibility with older version where auto_bind was a boolean
251                self.auto_bind = AUTO_BIND_DEFAULT
252            elif auto_bind is True:
253                self.auto_bind = AUTO_BIND_NO_TLS
254            else:
255                self.auto_bind = auto_bind
256            self.sasl_mechanism = sasl_mechanism
257            self.sasl_credentials = sasl_credentials
258            self._usage = ConnectionUsage() if collect_usage else None
259            self.socket = None
260            self.tls_started = False
261            self.sasl_in_progress = False
262            self.read_only = read_only
263            self._context_state = []
264            self._deferred_open = False
265            self._deferred_bind = False
266            self._deferred_start_tls = False
267            self._bind_controls = None
268            self._executing_deferred = False
269            self.lazy = lazy
270            self.pool_name = pool_name if pool_name else conf_default_pool_name
271            self.pool_size = pool_size
272            self.cred_store = cred_store
273            self.pool_lifetime = pool_lifetime
274            self.pool_keepalive = pool_keepalive
275            self.starting_tls = False
276            self.check_names = check_names
277            self.raise_exceptions = raise_exceptions
278            self.auto_range = True if auto_range else False
279            self.extend = ExtendedOperationsRoot(self)
280            self._entries = []
281            self.fast_decoder = fast_decoder
282            self.receive_timeout = receive_timeout
283            self.empty_attributes = return_empty_attributes
284            self.use_referral_cache = use_referral_cache
285            self.auto_escape = auto_escape
286            self.auto_encode = auto_encode
287
288            port_err = check_port_and_port_list(source_port, source_port_list)
289            if port_err:
290                if log_enabled(ERROR):
291                    log(ERROR, port_err)
292                raise LDAPInvalidPortError(port_err)
293            # using an empty string to bind a socket means "use the default as if this wasn't provided" because socket
294            # binding requires that you pass something for the ip if you want to pass a specific port
295            self.source_address = source_address if source_address is not None else ''
296            # using 0 as the source port to bind a socket means "use the default behavior of picking a random port from
297            # all ports as if this wasn't provided" because socket binding requires that you pass something for the port
298            # if you want to pass a specific ip
299            self.source_port_list = [0]
300            if source_port is not None:
301                self.source_port_list = [source_port]
302            elif source_port_list is not None:
303                self.source_port_list = source_port_list[:]
304
305            if isinstance(server, STRING_TYPES):
306                server = Server(server)
307            if isinstance(server, SEQUENCE_TYPES):
308                server = ServerPool(server, ROUND_ROBIN, active=True, exhaust=True)
309
310            if isinstance(server, ServerPool):
311                self.server_pool = server
312                self.server_pool.initialize(self)
313                self.server = self.server_pool.get_current_server(self)
314            else:
315                self.server_pool = None
316                self.server = server
317
318            # if self.authentication == SIMPLE and self.user and self.check_names:
319            #     self.user = safe_dn(self.user)
320            #     if log_enabled(EXTENDED):
321            #         log(EXTENDED, 'user name sanitized to <%s> for simple authentication via <%s>', self.user, self)
322
323            if self.strategy_type == SYNC:
324                self.strategy = SyncStrategy(self)
325            elif self.strategy_type == SAFE_SYNC:
326                self.strategy = SafeSyncStrategy(self)
327            elif self.strategy_type == ASYNC:
328                self.strategy = AsyncStrategy(self)
329            elif self.strategy_type == LDIF:
330                self.strategy = LdifProducerStrategy(self)
331            elif self.strategy_type == RESTARTABLE:
332                self.strategy = RestartableStrategy(self)
333            elif self.strategy_type == REUSABLE:
334                self.strategy = ReusableStrategy(self)
335                self.lazy = False
336            elif self.strategy_type == MOCK_SYNC:
337                self.strategy = MockSyncStrategy(self)
338            elif self.strategy_type == MOCK_ASYNC:
339                self.strategy = MockAsyncStrategy(self)
340            elif self.strategy_type == ASYNC_STREAM:
341                self.strategy = AsyncStreamStrategy(self)
342            else:
343                self.last_error = 'unknown strategy'
344                if log_enabled(ERROR):
345                    log(ERROR, '%s for <%s>', self.last_error, self)
346                raise LDAPUnknownStrategyError(self.last_error)
347
348            # maps strategy functions to connection functions
349            self.send = self.strategy.send
350            self.open = self.strategy.open
351            self.get_response = self.strategy.get_response
352            self.post_send_single_response = self.strategy.post_send_single_response
353            self.post_send_search = self.strategy.post_send_search
354
355            if not self.strategy.no_real_dsa:
356                self._do_auto_bind()
357            # else:  # for strategies with a fake server set get_info to NONE if server hasn't a schema
358            #     if self.server and not self.server.schema:
359            #         self.server.get_info = NONE
360            if log_enabled(BASIC):
361                if get_library_log_hide_sensitive_data():
362                    log(BASIC, 'instantiated Connection: <%s>', self.repr_with_sensitive_data_stripped())
363                else:
364                    log(BASIC, 'instantiated Connection: <%r>', self)
365
366    def _prepare_return_value(self, status, response=False):
367        if self.strategy.thread_safe:
368            temp_response = self.response
369            self.response = None
370            temp_request = self.request
371            self.request = None
372            return status, deepcopy(self.result), deepcopy(temp_response) if response else None, copy(temp_request)
373        return status
374
375    def _do_auto_bind(self):
376        if self.auto_bind and self.auto_bind not in [AUTO_BIND_NONE, AUTO_BIND_DEFAULT]:
377            if log_enabled(BASIC):
378                log(BASIC, 'performing automatic bind for <%s>', self)
379            if self.closed:
380                self.open(read_server_info=False)
381            if self.auto_bind == AUTO_BIND_NO_TLS:
382                self.bind(read_server_info=True)
383            elif self.auto_bind == AUTO_BIND_TLS_BEFORE_BIND:
384                if self.start_tls(read_server_info=False):
385                    self.bind(read_server_info=True)
386                else:
387                    error = 'automatic start_tls befored bind not successful' + (' - ' + self.last_error if self.last_error else '')
388                    if log_enabled(ERROR):
389                        log(ERROR, '%s for <%s>', error, self)
390                    self.unbind()  # unbind anyway to close connection
391                    raise LDAPStartTLSError(error)
392            elif self.auto_bind == AUTO_BIND_TLS_AFTER_BIND:
393                self.bind(read_server_info=False)
394                if not self.start_tls(read_server_info=True):
395                    error = 'automatic start_tls after bind not successful' + (' - ' + self.last_error if self.last_error else '')
396                    if log_enabled(ERROR):
397                        log(ERROR, '%s for <%s>', error, self)
398                    self.unbind()
399                    raise LDAPStartTLSError(error)
400            if not self.bound:
401                error = 'automatic bind not successful' + (' - ' + self.last_error if self.last_error else '')
402                if log_enabled(ERROR):
403                    log(ERROR, '%s for <%s>', error, self)
404                self.unbind()
405                raise LDAPBindError(error)
406
407    def __str__(self):
408        s = [
409            str(self.server) if self.server else 'None',
410            'user: ' + str(self.user),
411            'lazy' if self.lazy else 'not lazy',
412            'unbound' if not self.bound else ('deferred bind' if self._deferred_bind else 'bound'),
413            'closed' if self.closed else ('deferred open' if self._deferred_open else 'open'),
414            _format_socket_endpoints(self.socket),
415            'tls not started' if not self.tls_started else('deferred start_tls' if self._deferred_start_tls else 'tls started'),
416            'listening' if self.listening else 'not listening',
417            self.strategy.__class__.__name__ if hasattr(self, 'strategy') else 'No strategy',
418            'internal decoder' if self.fast_decoder else 'pyasn1 decoder'
419        ]
420        return ' - '.join(s)
421
422    def __repr__(self):
423        conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME')
424        if self.server_pool:
425            r = 'Connection(server={0.server_pool!r}'.format(self)
426        else:
427            r = 'Connection(server={0.server!r}'.format(self)
428        r += '' if self.user is None else ', user={0.user!r}'.format(self)
429        r += '' if self.password is None else ', password={0.password!r}'.format(self)
430        r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self)
431        r += '' if self.version is None else ', version={0.version!r}'.format(self)
432        r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self)
433        r += '' if self.strategy_type is None else ', client_strategy={0.strategy_type!r}'.format(self)
434        r += '' if self.auto_referrals is None else ', auto_referrals={0.auto_referrals!r}'.format(self)
435        r += '' if self.sasl_mechanism is None else ', sasl_mechanism={0.sasl_mechanism!r}'.format(self)
436        r += '' if self.sasl_credentials is None else ', sasl_credentials={0.sasl_credentials!r}'.format(self)
437        r += '' if self.check_names is None else ', check_names={0.check_names!r}'.format(self)
438        r += '' if self.usage is None else (', collect_usage=' + ('True' if self.usage else 'False'))
439        r += '' if self.read_only is None else ', read_only={0.read_only!r}'.format(self)
440        r += '' if self.lazy is None else ', lazy={0.lazy!r}'.format(self)
441        r += '' if self.raise_exceptions is None else ', raise_exceptions={0.raise_exceptions!r}'.format(self)
442        r += '' if (self.pool_name is None or self.pool_name == conf_default_pool_name) else ', pool_name={0.pool_name!r}'.format(self)
443        r += '' if self.pool_size is None else ', pool_size={0.pool_size!r}'.format(self)
444        r += '' if self.pool_lifetime is None else ', pool_lifetime={0.pool_lifetime!r}'.format(self)
445        r += '' if self.pool_keepalive is None else ', pool_keepalive={0.pool_keepalive!r}'.format(self)
446        r += '' if self.cred_store is None else (', cred_store=' + repr(self.cred_store))
447        r += '' if self.fast_decoder is None else (', fast_decoder=' + ('True' if self.fast_decoder else 'False'))
448        r += '' if self.auto_range is None else (', auto_range=' + ('True' if self.auto_range else 'False'))
449        r += '' if self.receive_timeout is None else ', receive_timeout={0.receive_timeout!r}'.format(self)
450        r += '' if self.empty_attributes is None else (', return_empty_attributes=' + ('True' if self.empty_attributes else 'False'))
451        r += '' if self.auto_encode is None else (', auto_encode=' + ('True' if self.auto_encode else 'False'))
452        r += '' if self.auto_escape is None else (', auto_escape=' + ('True' if self.auto_escape else 'False'))
453        r += '' if self.use_referral_cache is None else (', use_referral_cache=' + ('True' if self.use_referral_cache else 'False'))
454        r += ')'
455
456        return r
457
458    def repr_with_sensitive_data_stripped(self):
459        conf_default_pool_name = get_config_parameter('DEFAULT_THREADED_POOL_NAME')
460        if self.server_pool:
461            r = 'Connection(server={0.server_pool!r}'.format(self)
462        else:
463            r = 'Connection(server={0.server!r}'.format(self)
464        r += '' if self.user is None else ', user={0.user!r}'.format(self)
465        r += '' if self.password is None else ", password='{0}'".format('<stripped %d characters of sensitive data>' % len(self.password))
466        r += '' if self.auto_bind is None else ', auto_bind={0.auto_bind!r}'.format(self)
467        r += '' if self.version is None else ', version={0.version!r}'.format(self)
468        r += '' if self.authentication is None else ', authentication={0.authentication!r}'.format(self)
469        r += '' if self.strategy_type is None else ', client_strategy={0.strategy_type!r}'.format(self)
470        r += '' if self.auto_referrals is None else ', auto_referrals={0.auto_referrals!r}'.format(self)
471        r += '' if self.sasl_mechanism is None else ', sasl_mechanism={0.sasl_mechanism!r}'.format(self)
472        if self.sasl_mechanism == DIGEST_MD5:
473            r += '' if self.sasl_credentials is None else ", sasl_credentials=({0!r}, {1!r}, '{2}', {3!r})".format(self.sasl_credentials[0], self.sasl_credentials[1], '*' * len(self.sasl_credentials[2]), self.sasl_credentials[3])
474        else:
475            r += '' if self.sasl_credentials is None else ', sasl_credentials={0.sasl_credentials!r}'.format(self)
476        r += '' if self.check_names is None else ', check_names={0.check_names!r}'.format(self)
477        r += '' if self.usage is None else (', collect_usage=' + 'True' if self.usage else 'False')
478        r += '' if self.read_only is None else ', read_only={0.read_only!r}'.format(self)
479        r += '' if self.lazy is None else ', lazy={0.lazy!r}'.format(self)
480        r += '' if self.raise_exceptions is None else ', raise_exceptions={0.raise_exceptions!r}'.format(self)
481        r += '' if (self.pool_name is None or self.pool_name == conf_default_pool_name) else ', pool_name={0.pool_name!r}'.format(self)
482        r += '' if self.pool_size is None else ', pool_size={0.pool_size!r}'.format(self)
483        r += '' if self.pool_lifetime is None else ', pool_lifetime={0.pool_lifetime!r}'.format(self)
484        r += '' if self.pool_keepalive is None else ', pool_keepalive={0.pool_keepalive!r}'.format(self)
485        r += '' if self.cred_store is None else (', cred_store=' + repr(self.cred_store))
486        r += '' if self.fast_decoder is None else (', fast_decoder=' + 'True' if self.fast_decoder else 'False')
487        r += '' if self.auto_range is None else (', auto_range=' + ('True' if self.auto_range else 'False'))
488        r += '' if self.receive_timeout is None else ', receive_timeout={0.receive_timeout!r}'.format(self)
489        r += '' if self.empty_attributes is None else (', return_empty_attributes=' + 'True' if self.empty_attributes else 'False')
490        r += '' if self.auto_encode is None else (', auto_encode=' + ('True' if self.auto_encode else 'False'))
491        r += '' if self.auto_escape is None else (', auto_escape=' + ('True' if self.auto_escape else 'False'))
492        r += '' if self.use_referral_cache is None else (', use_referral_cache=' + ('True' if self.use_referral_cache else 'False'))
493        r += ')'
494
495        return r
496
497    @property
498    def stream(self):
499        """Used by the LDIFProducer strategy to accumulate the ldif-change operations with a single LDIF header
500        :return: reference to the response stream if defined in the strategy.
501        """
502        return self.strategy.get_stream() if self.strategy.can_stream else None
503
504    @stream.setter
505    def stream(self, value):
506        with self.connection_lock:
507            if self.strategy.can_stream:
508                self.strategy.set_stream(value)
509
510    @property
511    def usage(self):
512        """Usage statistics for the connection.
513        :return: Usage object
514        """
515        if not self._usage:
516            return None
517        if self.strategy.pooled:  # update master connection usage from pooled connections
518            self._usage.reset()
519            for worker in self.strategy.pool.workers:
520                self._usage += worker.connection.usage
521            self._usage += self.strategy.pool.terminated_usage
522        return self._usage
523
524    def __enter__(self):
525        with self.connection_lock:
526            self._context_state.append((self.bound, self.closed))  # save status out of context as a tuple in a list
527            if self.auto_bind != AUTO_BIND_NONE:
528                if self.auto_bind == AUTO_BIND_DEFAULT:
529                    self.auto_bind = AUTO_BIND_NO_TLS
530                if self.closed:
531                    self.open()
532                if not self.bound:
533                    if not self.bind():
534                        raise LDAPBindError('unable to bind')
535
536            return self
537
538    def __exit__(self, exc_type, exc_val, exc_tb):
539        with self.connection_lock:
540            context_bound, context_closed = self._context_state.pop()
541            if (not context_bound and self.bound) or self.stream:  # restore status prior to entering context
542                try:
543                    self.unbind()
544                except LDAPExceptionError:
545                    pass
546
547            if not context_closed and self.closed:
548                self.open()
549
550            if exc_type is not None:
551                if log_enabled(ERROR):
552                    log(ERROR, '%s for <%s>', exc_type, self)
553                return False  # re-raise LDAPExceptionError
554
555    def bind(self,
556             read_server_info=True,
557             controls=None):
558        """Bind to ldap Server with the authentication method and the user defined in the connection
559
560        :param read_server_info: reads info from server
561        :param controls: LDAP controls to send along with the bind operation
562        :type controls: list of tuple
563        :return: bool
564
565        """
566        if log_enabled(BASIC):
567            log(BASIC, 'start BIND operation via <%s>', self)
568        self.last_error = None
569        with self.connection_lock:
570            if self.lazy and not self._executing_deferred:
571                if self.strategy.pooled:
572                    self.strategy.validate_bind(controls)
573                self._deferred_bind = True
574                self._bind_controls = controls
575                self.bound = True
576                if log_enabled(BASIC):
577                    log(BASIC, 'deferring bind for <%s>', self)
578            else:
579                self._deferred_bind = False
580                self._bind_controls = None
581                if self.closed:  # try to open connection if closed
582                    self.open(read_server_info=False)
583                if self.authentication == ANONYMOUS:
584                    if log_enabled(PROTOCOL):
585                        log(PROTOCOL, 'performing anonymous BIND for <%s>', self)
586                    if not self.strategy.pooled:
587                        request = bind_operation(self.version, self.authentication, self.user, '', auto_encode=self.auto_encode)
588                        if log_enabled(PROTOCOL):
589                            log(PROTOCOL, 'anonymous BIND request <%s> sent via <%s>', bind_request_to_dict(request), self)
590                        response = self.post_send_single_response(self.send('bindRequest', request, controls))
591                    else:
592                        response = self.strategy.validate_bind(controls)  # only for REUSABLE
593                elif self.authentication == SIMPLE:
594                    if log_enabled(PROTOCOL):
595                        log(PROTOCOL, 'performing simple BIND for <%s>', self)
596                    if not self.strategy.pooled:
597                        request = bind_operation(self.version, self.authentication, self.user, self.password, auto_encode=self.auto_encode)
598                        if log_enabled(PROTOCOL):
599                            log(PROTOCOL, 'simple BIND request <%s> sent via <%s>', bind_request_to_dict(request), self)
600                        response = self.post_send_single_response(self.send('bindRequest', request, controls))
601                    else:
602                        response = self.strategy.validate_bind(controls)  # only for REUSABLE
603                elif self.authentication == SASL:
604                    if self.sasl_mechanism in SASL_AVAILABLE_MECHANISMS:
605                        if log_enabled(PROTOCOL):
606                            log(PROTOCOL, 'performing SASL BIND for <%s>', self)
607                        if not self.strategy.pooled:
608                            response = self.do_sasl_bind(controls)
609                        else:
610                            response = self.strategy.validate_bind(controls)  # only for REUSABLE
611                    else:
612                        self.last_error = 'requested SASL mechanism not supported'
613                        if log_enabled(ERROR):
614                            log(ERROR, '%s for <%s>', self.last_error, self)
615                        raise LDAPSASLMechanismNotSupportedError(self.last_error)
616                elif self.authentication == NTLM:
617                    if self.user and self.password and len(self.user.split('\\')) == 2:
618                        if log_enabled(PROTOCOL):
619                            log(PROTOCOL, 'performing NTLM BIND for <%s>', self)
620                        if not self.strategy.pooled:
621                            response = self.do_ntlm_bind(controls)
622                        else:
623                            response = self.strategy.validate_bind(controls)  # only for REUSABLE
624                    else:  # user or password missing
625                        self.last_error = 'NTLM needs domain\\username and a password'
626                        if log_enabled(ERROR):
627                            log(ERROR, '%s for <%s>', self.last_error, self)
628                        raise LDAPUnknownAuthenticationMethodError(self.last_error)
629                else:
630                    self.last_error = 'unknown authentication method'
631                    if log_enabled(ERROR):
632                        log(ERROR, '%s for <%s>', self.last_error, self)
633                    raise LDAPUnknownAuthenticationMethodError(self.last_error)
634
635                if not self.strategy.sync and not self.strategy.pooled and self.authentication not in (SASL, NTLM):  # get response if asynchronous except for SASL and NTLM that return the bind result even for asynchronous strategy
636                    _, result = self.get_response(response)
637                    if log_enabled(PROTOCOL):
638                        log(PROTOCOL, 'async BIND response id <%s> received via <%s>', result, self)
639                elif self.strategy.sync:
640                    result = self.result
641                    if log_enabled(PROTOCOL):
642                        log(PROTOCOL, 'BIND response <%s> received via <%s>', result, self)
643                elif self.strategy.pooled or self.authentication in (SASL, NTLM):  # asynchronous SASL and NTLM or reusable strtegy get the bind result synchronously
644                    result = response
645                else:
646                    self.last_error = 'unknown authentication method'
647                    if log_enabled(ERROR):
648                        log(ERROR, '%s for <%s>', self.last_error, self)
649                    raise LDAPUnknownAuthenticationMethodError(self.last_error)
650
651                if result is None:
652                    # self.bound = True if self.strategy_type == REUSABLE else False
653                    self.bound = False
654                elif result is True:
655                    self.bound = True
656                elif result is False:
657                    self.bound = False
658                else:
659                    self.bound = True if result['result'] == RESULT_SUCCESS else False
660                    if not self.bound and result and result['description'] and not self.last_error:
661                        self.last_error = result['description']
662
663                if read_server_info and self.bound:
664                    self.refresh_server_info()
665            self._entries = []
666
667            if log_enabled(BASIC):
668                log(BASIC, 'done BIND operation, result <%s>', self.bound)
669
670            return self._prepare_return_value(self.bound, self.result)
671
672    def rebind(self,
673               user=None,
674               password=None,
675               authentication=None,
676               sasl_mechanism=None,
677               sasl_credentials=None,
678               read_server_info=True,
679               controls=None
680               ):
681
682        if log_enabled(BASIC):
683            log(BASIC, 'start (RE)BIND operation via <%s>', self)
684        self.last_error = None
685        with self.connection_lock:
686            if user:
687                self.user = user
688            if password is not None:
689                self.password = password
690            if not authentication and user:
691                self.authentication = SIMPLE
692            if authentication in [SIMPLE, ANONYMOUS, SASL, NTLM]:
693                self.authentication = authentication
694            elif authentication is not None:
695                self.last_error = 'unknown authentication method'
696                if log_enabled(ERROR):
697                    log(ERROR, '%s for <%s>', self.last_error, self)
698                raise LDAPUnknownAuthenticationMethodError(self.last_error)
699            if sasl_mechanism:
700                self.sasl_mechanism = sasl_mechanism
701            if sasl_credentials:
702                self.sasl_credentials = sasl_credentials
703
704            # if self.authentication == SIMPLE and self.user and self.check_names:
705            #     self.user = safe_dn(self.user)
706            #     if log_enabled(EXTENDED):
707            #         log(EXTENDED, 'user name sanitized to <%s> for rebind via <%s>', self.user, self)
708
709            if not self.strategy.pooled:
710                try:
711                    return self.bind(read_server_info, controls)
712                except LDAPSocketReceiveError:
713                    self.last_error = 'Unable to rebind as a different user, furthermore the server abruptly closed the connection'
714                    if log_enabled(ERROR):
715                        log(ERROR, '%s for <%s>', self.last_error, self)
716                    raise LDAPBindError(self.last_error)
717            else:
718                self.strategy.pool.rebind_pool()
719                return self._prepare_return_value(True, self.result)
720
721    def unbind(self,
722               controls=None):
723        """Unbind the connected user. Unbind implies closing session as per RFC4511 (4.3)
724
725        :param controls: LDAP controls to send along with the bind operation
726
727        """
728        if log_enabled(BASIC):
729            log(BASIC, 'start UNBIND operation via <%s>', self)
730
731        if self.use_referral_cache:
732            self.strategy.unbind_referral_cache()
733
734        self.last_error = None
735        with self.connection_lock:
736            if self.lazy and not self._executing_deferred and (self._deferred_bind or self._deferred_open):  # _clear deferred status
737                self.strategy.close()
738                self._deferred_open = False
739                self._deferred_bind = False
740                self._deferred_start_tls = False
741            elif not self.closed:
742                request = unbind_operation()
743                if log_enabled(PROTOCOL):
744                    log(PROTOCOL, 'UNBIND request sent via <%s>', self)
745                self.send('unbindRequest', request, controls)
746                self.strategy.close()
747
748            if log_enabled(BASIC):
749                log(BASIC, 'done UNBIND operation, result <%s>', True)
750
751            return self._prepare_return_value(True)
752
753    def search(self,
754               search_base,
755               search_filter,
756               search_scope=SUBTREE,
757               dereference_aliases=DEREF_ALWAYS,
758               attributes=None,
759               size_limit=0,
760               time_limit=0,
761               types_only=False,
762               get_operational_attributes=False,
763               controls=None,
764               paged_size=None,
765               paged_criticality=False,
766               paged_cookie=None,
767               auto_escape=None):
768        """
769        Perform an ldap search:
770
771        - If attributes is empty noRFC2696 with the specified size
772        - If paged is 0 and cookie is present the search is abandoned on
773          server attribute is returned
774        - If attributes is ALL_ATTRIBUTES all attributes are returned
775        - If paged_size is an int greater than 0 a simple paged search
776          is tried as described in
777        - Cookie is an opaque string received in the last paged search
778          and must be used on the next paged search response
779        - If lazy == True open and bind will be deferred until another
780          LDAP operation is performed
781        - If mssing_attributes == True then an attribute not returned by the server is set to None
782        - If auto_escape is set it overrides the Connection auto_escape
783        """
784        conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
785        if log_enabled(BASIC):
786            log(BASIC, 'start SEARCH operation via <%s>', self)
787
788        if self.check_names and search_base:
789            search_base = safe_dn(search_base)
790            if log_enabled(EXTENDED):
791                log(EXTENDED, 'search base sanitized to <%s> for SEARCH operation via <%s>', search_base, self)
792
793        with self.connection_lock:
794            self._fire_deferred()
795            if not attributes:
796                attributes = [NO_ATTRIBUTES]
797            elif attributes == ALL_ATTRIBUTES:
798                attributes = [ALL_ATTRIBUTES]
799
800            if isinstance(attributes, STRING_TYPES):
801                attributes = [attributes]
802
803            if get_operational_attributes and isinstance(attributes, list):
804                attributes.append(ALL_OPERATIONAL_ATTRIBUTES)
805            elif get_operational_attributes and isinstance(attributes, tuple):
806                attributes += (ALL_OPERATIONAL_ATTRIBUTES, )  # concatenate tuple
807
808            if isinstance(paged_size, int):
809                if log_enabled(PROTOCOL):
810                    log(PROTOCOL, 'performing paged search for %d items with cookie <%s> for <%s>', paged_size, escape_bytes(paged_cookie), self)
811
812                if controls is None:
813                    controls = []
814                else:
815                    # Copy the controls to prevent modifying the original object
816                    controls = list(controls)
817                controls.append(paged_search_control(paged_criticality, paged_size, paged_cookie))
818
819            if self.server and self.server.schema and self.check_names:
820                for attribute_name in attributes:
821                    if ';' in attribute_name:  # remove tags
822                        attribute_name_to_check = attribute_name.split(';')[0]
823                    else:
824                        attribute_name_to_check = attribute_name
825                    if self.server.schema and attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types:
826                        self.last_error = 'invalid attribute type ' + attribute_name_to_check
827                        if log_enabled(ERROR):
828                            log(ERROR, '%s for <%s>', self.last_error, self)
829                        raise LDAPAttributeError(self.last_error)
830
831            request = search_operation(search_base,
832                                       search_filter,
833                                       search_scope,
834                                       dereference_aliases,
835                                       attributes,
836                                       size_limit,
837                                       time_limit,
838                                       types_only,
839                                       self.auto_escape if auto_escape is None else auto_escape,
840                                       self.auto_encode,
841                                       self.server.schema if self.server else None,
842                                       validator=self.server.custom_validator,
843                                       check_names=self.check_names)
844            if log_enabled(PROTOCOL):
845                log(PROTOCOL, 'SEARCH request <%s> sent via <%s>', search_request_to_dict(request), self)
846            response = self.post_send_search(self.send('searchRequest', request, controls))
847            self._entries = []
848
849            if isinstance(response, int):  # asynchronous strategy
850                return_value = response
851                if log_enabled(PROTOCOL):
852                    log(PROTOCOL, 'async SEARCH response id <%s> received via <%s>', return_value, self)
853            else:
854                return_value = True if self.result['type'] == 'searchResDone' and len(response) > 0 else False
855                if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
856                    self.last_error = self.result['description']
857
858                if log_enabled(PROTOCOL):
859                    for entry in response:
860                        if entry['type'] == 'searchResEntry':
861                            log(PROTOCOL, 'SEARCH response entry <%s> received via <%s>', entry, self)
862                        elif entry['type'] == 'searchResRef':
863                            log(PROTOCOL, 'SEARCH response reference <%s> received via <%s>', entry, self)
864
865            if log_enabled(BASIC):
866                log(BASIC, 'done SEARCH operation, result <%s>', return_value)
867
868            return self._prepare_return_value(return_value, response=True)
869
870    def compare(self,
871                dn,
872                attribute,
873                value,
874                controls=None):
875        """
876        Perform a compare operation
877        """
878        conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
879
880        if log_enabled(BASIC):
881            log(BASIC, 'start COMPARE operation via <%s>', self)
882        self.last_error = None
883        if self.check_names:
884            dn = safe_dn(dn)
885            if log_enabled(EXTENDED):
886                log(EXTENDED, 'dn sanitized to <%s> for COMPARE operation via <%s>', dn, self)
887
888        if self.server and self.server.schema and self.check_names:
889            if ';' in attribute:  # remove tags for checking
890                attribute_name_to_check = attribute.split(';')[0]
891            else:
892                attribute_name_to_check = attribute
893
894            if self.server.schema.attribute_types and attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types:
895                self.last_error = 'invalid attribute type ' + attribute_name_to_check
896                if log_enabled(ERROR):
897                    log(ERROR, '%s for <%s>', self.last_error, self)
898                raise LDAPAttributeError(self.last_error)
899
900        if isinstance(value, SEQUENCE_TYPES):  # value can't be a sequence
901            self.last_error = 'value cannot be a sequence'
902            if log_enabled(ERROR):
903                log(ERROR, '%s for <%s>', self.last_error, self)
904            raise LDAPInvalidValueError(self.last_error)
905
906        with self.connection_lock:
907            self._fire_deferred()
908            request = compare_operation(dn, attribute, value, self.auto_encode, self.server.schema if self.server else None, validator=self.server.custom_validator if self.server else None, check_names=self.check_names)
909            if log_enabled(PROTOCOL):
910                log(PROTOCOL, 'COMPARE request <%s> sent via <%s>', compare_request_to_dict(request), self)
911            response = self.post_send_single_response(self.send('compareRequest', request, controls))
912            self._entries = []
913            if isinstance(response, int):
914                return_value = response
915                if log_enabled(PROTOCOL):
916                    log(PROTOCOL, 'async COMPARE response id <%s> received via <%s>', return_value, self)
917            else:
918                return_value = True if self.result['type'] == 'compareResponse' and self.result['result'] == RESULT_COMPARE_TRUE else False
919                if not return_value and self.result['result'] not in [RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE] and not self.last_error:
920                    self.last_error = self.result['description']
921
922                if log_enabled(PROTOCOL):
923                    log(PROTOCOL, 'COMPARE response <%s> received via <%s>', response, self)
924
925            if log_enabled(BASIC):
926                log(BASIC, 'done COMPARE operation, result <%s>', return_value)
927
928            return self._prepare_return_value(return_value)
929
930    def add(self,
931            dn,
932            object_class=None,
933            attributes=None,
934            controls=None):
935        """
936        Add dn to the DIT, object_class is None, a class name or a list
937        of class names.
938
939        Attributes is a dictionary in the form 'attr': 'val' or 'attr':
940        ['val1', 'val2', ...] for multivalued attributes
941        """
942        conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
943        conf_classes_excluded_from_check = [v.lower() for v in get_config_parameter('CLASSES_EXCLUDED_FROM_CHECK')]
944        if log_enabled(BASIC):
945            log(BASIC, 'start ADD operation via <%s>', self)
946        self.last_error = None
947        _attributes = deepcopy(attributes)  # dict could change when adding objectClass values
948        if self.check_names:
949            dn = safe_dn(dn)
950            if log_enabled(EXTENDED):
951                log(EXTENDED, 'dn sanitized to <%s> for ADD operation via <%s>', dn, self)
952
953        with self.connection_lock:
954            self._fire_deferred()
955            attr_object_class = []
956            if object_class is None:
957                parm_object_class = []
958            else:
959                parm_object_class = list(object_class) if isinstance(object_class, SEQUENCE_TYPES) else [object_class]
960
961            object_class_attr_name = ''
962            if _attributes:
963                for attr in _attributes:
964                    if attr.lower() == 'objectclass':
965                        object_class_attr_name = attr
966                        attr_object_class = list(_attributes[object_class_attr_name]) if isinstance(_attributes[object_class_attr_name], SEQUENCE_TYPES) else [_attributes[object_class_attr_name]]
967                        break
968            else:
969                _attributes = dict()
970
971            if not object_class_attr_name:
972                object_class_attr_name = 'objectClass'
973
974            attr_object_class = [to_unicode(object_class) for object_class in attr_object_class]  # converts objectclass to unicode in case of bytes value
975            _attributes[object_class_attr_name] = reduce(lambda x, y: x + [y] if y not in x else x, parm_object_class + attr_object_class, [])  # remove duplicate ObjectClasses
976
977            if not _attributes[object_class_attr_name]:
978                self.last_error = 'objectClass attribute is mandatory'
979                if log_enabled(ERROR):
980                    log(ERROR, '%s for <%s>', self.last_error, self)
981                raise LDAPObjectClassError(self.last_error)
982
983            if self.server and self.server.schema and self.check_names:
984                for object_class_name in _attributes[object_class_attr_name]:
985                    if object_class_name.lower() not in conf_classes_excluded_from_check and object_class_name not in self.server.schema.object_classes:
986                        self.last_error = 'invalid object class ' + str(object_class_name)
987                        if log_enabled(ERROR):
988                            log(ERROR, '%s for <%s>', self.last_error, self)
989                        raise LDAPObjectClassError(self.last_error)
990
991                for attribute_name in _attributes:
992                    if ';' in attribute_name:  # remove tags for checking
993                        attribute_name_to_check = attribute_name.split(';')[0]
994                    else:
995                        attribute_name_to_check = attribute_name
996
997                    if attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types:
998                        self.last_error = 'invalid attribute type ' + attribute_name_to_check
999                        if log_enabled(ERROR):
1000                            log(ERROR, '%s for <%s>', self.last_error, self)
1001                        raise LDAPAttributeError(self.last_error)
1002
1003            request = add_operation(dn, _attributes, self.auto_encode, self.server.schema if self.server else None, validator=self.server.custom_validator if self.server else None, check_names=self.check_names)
1004            if log_enabled(PROTOCOL):
1005                log(PROTOCOL, 'ADD request <%s> sent via <%s>', add_request_to_dict(request), self)
1006            response = self.post_send_single_response(self.send('addRequest', request, controls))
1007            self._entries = []
1008
1009            if isinstance(response, STRING_TYPES + (int, )):
1010                return_value = response
1011                if log_enabled(PROTOCOL):
1012                    log(PROTOCOL, 'async ADD response id <%s> received via <%s>', return_value, self)
1013            else:
1014                if log_enabled(PROTOCOL):
1015                    log(PROTOCOL, 'ADD response <%s> received via <%s>', response, self)
1016                return_value = True if self.result['type'] == 'addResponse' and self.result['result'] == RESULT_SUCCESS else False
1017                if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
1018                    self.last_error = self.result['description']
1019
1020            if log_enabled(BASIC):
1021                log(BASIC, 'done ADD operation, result <%s>', return_value)
1022
1023            return self._prepare_return_value(return_value)
1024
1025    def delete(self,
1026               dn,
1027               controls=None):
1028        """
1029        Delete the entry identified by the DN from the DIB.
1030        """
1031        if log_enabled(BASIC):
1032            log(BASIC, 'start DELETE operation via <%s>', self)
1033        self.last_error = None
1034        if self.check_names:
1035            dn = safe_dn(dn)
1036            if log_enabled(EXTENDED):
1037                log(EXTENDED, 'dn sanitized to <%s> for DELETE operation via <%s>', dn, self)
1038
1039        with self.connection_lock:
1040            self._fire_deferred()
1041            if self.read_only:
1042                self.last_error = 'connection is read-only'
1043                if log_enabled(ERROR):
1044                    log(ERROR, '%s for <%s>', self.last_error, self)
1045                raise LDAPConnectionIsReadOnlyError(self.last_error)
1046
1047            request = delete_operation(dn)
1048            if log_enabled(PROTOCOL):
1049                log(PROTOCOL, 'DELETE request <%s> sent via <%s>', delete_request_to_dict(request), self)
1050            response = self.post_send_single_response(self.send('delRequest', request, controls))
1051            self._entries = []
1052
1053            if isinstance(response, STRING_TYPES + (int, )):
1054                return_value = response
1055                if log_enabled(PROTOCOL):
1056                    log(PROTOCOL, 'async DELETE response id <%s> received via <%s>', return_value, self)
1057            else:
1058                if log_enabled(PROTOCOL):
1059                    log(PROTOCOL, 'DELETE response <%s> received via <%s>', response, self)
1060                return_value = True if self.result['type'] == 'delResponse' and self.result['result'] == RESULT_SUCCESS else False
1061                if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
1062                    self.last_error = self.result['description']
1063
1064            if log_enabled(BASIC):
1065                log(BASIC, 'done DELETE operation, result <%s>', return_value)
1066
1067            return self._prepare_return_value(return_value)
1068
1069    def modify(self,
1070               dn,
1071               changes,
1072               controls=None):
1073        """
1074        Modify attributes of entry
1075
1076        - changes is a dictionary in the form {'attribute1': change), 'attribute2': [change, change, ...], ...}
1077        - change is (operation, [value1, value2, ...])
1078        - operation is 0 (MODIFY_ADD), 1 (MODIFY_DELETE), 2 (MODIFY_REPLACE), 3 (MODIFY_INCREMENT)
1079        """
1080        conf_attributes_excluded_from_check = [v.lower() for v in get_config_parameter('ATTRIBUTES_EXCLUDED_FROM_CHECK')]
1081
1082        if log_enabled(BASIC):
1083            log(BASIC, 'start MODIFY operation via <%s>', self)
1084        self.last_error = None
1085        if self.check_names:
1086            dn = safe_dn(dn)
1087            if log_enabled(EXTENDED):
1088                log(EXTENDED, 'dn sanitized to <%s> for MODIFY operation via <%s>', dn, self)
1089
1090        with self.connection_lock:
1091            self._fire_deferred()
1092            if self.read_only:
1093                self.last_error = 'connection is read-only'
1094                if log_enabled(ERROR):
1095                    log(ERROR, '%s for <%s>', self.last_error, self)
1096                raise LDAPConnectionIsReadOnlyError(self.last_error)
1097
1098            if not isinstance(changes, dict):
1099                self.last_error = 'changes must be a dictionary'
1100                if log_enabled(ERROR):
1101                    log(ERROR, '%s for <%s>', self.last_error, self)
1102                raise LDAPChangeError(self.last_error)
1103
1104            if not changes:
1105                self.last_error = 'no changes in modify request'
1106                if log_enabled(ERROR):
1107                    log(ERROR, '%s for <%s>', self.last_error, self)
1108                raise LDAPChangeError(self.last_error)
1109
1110            changelist = dict()
1111            for attribute_name in changes:
1112                if self.server and self.server.schema and self.check_names:
1113                    if ';' in attribute_name:  # remove tags for checking
1114                        attribute_name_to_check = attribute_name.split(';')[0]
1115                    else:
1116                        attribute_name_to_check = attribute_name
1117
1118                    if self.server.schema.attribute_types and attribute_name_to_check.lower() not in conf_attributes_excluded_from_check and attribute_name_to_check not in self.server.schema.attribute_types:
1119                        self.last_error = 'invalid attribute type ' + attribute_name_to_check
1120                        if log_enabled(ERROR):
1121                            log(ERROR, '%s for <%s>', self.last_error, self)
1122                        raise LDAPAttributeError(self.last_error)
1123                change = changes[attribute_name]
1124                if isinstance(change, SEQUENCE_TYPES) and change[0] in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT, 0, 1, 2, 3]:
1125                    if len(change) != 2:
1126                        self.last_error = 'malformed change'
1127                        if log_enabled(ERROR):
1128                            log(ERROR, '%s for <%s>', self.last_error, self)
1129                        raise LDAPChangeError(self.last_error)
1130
1131                    changelist[attribute_name] = [change]  # insert change in a list
1132                else:
1133                    for change_operation in change:
1134                        if len(change_operation) != 2 or change_operation[0] not in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT, 0, 1, 2, 3]:
1135                            self.last_error = 'invalid change list'
1136                            if log_enabled(ERROR):
1137                                log(ERROR, '%s for <%s>', self.last_error, self)
1138                            raise LDAPChangeError(self.last_error)
1139                    changelist[attribute_name] = change
1140            request = modify_operation(dn, changelist, self.auto_encode, self.server.schema if self.server else None, validator=self.server.custom_validator if self.server else None, check_names=self.check_names)
1141            if log_enabled(PROTOCOL):
1142                log(PROTOCOL, 'MODIFY request <%s> sent via <%s>', modify_request_to_dict(request), self)
1143            response = self.post_send_single_response(self.send('modifyRequest', request, controls))
1144            self._entries = []
1145
1146            if isinstance(response, STRING_TYPES + (int, )):
1147                return_value = response
1148                if log_enabled(PROTOCOL):
1149                    log(PROTOCOL, 'async MODIFY response id <%s> received via <%s>', return_value, self)
1150            else:
1151                if log_enabled(PROTOCOL):
1152                    log(PROTOCOL, 'MODIFY response <%s> received via <%s>', response, self)
1153                return_value = True if self.result['type'] == 'modifyResponse' and self.result['result'] == RESULT_SUCCESS else False
1154                if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
1155                    self.last_error = self.result['description']
1156
1157            if log_enabled(BASIC):
1158                log(BASIC, 'done MODIFY operation, result <%s>', return_value)
1159
1160            return self._prepare_return_value(return_value)
1161
1162    def modify_dn(self,
1163                  dn,
1164                  relative_dn,
1165                  delete_old_dn=True,
1166                  new_superior=None,
1167                  controls=None):
1168        """
1169        Modify DN of the entry or performs a move of the entry in the
1170        DIT.
1171        """
1172        if log_enabled(BASIC):
1173            log(BASIC, 'start MODIFY DN operation via <%s>', self)
1174        self.last_error = None
1175        if self.check_names:
1176            dn = safe_dn(dn)
1177            if log_enabled(EXTENDED):
1178                log(EXTENDED, 'dn sanitized to <%s> for MODIFY DN operation via <%s>', dn, self)
1179            relative_dn = safe_dn(relative_dn)
1180            if log_enabled(EXTENDED):
1181                log(EXTENDED, 'relative dn sanitized to <%s> for MODIFY DN operation via <%s>', relative_dn, self)
1182
1183        with self.connection_lock:
1184            self._fire_deferred()
1185            if self.read_only:
1186                self.last_error = 'connection is read-only'
1187                if log_enabled(ERROR):
1188                    log(ERROR, '%s for <%s>', self.last_error, self)
1189                raise LDAPConnectionIsReadOnlyError(self.last_error)
1190
1191            # if new_superior and not dn.startswith(relative_dn):  # as per RFC4511 (4.9)
1192            #     self.last_error = 'DN cannot change while performing moving'
1193            #     if log_enabled(ERROR):
1194            #         log(ERROR, '%s for <%s>', self.last_error, self)
1195            #     raise LDAPChangeError(self.last_error)
1196
1197            request = modify_dn_operation(dn, relative_dn, delete_old_dn, new_superior)
1198            if log_enabled(PROTOCOL):
1199                log(PROTOCOL, 'MODIFY DN request <%s> sent via <%s>', modify_dn_request_to_dict(request), self)
1200            response = self.post_send_single_response(self.send('modDNRequest', request, controls))
1201            self._entries = []
1202
1203            if isinstance(response, STRING_TYPES + (int, )):
1204                return_value = response
1205                if log_enabled(PROTOCOL):
1206                    log(PROTOCOL, 'async MODIFY DN response id <%s> received via <%s>', return_value, self)
1207            else:
1208                if log_enabled(PROTOCOL):
1209                    log(PROTOCOL, 'MODIFY DN response <%s> received via <%s>', response, self)
1210                return_value = True if self.result['type'] == 'modDNResponse' and self.result['result'] == RESULT_SUCCESS else False
1211                if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
1212                    self.last_error = self.result['description']
1213
1214            if log_enabled(BASIC):
1215                log(BASIC, 'done MODIFY DN operation, result <%s>', return_value)
1216
1217            return self._prepare_return_value(return_value)
1218
1219    def abandon(self,
1220                message_id,
1221                controls=None):
1222        """
1223        Abandon the operation indicated by message_id
1224        """
1225        if log_enabled(BASIC):
1226            log(BASIC, 'start ABANDON operation via <%s>', self)
1227        self.last_error = None
1228        with self.connection_lock:
1229            self._fire_deferred()
1230            return_value = False
1231            if self.strategy._outstanding or message_id == 0:
1232                # only current  operation should be abandoned, abandon, bind and unbind cannot ever be abandoned,
1233                # messagiId 0 is invalid and should be used as a "ping" to keep alive the connection
1234                if (self.strategy._outstanding and message_id in self.strategy._outstanding and self.strategy._outstanding[message_id]['type'] not in ['abandonRequest', 'bindRequest', 'unbindRequest']) or message_id == 0:
1235                    request = abandon_operation(message_id)
1236                    if log_enabled(PROTOCOL):
1237                        log(PROTOCOL, 'ABANDON request: <%s> sent via <%s>', abandon_request_to_dict(request), self)
1238                    self.send('abandonRequest', request, controls)
1239                    self.result = None
1240                    self.response = None
1241                    self._entries = []
1242                    return_value = True
1243                else:
1244                    if log_enabled(ERROR):
1245                        log(ERROR, 'cannot abandon a Bind, an Unbind or an Abandon operation or message ID %s not found via <%s>', str(message_id), self)
1246
1247            if log_enabled(BASIC):
1248                log(BASIC, 'done ABANDON operation, result <%s>', return_value)
1249
1250            return self._prepare_return_value(return_value)
1251
1252    def extended(self,
1253                 request_name,
1254                 request_value=None,
1255                 controls=None,
1256                 no_encode=None):
1257        """
1258        Performs an extended operation
1259        """
1260        if log_enabled(BASIC):
1261            log(BASIC, 'start EXTENDED operation via <%s>', self)
1262        self.last_error = None
1263        with self.connection_lock:
1264            self._fire_deferred()
1265            request = extended_operation(request_name, request_value, no_encode=no_encode)
1266            if log_enabled(PROTOCOL):
1267                log(PROTOCOL, 'EXTENDED request <%s> sent via <%s>', extended_request_to_dict(request), self)
1268            response = self.post_send_single_response(self.send('extendedReq', request, controls))
1269            self._entries = []
1270            if isinstance(response, int):
1271                return_value = response
1272                if log_enabled(PROTOCOL):
1273                    log(PROTOCOL, 'async EXTENDED response id <%s> received via <%s>', return_value, self)
1274            else:
1275                if log_enabled(PROTOCOL):
1276                    log(PROTOCOL, 'EXTENDED response <%s> received via <%s>', response, self)
1277                return_value = True if self.result['type'] == 'extendedResp' and self.result['result'] == RESULT_SUCCESS else False
1278                if not return_value and self.result['result'] not in [RESULT_SUCCESS] and not self.last_error:
1279                    self.last_error = self.result['description']
1280
1281            if log_enabled(BASIC):
1282                log(BASIC, 'done EXTENDED operation, result <%s>', return_value)
1283
1284            return self._prepare_return_value(return_value, response=True)
1285
1286    def start_tls(self, read_server_info=True):  # as per RFC4511. Removal of TLS is defined as MAY in RFC4511 so the client can't implement a generic stop_tls method0
1287        if log_enabled(BASIC):
1288            log(BASIC, 'start START TLS operation via <%s>', self)
1289
1290        with self.connection_lock:
1291            return_value = False
1292            self.result = None
1293
1294            if not self.server.tls:
1295                self.server.tls = Tls()
1296
1297            if self.lazy and not self._executing_deferred:
1298                self._deferred_start_tls = True
1299                self.tls_started = True
1300                return_value = True
1301                if log_enabled(BASIC):
1302                    log(BASIC, 'deferring START TLS for <%s>', self)
1303            else:
1304                self._deferred_start_tls = False
1305                if self.closed:
1306                    self.open()
1307                if self.server.tls.start_tls(self) and self.strategy.sync:  # for asynchronous connections _start_tls is run by the strategy
1308                    if read_server_info:
1309                        self.refresh_server_info()  # refresh server info as per RFC4515 (3.1.5)
1310                    return_value = True
1311                elif not self.strategy.sync:
1312                    return_value = True
1313
1314            if log_enabled(BASIC):
1315                log(BASIC, 'done START TLS operation, result <%s>', return_value)
1316
1317            return self._prepare_return_value(return_value)
1318
1319    def do_sasl_bind(self,
1320                     controls):
1321        if log_enabled(BASIC):
1322            log(BASIC, 'start SASL BIND operation via <%s>', self)
1323        self.last_error = None
1324        with self.connection_lock:
1325            result = None
1326
1327            if not self.sasl_in_progress:
1328                self.sasl_in_progress = True
1329                try:
1330                    if self.sasl_mechanism == EXTERNAL:
1331                        result = sasl_external(self, controls)
1332                    elif self.sasl_mechanism == DIGEST_MD5:
1333                        result = sasl_digest_md5(self, controls)
1334                    elif self.sasl_mechanism == GSSAPI:
1335                        from ..protocol.sasl.kerberos import sasl_gssapi  # needs the gssapi package
1336                        result = sasl_gssapi(self, controls)
1337                    elif self.sasl_mechanism == 'PLAIN':
1338                        result = sasl_plain(self, controls)
1339                finally:
1340                    self.sasl_in_progress = False
1341
1342            if log_enabled(BASIC):
1343                log(BASIC, 'done SASL BIND operation, result <%s>', result)
1344
1345            return result
1346
1347    def do_ntlm_bind(self,
1348                     controls):
1349        if log_enabled(BASIC):
1350            log(BASIC, 'start NTLM BIND operation via <%s>', self)
1351        self.last_error = None
1352        with self.connection_lock:
1353            result = None
1354            if not self.sasl_in_progress:
1355                self.sasl_in_progress = True  # ntlm is same of sasl authentication
1356                try:
1357                    # additional import for NTLM
1358                    from ..utils.ntlm import NtlmClient
1359                    domain_name, user_name = self.user.split('\\', 1)
1360                    ntlm_client = NtlmClient(user_name=user_name, domain=domain_name, password=self.password)
1361
1362                    # as per https://msdn.microsoft.com/en-us/library/cc223501.aspx
1363                    # send a sicilyPackageDiscovery request (in the bindRequest)
1364                    request = bind_operation(self.version, 'SICILY_PACKAGE_DISCOVERY', ntlm_client)
1365                    if log_enabled(PROTOCOL):
1366                        log(PROTOCOL, 'NTLM SICILY PACKAGE DISCOVERY request sent via <%s>', self)
1367                    response = self.post_send_single_response(self.send('bindRequest', request, controls))
1368                    if not self.strategy.sync:
1369                        _, result = self.get_response(response)
1370                    else:
1371                        result = response[0]
1372                    if 'server_creds' in result:
1373                        sicily_packages = result['server_creds'].decode('ascii').split(';')
1374                        if 'NTLM' in sicily_packages:  # NTLM available on server
1375                            request = bind_operation(self.version, 'SICILY_NEGOTIATE_NTLM', ntlm_client)
1376                            if log_enabled(PROTOCOL):
1377                                log(PROTOCOL, 'NTLM SICILY NEGOTIATE request sent via <%s>', self)
1378                            response = self.post_send_single_response(self.send('bindRequest', request, controls))
1379                            if not self.strategy.sync:
1380                                _, result = self.get_response(response)
1381                            else:
1382                                if log_enabled(PROTOCOL):
1383                                    log(PROTOCOL, 'NTLM SICILY NEGOTIATE response <%s> received via <%s>', response[0],
1384                                        self)
1385                                result = response[0]
1386
1387                            if result['result'] == RESULT_SUCCESS:
1388                                request = bind_operation(self.version, 'SICILY_RESPONSE_NTLM', ntlm_client,
1389                                                         result['server_creds'])
1390                                if log_enabled(PROTOCOL):
1391                                    log(PROTOCOL, 'NTLM SICILY RESPONSE NTLM request sent via <%s>', self)
1392                                response = self.post_send_single_response(self.send('bindRequest', request, controls))
1393                                if not self.strategy.sync:
1394                                    _, result = self.get_response(response)
1395                                else:
1396                                    if log_enabled(PROTOCOL):
1397                                        log(PROTOCOL, 'NTLM BIND response <%s> received via <%s>', response[0], self)
1398                                    result = response[0]
1399                    else:
1400                        result = None
1401                finally:
1402                    self.sasl_in_progress = False
1403
1404                if log_enabled(BASIC):
1405                    log(BASIC, 'done SASL NTLM operation, result <%s>', result)
1406
1407                return result
1408
1409    def refresh_server_info(self):
1410        # if self.strategy.no_real_dsa:  # do not refresh for mock strategies
1411        #     return
1412
1413        if not self.strategy.pooled:
1414            with self.connection_lock:
1415                if not self.closed:
1416                    if log_enabled(BASIC):
1417                        log(BASIC, 'refreshing server info for <%s>', self)
1418                    previous_response = self.response
1419                    previous_result = self.result
1420                    previous_entries = self._entries
1421                    self.server.get_info_from_server(self)
1422                    self.response = previous_response
1423                    self.result = previous_result
1424                    self._entries = previous_entries
1425        else:
1426            if log_enabled(BASIC):
1427                log(BASIC, 'refreshing server info from pool for <%s>', self)
1428            self.strategy.pool.get_info_from_server()
1429
1430    def response_to_ldif(self,
1431                         search_result=None,
1432                         all_base64=False,
1433                         line_separator=None,
1434                         sort_order=None,
1435                         stream=None):
1436        with self.connection_lock:
1437            if search_result is None:
1438                search_result = self.response
1439
1440            if isinstance(search_result, SEQUENCE_TYPES):
1441                ldif_lines = operation_to_ldif('searchResponse', search_result, all_base64, sort_order=sort_order)
1442                ldif_lines = add_ldif_header(ldif_lines)
1443                line_separator = line_separator or linesep
1444                ldif_output = line_separator.join(ldif_lines)
1445                if stream:
1446                    if stream.tell() == 0:
1447                        header = add_ldif_header(['-'])[0]
1448                        stream.write(prepare_for_stream(header + line_separator + line_separator))
1449                    stream.write(prepare_for_stream(ldif_output + line_separator + line_separator))
1450                if log_enabled(BASIC):
1451                    log(BASIC, 'building LDIF output <%s> for <%s>', ldif_output, self)
1452                return ldif_output
1453
1454            return None
1455
1456    def response_to_json(self,
1457                         raw=False,
1458                         search_result=None,
1459                         indent=4,
1460                         sort=True,
1461                         stream=None,
1462                         checked_attributes=True,
1463                         include_empty=True):
1464
1465        with self.connection_lock:
1466            if search_result is None:
1467                search_result = self.response
1468
1469            if isinstance(search_result, SEQUENCE_TYPES):
1470                json_dict = dict()
1471                json_dict['entries'] = []
1472
1473                for response in search_result:
1474                    if response['type'] == 'searchResEntry':
1475                        entry = dict()
1476
1477                        entry['dn'] = response['dn']
1478                        if checked_attributes:
1479                            if not include_empty:
1480                                # needed for python 2.6 compatibility
1481                                entry['attributes'] = dict((key, response['attributes'][key]) for key in response['attributes'] if response['attributes'][key])
1482                            else:
1483                                entry['attributes'] = dict(response['attributes'])
1484                        if raw:
1485                            if not include_empty:
1486                                # needed for python 2.6 compatibility
1487                                entry['raw_attributes'] = dict((key, response['raw_attributes'][key]) for key in response['raw_attributes'] if response['raw:attributes'][key])
1488                            else:
1489                                entry['raw'] = dict(response['raw_attributes'])
1490                        json_dict['entries'].append(entry)
1491
1492                if str is bytes:  # Python 2
1493                    check_json_dict(json_dict)
1494
1495                json_output = json.dumps(json_dict, ensure_ascii=True, sort_keys=sort, indent=indent, check_circular=True, default=format_json, separators=(',', ': '))
1496
1497                if log_enabled(BASIC):
1498                    log(BASIC, 'building JSON output <%s> for <%s>', json_output, self)
1499                if stream:
1500                    stream.write(json_output)
1501
1502                return json_output
1503
1504    def response_to_file(self,
1505                         target,
1506                         raw=False,
1507                         indent=4,
1508                         sort=True):
1509        with self.connection_lock:
1510            if self.response:
1511                if isinstance(target, STRING_TYPES):
1512                    target = open(target, 'w+')
1513
1514                if log_enabled(BASIC):
1515                    log(BASIC, 'writing response to file for <%s>', self)
1516
1517                target.writelines(self.response_to_json(raw=raw, indent=indent, sort=sort))
1518                target.close()
1519
1520    def _fire_deferred(self, read_info=None):
1521        # if read_info is None reads the schema and server info if not present, if False doesn't read server info, if True reads always server info
1522        with self.connection_lock:
1523            if self.lazy and not self._executing_deferred:
1524                self._executing_deferred = True
1525
1526                if log_enabled(BASIC):
1527                    log(BASIC, 'executing deferred (open: %s, start_tls: %s, bind: %s) for <%s>', self._deferred_open, self._deferred_start_tls, self._deferred_bind, self)
1528                try:
1529                    if self._deferred_open:
1530                        self.open(read_server_info=False)
1531                    if self._deferred_start_tls:
1532                        if not self.start_tls(read_server_info=False):
1533                            error = 'deferred start_tls not successful' + (' - ' + self.last_error if self.last_error else '')
1534                            if log_enabled(ERROR):
1535                                log(ERROR, '%s for <%s>', error, self)
1536                            self.unbind()
1537                            raise LDAPStartTLSError(error)
1538                    if self._deferred_bind:
1539                        self.bind(read_server_info=False, controls=self._bind_controls)
1540                    if (read_info is None and (not self.server.info and self.server.get_info in [DSA, ALL]) or (not self.server.schema and self.server.get_info in [SCHEMA, ALL])) or read_info:
1541                        self.refresh_server_info()
1542                except LDAPExceptionError as e:
1543                    if log_enabled(ERROR):
1544                        log(ERROR, '%s for <%s>', e, self)
1545                    raise  # re-raise LDAPExceptionError
1546                finally:
1547                    self._executing_deferred = False
1548
1549    @property
1550    def entries(self):
1551        if self.response:
1552            if not self._entries:
1553                self._entries = self._get_entries(self.response, self.request)
1554        return self._entries
1555
1556    def _get_entries(self, search_response, search_request):
1557        with self.connection_lock:
1558            from .. import ObjectDef, Reader
1559
1560            # build a table of ObjectDefs, grouping the entries found in search_response for their attributes set, subset will be included in superset
1561            attr_sets = []
1562            for response in search_response:
1563                if response['type'] == 'searchResEntry':
1564                    resp_attr_set = set(response['attributes'].keys())
1565                    if resp_attr_set not in attr_sets:
1566                        attr_sets.append(resp_attr_set)
1567            attr_sets.sort(key=lambda x: -len(x))  # sorts the list in descending length order
1568            unique_attr_sets = []
1569            for attr_set in attr_sets:
1570                for unique_set in unique_attr_sets:
1571                    if unique_set >= attr_set:  # checks if unique set is a superset of attr_set
1572                        break
1573                else:  # the attr_set is not a subset of any element in unique_attr_sets
1574                    unique_attr_sets.append(attr_set)
1575            object_defs = []
1576            for attr_set in unique_attr_sets:
1577                object_def = ObjectDef(schema=self.server.schema)
1578                object_def += list(attr_set)  # converts the set in a list to be added to the object definition
1579                object_defs.append((attr_set,
1580                                    object_def,
1581                                    Reader(self, object_def, search_request['base'], search_request['filter'], attributes=attr_set) if self.strategy.sync else Reader(self, object_def, '', '', attributes=attr_set))
1582                                   )  # objects_defs contains a tuple with the set, the ObjectDef and a cursor
1583
1584            entries = []
1585            for response in search_response:
1586                if response['type'] == 'searchResEntry':
1587                    resp_attr_set = set(response['attributes'].keys())
1588                    for object_def in object_defs:
1589                        if resp_attr_set <= object_def[0]:  # finds the ObjectDef for the attribute set of this entry
1590                            entry = object_def[2]._create_entry(response)
1591                            entries.append(entry)
1592                            break
1593                    else:
1594                        self.last_error = 'attribute set not found for ' + str(resp_attr_set)
1595                        if log_enabled(ERROR):
1596                            log(ERROR, self.last_error, self)
1597                        raise LDAPObjectError(self.last_error)
1598
1599        return entries
1600