summaryrefslogtreecommitdiff
path: root/ldap3
diff options
context:
space:
mode:
Diffstat (limited to 'ldap3')
-rw-r--r--ldap3/__init__.py11
-rw-r--r--ldap3/abstract/cursor.py16
-rw-r--r--ldap3/abstract/entry.py39
-rw-r--r--ldap3/core/connection.py140
-rw-r--r--ldap3/core/exceptions.py8
-rw-r--r--ldap3/core/server.py45
-rw-r--r--ldap3/core/tls.py4
-rw-r--r--ldap3/extend/microsoft/addMembersToGroups.py15
-rw-r--r--ldap3/extend/microsoft/dirSync.py7
-rw-r--r--ldap3/extend/microsoft/modifyPassword.py5
-rw-r--r--ldap3/extend/microsoft/removeMembersFromGroups.py11
-rw-r--r--ldap3/extend/microsoft/unlockAccount.py9
-rw-r--r--ldap3/extend/novell/addMembersToGroups.py22
-rw-r--r--ldap3/extend/novell/checkGroupsMemberships.py12
-rw-r--r--ldap3/extend/novell/removeMembersFromGroups.py22
-rw-r--r--ldap3/extend/operation.py31
-rw-r--r--ldap3/extend/standard/PagedSearch.py9
-rw-r--r--ldap3/operation/search.py3
-rw-r--r--ldap3/protocol/formatters/validators.py6
-rw-r--r--ldap3/protocol/rfc2849.py7
-rw-r--r--ldap3/protocol/sasl/digestMd5.py4
-rw-r--r--ldap3/protocol/sasl/kerberos.py273
-rw-r--r--ldap3/strategy/base.py77
-rw-r--r--ldap3/strategy/restartable.py515
-rw-r--r--ldap3/strategy/safeSync.py32
-rw-r--r--ldap3/strategy/sync.py2
-rw-r--r--ldap3/utils/config.py9
-rw-r--r--ldap3/utils/conv.py4
-rw-r--r--ldap3/utils/ntlm.py10
-rw-r--r--ldap3/utils/port_validators.py2
-rw-r--r--ldap3/utils/uri.py236
-rw-r--r--ldap3/version.py8
32 files changed, 918 insertions, 676 deletions
diff --git a/ldap3/__init__.py b/ldap3/__init__.py
index 9bb5435..2a304ee 100644
--- a/ldap3/__init__.py
+++ b/ldap3/__init__.py
@@ -37,11 +37,11 @@ DIGEST_MD5 = 'DIGEST-MD5'
KERBEROS = GSSAPI = 'GSSAPI'
PLAIN = 'PLAIN'
-AUTO_BIND_DEFAULT = 'DEFAULT' # binds connection whens using "with" context manager
-AUTO_BIND_NONE = 'NONE' # same as False
-AUTO_BIND_NO_TLS = 'NO_TLS' # same as True
-AUTO_BIND_TLS_BEFORE_BIND = 'TLS_BEFORE_BIND'
-AUTO_BIND_TLS_AFTER_BIND = 'TLS_AFTER_BIND'
+AUTO_BIND_DEFAULT = 'DEFAULT' # binds connection when using "with" context manager
+AUTO_BIND_NONE = 'NONE' # same as False, no bind is performed
+AUTO_BIND_NO_TLS = 'NO_TLS' # same as True, bind is performed without tls
+AUTO_BIND_TLS_BEFORE_BIND = 'TLS_BEFORE_BIND' # start_tls is performed before bind
+AUTO_BIND_TLS_AFTER_BIND = 'TLS_AFTER_BIND' # start_tls is performed after bind
# server IP dual stack mode
IP_SYSTEM_DEFAULT = 'IP_SYSTEM_DEFAULT'
@@ -74,6 +74,7 @@ MODIFY_INCREMENT = 'MODIFY_INCREMENT'
# client strategies
SYNC = 'SYNC'
+SAFE_SYNC = 'SAFE_SYNC'
ASYNC = 'ASYNC'
LDIF = 'LDIF'
RESTARTABLE = 'RESTARTABLE'
diff --git a/ldap3/abstract/cursor.py b/ldap3/abstract/cursor.py
index 9259a2c..690e87d 100644
--- a/ldap3/abstract/cursor.py
+++ b/ldap3/abstract/cursor.py
@@ -349,8 +349,11 @@ class Cursor(object):
if not self.connection.strategy.sync:
response, result, request = self.connection.get_response(result, get_request=True)
else:
- response = self.connection.response
- result = self.connection.result
+ if self.connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ response = self.connection.response
+ result = self.connection.result
request = self.connection.request
self._store_operation_in_history(request, result, response)
@@ -820,9 +823,12 @@ class Writer(Cursor):
if not self.connection.strategy.sync:
response, result, request = self.connection.get_response(result, get_request=True)
else:
- response = self.connection.response
- result = self.connection.result
- request = self.connection.request
+ if self.connection.strategy.thread_safe:
+ _, result, response, request = result
+ else:
+ response = self.connection.response
+ result = self.connection.result
+ request = self.connection.request
if result['result'] in [RESULT_SUCCESS]:
break
diff --git a/ldap3/abstract/entry.py b/ldap3/abstract/entry.py
index b73c50f..2375796 100644
--- a/ldap3/abstract/entry.py
+++ b/ldap3/abstract/entry.py
@@ -31,6 +31,7 @@ except ImportError:
from ..utils.ordDict import OrderedDict # for Python 2.6
from os import linesep
+from copy import deepcopy
from .. import STRING_TYPES, SEQUENCE_TYPES, MODIFY_ADD, MODIFY_REPLACE
from .attribute import WritableAttribute
@@ -304,7 +305,7 @@ class EntryBase(object):
@property
def entry_attributes_as_dict(self):
- return dict((attribute_key, attribute_value.values) for (attribute_key, attribute_value) in self._state.attributes.items())
+ return dict((attribute_key, deepcopy(attribute_value.values)) for (attribute_key, attribute_value) in self._state.attributes.items())
@property
def entry_read_time(self):
@@ -535,9 +536,12 @@ class WritableEntry(EntryBase):
if not self.entry_cursor.connection.strategy.sync:
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
else:
- response = self.entry_cursor.connection.response
- result = self.entry_cursor.connection.result
- request = self.entry_cursor.connection.request
+ if self.entry_cursor.connection.strategy.thread_safe:
+ _, result, response, request = result
+ else:
+ response = self.entry_cursor.connection.response
+ result = self.entry_cursor.connection.result
+ request = self.entry_cursor.connection.request
self.entry_cursor._store_operation_in_history(request, result, response)
if result['result'] == RESULT_SUCCESS:
dn = self.entry_dn
@@ -557,9 +561,12 @@ class WritableEntry(EntryBase):
if not self.entry_cursor.connection.strategy.sync:
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
else:
- response = self.entry_cursor.connection.response
- result = self.entry_cursor.connection.result
- request = self.entry_cursor.connection.request
+ if self.entry_cursor.connection.strategy.thread_safe:
+ _, result, response, request = result
+ else:
+ response = self.entry_cursor.connection.response
+ result = self.entry_cursor.connection.result
+ request = self.entry_cursor.connection.request
self.entry_cursor._store_operation_in_history(request, result, response)
if result['result'] == RESULT_SUCCESS:
self._state.dn = safe_dn('+'.join(safe_rdn(self.entry_dn)) + ',' + self._state._to)
@@ -577,9 +584,12 @@ class WritableEntry(EntryBase):
if not self.entry_cursor.connection.strategy.sync:
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
else:
- response = self.entry_cursor.connection.response
- result = self.entry_cursor.connection.result
- request = self.entry_cursor.connection.request
+ if self.entry_cursor.connection.strategy.thread_safe:
+ _, result, response, request = result
+ else:
+ response = self.entry_cursor.connection.response
+ result = self.entry_cursor.connection.result
+ request = self.entry_cursor.connection.request
self.entry_cursor._store_operation_in_history(request, result, response)
if result['result'] == RESULT_SUCCESS:
self._state.dn = rdn + ',' + ','.join(to_dn(self.entry_dn)[1:])
@@ -628,9 +638,12 @@ class WritableEntry(EntryBase):
if not self.entry_cursor.connection.strategy.sync: # asynchronous request
response, result, request = self.entry_cursor.connection.get_response(result, get_request=True)
else:
- response = self.entry_cursor.connection.response
- result = self.entry_cursor.connection.result
- request = self.entry_cursor.connection.request
+ if self.entry_cursor.connection.strategy.thread_safe:
+ _, result, response, request = result
+ else:
+ response = self.entry_cursor.connection.response
+ result = self.entry_cursor.connection.result
+ request = self.entry_cursor.connection.request
self.entry_cursor._store_operation_in_history(request, result, response)
if result['result'] == RESULT_SUCCESS:
diff --git a/ldap3/core/connection.py b/ldap3/core/connection.py
index 0f148e8..6d5b71b 100644
--- a/ldap3/core/connection.py
+++ b/ldap3/core/connection.py
@@ -22,7 +22,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
-from copy import deepcopy
+from copy import deepcopy, copy
from os import linesep
from threading import RLock, Lock
from functools import reduce
@@ -30,9 +30,9 @@ import json
from .. import ANONYMOUS, SIMPLE, SASL, MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, get_config_parameter, DEREF_ALWAYS, \
SUBTREE, ASYNC, SYNC, NO_ATTRIBUTES, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, MODIFY_INCREMENT, LDIF, ASYNC_STREAM, \
- RESTARTABLE, ROUND_ROBIN, REUSABLE, AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_TLS_BEFORE_BIND,\
+ RESTARTABLE, ROUND_ROBIN, REUSABLE, AUTO_BIND_DEFAULT, AUTO_BIND_NONE, AUTO_BIND_TLS_BEFORE_BIND, SAFE_SYNC, \
AUTO_BIND_TLS_AFTER_BIND, AUTO_BIND_NO_TLS, STRING_TYPES, SEQUENCE_TYPES, MOCK_SYNC, MOCK_ASYNC, NTLM, EXTERNAL,\
- DIGEST_MD5, GSSAPI, PLAIN
+ DIGEST_MD5, GSSAPI, PLAIN, DSA, SCHEMA, ALL
from .results import RESULT_SUCCESS, RESULT_COMPARE_TRUE, RESULT_COMPARE_FALSE
from ..extend import ExtendedOperationsRoot
@@ -52,6 +52,7 @@ from ..protocol.sasl.digestMd5 import sasl_digest_md5
from ..protocol.sasl.external import sasl_external
from ..protocol.sasl.plain import sasl_plain
from ..strategy.sync import SyncStrategy
+from ..strategy.safeSync import SafeSyncStrategy
from ..strategy.mockAsync import MockAsyncStrategy
from ..strategy.asynchronous import AsyncStrategy
from ..strategy.reusable import ReusableStrategy
@@ -65,8 +66,7 @@ from .usage import ConnectionUsage
from .tls import Tls
from .exceptions import LDAPUnknownStrategyError, LDAPBindError, LDAPUnknownAuthenticationMethodError, \
LDAPSASLMechanismNotSupportedError, LDAPObjectClassError, LDAPConnectionIsReadOnlyError, LDAPChangeError, LDAPExceptionError, \
- LDAPObjectError, LDAPSocketReceiveError, LDAPAttributeError, LDAPInvalidValueError, LDAPConfigurationError, \
- LDAPInvalidPortError
+ LDAPObjectError, LDAPSocketReceiveError, LDAPAttributeError, LDAPInvalidValueError, LDAPInvalidPortError, LDAPStartTLSError
from ..utils.conv import escape_bytes, prepare_for_stream, check_json_dict, format_json, to_unicode
from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, EXTENDED, get_library_log_hide_sensitive_data
@@ -80,6 +80,7 @@ SASL_AVAILABLE_MECHANISMS = [EXTERNAL,
PLAIN]
CLIENT_STRATEGIES = [SYNC,
+ SAFE_SYNC,
ASYNC,
LDIF,
RESTARTABLE,
@@ -116,7 +117,6 @@ def _format_socket_endpoints(sock):
return '<no socket>'
-# noinspection PyProtectedMember
class Connection(object):
"""Main ldap connection class.
@@ -181,7 +181,6 @@ class Connection(object):
: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
:type source_port_list: list
"""
-
def __init__(self,
server,
user=None,
@@ -323,6 +322,8 @@ class Connection(object):
if self.strategy_type == SYNC:
self.strategy = SyncStrategy(self)
+ elif self.strategy_type == SAFE_SYNC:
+ self.strategy = SafeSyncStrategy(self)
elif self.strategy_type == ASYNC:
self.strategy = AsyncStrategy(self)
elif self.strategy_type == LDIF:
@@ -352,7 +353,7 @@ class Connection(object):
self.post_send_search = self.strategy.post_send_search
if not self.strategy.no_real_dsa:
- self.do_auto_bind()
+ self._do_auto_bind()
# else: # for strategies with a fake server set get_info to NONE if server hasn't a schema
# if self.server and not self.server.schema:
# self.server.get_info = NONE
@@ -362,7 +363,16 @@ class Connection(object):
else:
log(BASIC, 'instantiated Connection: <%r>', self)
- def do_auto_bind(self):
+ def _prepare_return_value(self, status, response=False):
+ if self.strategy.thread_safe:
+ temp_response = self.response
+ self.response = None
+ temp_request = self.request
+ self.request = None
+ return status, deepcopy(self.result), deepcopy(temp_response) if response else None, copy(temp_request)
+ return status
+
+ def _do_auto_bind(self):
if self.auto_bind and self.auto_bind not in [AUTO_BIND_NONE, AUTO_BIND_DEFAULT]:
if log_enabled(BASIC):
log(BASIC, 'performing automatic bind for <%s>', self)
@@ -371,17 +381,28 @@ class Connection(object):
if self.auto_bind == AUTO_BIND_NO_TLS:
self.bind(read_server_info=True)
elif self.auto_bind == AUTO_BIND_TLS_BEFORE_BIND:
- self.start_tls(read_server_info=False)
- self.bind(read_server_info=True)
+ if self.start_tls(read_server_info=False):
+ self.bind(read_server_info=True)
+ else:
+ error = 'automatic start_tls befored bind not successful' + (' - ' + self.last_error if self.last_error else '')
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', error, self)
+ self.unbind() # unbind anyway to close connection
+ raise LDAPStartTLSError(error)
elif self.auto_bind == AUTO_BIND_TLS_AFTER_BIND:
self.bind(read_server_info=False)
- self.start_tls(read_server_info=True)
+ if not self.start_tls(read_server_info=True):
+ error = 'automatic start_tls after bind not successful' + (' - ' + self.last_error if self.last_error else '')
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', error, self)
+ self.unbind()
+ raise LDAPStartTLSError(error)
if not self.bound:
- self.last_error = 'automatic bind not successful' + (' - ' + self.last_error if self.last_error else '')
+ error = 'automatic bind not successful' + (' - ' + self.last_error if self.last_error else '')
if log_enabled(ERROR):
- log(ERROR, '%s for <%s>', self.last_error, self)
+ log(ERROR, '%s for <%s>', error, self)
self.unbind()
- raise LDAPBindError(self.last_error)
+ raise LDAPBindError(error)
def __str__(self):
s = [
@@ -509,11 +530,11 @@ class Connection(object):
if self.closed:
self.open()
if not self.bound:
- self.bind()
+ if not self.bind():
+ raise LDAPBindError('unable to bind')
return self
- # noinspection PyUnusedLocal
def __exit__(self, exc_type, exc_val, exc_tb):
with self.connection_lock:
context_bound, context_closed = self._context_state.pop()
@@ -646,7 +667,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done BIND operation, result <%s>', self.bound)
- return self.bound
+ return self._prepare_return_value(self.bound, self.result)
def rebind(self,
user=None,
@@ -689,10 +710,13 @@ class Connection(object):
try:
return self.bind(read_server_info, controls)
except LDAPSocketReceiveError:
- raise LDAPBindError('Unable to rebind as a different user, furthermore the server abruptly closed the connection')
+ self.last_error = 'Unable to rebind as a different user, furthermore the server abruptly closed the connection'
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', self.last_error, self)
+ raise LDAPBindError(self.last_error)
else:
self.strategy.pool.rebind_pool()
- return True
+ return self._prepare_return_value(True, self.result)
def unbind(self,
controls=None):
@@ -724,7 +748,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done UNBIND operation, result <%s>', True)
- return True
+ return self._prepare_return_value(True)
def search(self,
search_base,
@@ -799,7 +823,10 @@ class Connection(object):
else:
attribute_name_to_check = attribute_name
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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
+ self.last_error = 'invalid attribute type ' + attribute_name_to_check
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', self.last_error, self)
+ raise LDAPAttributeError(self.last_error)
request = search_operation(search_base,
search_filter,
@@ -838,7 +865,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done SEARCH operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value, response=True)
def compare(self,
dn,
@@ -865,10 +892,16 @@ class Connection(object):
attribute_name_to_check = attribute
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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
+ self.last_error = 'invalid attribute type ' + attribute_name_to_check
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', self.last_error, self)
+ raise LDAPAttributeError(self.last_error)
if isinstance(value, SEQUENCE_TYPES): # value can't be a sequence
- raise LDAPInvalidValueError('value cannot be a sequence')
+ self.last_error = 'value cannot be a sequence'
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', self.last_error, self)
+ raise LDAPInvalidValueError(self.last_error)
with self.connection_lock:
self._fire_deferred()
@@ -892,7 +925,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done COMPARE operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value)
def add(self,
dn,
@@ -950,7 +983,10 @@ class Connection(object):
if self.server and self.server.schema and self.check_names:
for object_class_name in _attributes[object_class_attr_name]:
if object_class_name.lower() not in conf_classes_excluded_from_check and object_class_name not in self.server.schema.object_classes:
- raise LDAPObjectClassError('invalid object class ' + str(object_class_name))
+ self.last_error = 'invalid object class ' + str(object_class_name)
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', self.last_error, self)
+ raise LDAPObjectClassError(self.last_error)
for attribute_name in _attributes:
if ';' in attribute_name: # remove tags for checking
@@ -959,7 +995,10 @@ class Connection(object):
attribute_name_to_check = attribute_name
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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
+ self.last_error = 'invalid attribute type ' + attribute_name_to_check
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', self.last_error, self)
+ raise LDAPAttributeError(self.last_error)
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)
if log_enabled(PROTOCOL):
@@ -981,7 +1020,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done ADD operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value)
def delete(self,
dn,
@@ -1025,7 +1064,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done DELETE operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value)
def modify(self,
dn,
@@ -1077,7 +1116,10 @@ class Connection(object):
attribute_name_to_check = attribute_name
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:
- raise LDAPAttributeError('invalid attribute type ' + attribute_name_to_check)
+ self.last_error = 'invalid attribute type ' + attribute_name_to_check
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', self.last_error, self)
+ raise LDAPAttributeError(self.last_error)
change = changes[attribute_name]
if isinstance(change, SEQUENCE_TYPES) and change[0] in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT, 0, 1, 2, 3]:
if len(change) != 2:
@@ -1115,7 +1157,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done MODIFY operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value)
def modify_dn(self,
dn,
@@ -1172,7 +1214,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done MODIFY DN operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value)
def abandon(self,
message_id,
@@ -1205,7 +1247,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done ABANDON operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value)
def extended(self,
request_name,
@@ -1239,15 +1281,16 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done EXTENDED operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value, response=True)
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
-
if log_enabled(BASIC):
log(BASIC, 'start START TLS operation via <%s>', self)
with self.connection_lock:
return_value = False
+ self.result = None
+
if not self.server.tls:
self.server.tls = Tls()
@@ -1271,7 +1314,7 @@ class Connection(object):
if log_enabled(BASIC):
log(BASIC, 'done START TLS operation, result <%s>', return_value)
- return return_value
+ return self._prepare_return_value(return_value)
def do_sasl_bind(self,
controls):
@@ -1474,7 +1517,8 @@ class Connection(object):
target.writelines(self.response_to_json(raw=raw, indent=indent, sort=sort))
target.close()
- def _fire_deferred(self, read_info=True):
+ def _fire_deferred(self, read_info=None):
+ # 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
with self.connection_lock:
if self.lazy and not self._executing_deferred:
self._executing_deferred = True
@@ -1485,10 +1529,15 @@ class Connection(object):
if self._deferred_open:
self.open(read_server_info=False)
if self._deferred_start_tls:
- self.start_tls(read_server_info=False)
+ if not self.start_tls(read_server_info=False):
+ error = 'deferred start_tls not successful' + (' - ' + self.last_error if self.last_error else '')
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', error, self)
+ self.unbind()
+ raise LDAPStartTLSError(error)
if self._deferred_bind:
self.bind(read_server_info=False, controls=self._bind_controls)
- if read_info:
+ 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:
self.refresh_server_info()
except LDAPExceptionError as e:
if log_enabled(ERROR):
@@ -1501,10 +1550,10 @@ class Connection(object):
def entries(self):
if self.response:
if not self._entries:
- self._entries = self._get_entries(self.response)
+ self._entries = self._get_entries(self.response, self.request)
return self._entries
- def _get_entries(self, search_response):
+ def _get_entries(self, search_response, search_request):
with self.connection_lock:
from .. import ObjectDef, Reader
@@ -1529,7 +1578,7 @@ class Connection(object):
object_def += list(attr_set) # converts the set in a list to be added to the object definition
object_defs.append((attr_set,
object_def,
- Reader(self, object_def, self.request['base'], self.request['filter'], attributes=attr_set) if self.strategy.sync else Reader(self, object_def, '', '', attributes=attr_set))
+ 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))
) # objects_defs contains a tuple with the set, the ObjectDef and a cursor
entries = []
@@ -1542,8 +1591,9 @@ class Connection(object):
entries.append(entry)
break
else:
+ self.last_error = 'attribute set not found for ' + str(resp_attr_set)
if log_enabled(ERROR):
- log(ERROR, 'attribute set not found for %s in <%s>', resp_attr_set, self)
- raise LDAPObjectError('attribute set not found for ' + str(resp_attr_set))
+ log(ERROR, self.last_error, self)
+ raise LDAPObjectError(self.last_error)
return entries
diff --git a/ldap3/core/exceptions.py b/ldap3/core/exceptions.py
index 29aed26..42e895a 100644
--- a/ldap3/core/exceptions.py
+++ b/ldap3/core/exceptions.py
@@ -598,12 +598,8 @@ def communication_exception_factory(exc_to_raise, exc):
raise LDAPExceptionError('unable to generate exception type ' + str(exc_to_raise))
-def start_tls_exception_factory(exc_to_raise, exc):
+def start_tls_exception_factory(exc):
"""
Generates a new exception class of the requested type merged with the exception raised by the interpreter
"""
-
- if exc_to_raise.__name__ == 'LDAPStartTLSError':
- return type(exc_to_raise.__name__, (exc_to_raise, type(exc)), dict())
- else:
- raise LDAPExceptionError('unable to generate exception type ' + str(exc_to_raise))
+ return type(LDAPStartTLSError.__name__, (LDAPStartTLSError, type(exc)), dict())
diff --git a/ldap3/core/server.py b/ldap3/core/server.py
index 43189ef..41ff2af 100644
--- a/ldap3/core/server.py
+++ b/ldap3/core/server.py
@@ -419,11 +419,18 @@ class Server(object):
'+'], # requests all remaining attributes (other),
get_operational_attributes=True)
+ if connection.strategy.thread_safe:
+ status, result, response, _ = result
+ else:
+ status = result
+ result = connection.result
+ response = connection.response
+
with self.dit_lock:
- if isinstance(result, bool): # sync request
- self._dsa_info = DsaInfo(connection.response[0]['attributes'], connection.response[0]['raw_attributes']) if result else self._dsa_info
- elif result: # asynchronous request, must check if attributes in response
- results, _ = connection.get_response(result)
+ if connection.strategy.sync: # sync request
+ self._dsa_info = DsaInfo(response[0]['attributes'], response[0]['raw_attributes']) if status else self._dsa_info
+ elif status: # asynchronous request, must check if attributes in response
+ results, _ = connection.get_response(status)
if len(results) == 1 and 'attributes' in results[0] and 'raw_attributes' in results[0]:
self._dsa_info = DsaInfo(results[0]['attributes'], results[0]['raw_attributes'])
@@ -446,12 +453,18 @@ class Server(object):
schema_entry = self._dsa_info.schema_entry if self._dsa_info.schema_entry else None
else:
result = connection.search(entry, '(objectClass=*)', BASE, attributes=['subschemaSubentry'], get_operational_attributes=True)
- if isinstance(result, bool): # sync request
- if result and 'subschemaSubentry' in connection.response[0]['raw_attributes']:
- if len(connection.response[0]['raw_attributes']['subschemaSubentry']) > 0:
- schema_entry = connection.response[0]['raw_attributes']['subschemaSubentry'][0]
+ if connection.strategy.thread_safe:
+ status, result, response, _ = result
+ else:
+ status = result
+ result = connection.result
+ response = connection.response
+ if connection.strategy.sync: # sync request
+ if status and 'subschemaSubentry' in response[0]['raw_attributes']:
+ if len(response[0]['raw_attributes']['subschemaSubentry']) > 0:
+ schema_entry = response[0]['raw_attributes']['subschemaSubentry'][0]
else: # asynchronous request, must check if subschemaSubentry in attributes
- results, _ = connection.get_response(result)
+ results, _ = connection.get_response(status)
if len(results) == 1 and 'raw_attributes' in results[0] and 'subschemaSubentry' in results[0]['attributes']:
if len(results[0]['raw_attributes']['subschemaSubentry']) > 0:
schema_entry = results[0]['raw_attributes']['subschemaSubentry'][0]
@@ -475,13 +488,19 @@ class Server(object):
'*'], # requests all remaining attributes (other)
get_operational_attributes=True
)
+ if connection.strategy.thread_safe:
+ status, result, response, _ = result
+ else:
+ status = result
+ result = connection.result
+ response = connection.response
with self.dit_lock:
self._schema_info = None
- if result:
- if isinstance(result, bool): # sync request
- self._schema_info = SchemaInfo(schema_entry, connection.response[0]['attributes'], connection.response[0]['raw_attributes']) if result else None
+ if status:
+ if connection.strategy.sync: # sync request
+ self._schema_info = SchemaInfo(schema_entry, response[0]['attributes'], response[0]['raw_attributes'])
else: # asynchronous request, must check if attributes in response
- results, result = connection.get_response(result)
+ results, result = connection.get_response(status)
if len(results) == 1 and 'attributes' in results[0] and 'raw_attributes' in results[0]:
self._schema_info = SchemaInfo(schema_entry, results[0]['attributes'], results[0]['raw_attributes'])
if self._schema_info and not self._schema_info.is_valid(): # flaky servers can return an empty schema, checks if it is so and set schema to None
diff --git a/ldap3/core/tls.py b/ldap3/core/tls.py
index 1539b9f..ca8592b 100644
--- a/ldap3/core/tls.py
+++ b/ldap3/core/tls.py
@@ -23,7 +23,7 @@
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
-from .exceptions import LDAPSSLNotSupportedError, LDAPSSLConfigurationError, LDAPStartTLSError, LDAPCertificateError, start_tls_exception_factory
+from .exceptions import LDAPSSLNotSupportedError, LDAPSSLConfigurationError, LDAPCertificateError, start_tls_exception_factory, LDAPStartTLSError
from .. import SEQUENCE_TYPES
from ..utils.log import log, log_enabled, ERROR, BASIC, NETWORK
@@ -286,7 +286,7 @@ class Tls(object):
connection.last_error = 'wrap socket error: ' + str(e)
if log_enabled(ERROR):
log(ERROR, 'error <%s> wrapping socket for TLS in <%s>', connection.last_error, connection)
- raise start_tls_exception_factory(LDAPStartTLSError, e)(connection.last_error)
+ raise start_tls_exception_factory(e)(connection.last_error)
finally:
connection.starting_tls = False
diff --git a/ldap3/extend/microsoft/addMembersToGroups.py b/ldap3/extend/microsoft/addMembersToGroups.py
index eaf6cfd..fb6528a 100644
--- a/ldap3/extend/microsoft/addMembersToGroups.py
+++ b/ldap3/extend/microsoft/addMembersToGroups.py
@@ -57,13 +57,15 @@ def ad_add_members_to_groups(connection,
error = False
for group in groups_dn:
if fix: # checks for existance of group and for already assigned members
- result = connection.search(group, '(objectclass=*)', BASE, dereference_aliases=DEREF_NEVER,
- attributes=['member'])
-
+ result = connection.search(group, '(objectclass=*)', BASE, dereference_aliases=DEREF_NEVER, attributes=['member'])
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ response = connection.response
+ result = connection.result
if not result['description'] == 'success':
raise LDAPInvalidDnError(group + ' not found')
@@ -82,7 +84,10 @@ def ad_add_members_to_groups(connection,
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
if result['description'] != 'success':
error = True
result_error_params = ['result', 'description', 'dn', 'message']
diff --git a/ldap3/extend/microsoft/dirSync.py b/ldap3/extend/microsoft/dirSync.py
index db403a1..e5dd8ae 100644
--- a/ldap3/extend/microsoft/dirSync.py
+++ b/ldap3/extend/microsoft/dirSync.py
@@ -77,8 +77,11 @@ class DirSync(object):
if not self.connection.strategy.sync:
response, result = self.connection.get_response(result)
else:
- response = self.connection.response
- result = self.connection.result
+ if self.connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ response = self.connection.response
+ result = self.connection.result
if result['description'] == 'success' and 'controls' in result and '1.2.840.113556.1.4.841' in result['controls']:
self.more_results = result['controls']['1.2.840.113556.1.4.841']['value']['more_results']
diff --git a/ldap3/extend/microsoft/modifyPassword.py b/ldap3/extend/microsoft/modifyPassword.py
index 0bf1c06..e304caf 100644
--- a/ldap3/extend/microsoft/modifyPassword.py
+++ b/ldap3/extend/microsoft/modifyPassword.py
@@ -56,7 +56,10 @@ def ad_modify_password(connection, user_dn, new_password, old_password, controls
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
# change successful, returns True
if result['result'] == RESULT_SUCCESS:
diff --git a/ldap3/extend/microsoft/removeMembersFromGroups.py b/ldap3/extend/microsoft/removeMembersFromGroups.py
index 0998713..86b799a 100644
--- a/ldap3/extend/microsoft/removeMembersFromGroups.py
+++ b/ldap3/extend/microsoft/removeMembersFromGroups.py
@@ -62,7 +62,11 @@ def ad_remove_members_from_groups(connection,
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ response = connection.response
+ result = connection.result
if not result['description'] == 'success':
raise LDAPInvalidDnError(group + ' not found')
@@ -81,7 +85,10 @@ def ad_remove_members_from_groups(connection,
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
if result['description'] != 'success':
error = True
result_error_params = ['result', 'description', 'dn', 'message']
diff --git a/ldap3/extend/microsoft/unlockAccount.py b/ldap3/extend/microsoft/unlockAccount.py
index bc59b58..b1c65b8 100644
--- a/ldap3/extend/microsoft/unlockAccount.py
+++ b/ldap3/extend/microsoft/unlockAccount.py
@@ -33,14 +33,15 @@ from ...utils.dn import safe_dn
def ad_unlock_account(connection, user_dn, controls=None):
if connection.check_names:
user_dn = safe_dn(user_dn)
- result = connection.modify(user_dn,
- {'lockoutTime': [(MODIFY_REPLACE, ['0'])]},
- controls)
+ result = connection.modify(user_dn, {'lockoutTime': [(MODIFY_REPLACE, ['0'])]}, controls)
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
# change successful, returns True
if result['result'] == RESULT_SUCCESS:
diff --git a/ldap3/extend/novell/addMembersToGroups.py b/ldap3/extend/novell/addMembersToGroups.py
index d649dc8..b832b9a 100644
--- a/ldap3/extend/novell/addMembersToGroups.py
+++ b/ldap3/extend/novell/addMembersToGroups.py
@@ -74,7 +74,11 @@ def edir_add_members_to_groups(connection,
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ result = connection.result
+ response = connection.response
if not result['description'] == 'success':
raise LDAPInvalidDnError(member + ' not found')
@@ -98,7 +102,10 @@ def edir_add_members_to_groups(connection,
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
if result['description'] != 'success':
error = True
break
@@ -111,7 +118,11 @@ def edir_add_members_to_groups(connection,
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ result = connection.result
+ response = connection.response
if not result['description'] == 'success':
raise LDAPInvalidDnError(group + ' not found')
@@ -136,7 +147,10 @@ def edir_add_members_to_groups(connection,
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
if result['description'] != 'success':
error = True
break
diff --git a/ldap3/extend/novell/checkGroupsMemberships.py b/ldap3/extend/novell/checkGroupsMemberships.py
index c51dbf2..0f65b5d 100644
--- a/ldap3/extend/novell/checkGroupsMemberships.py
+++ b/ldap3/extend/novell/checkGroupsMemberships.py
@@ -55,7 +55,11 @@ def _check_members_have_memberships(connection,
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ result = connection.result
+ response = connection.response
if not result['description'] == 'success': # member not found in DIT
raise LDAPInvalidDnError(member + ' not found')
@@ -99,7 +103,11 @@ def _check_groups_contain_members(connection,
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ result = connection.result
+ response = connection.response
if not result['description'] == 'success':
raise LDAPInvalidDnError(group + ' not found')
diff --git a/ldap3/extend/novell/removeMembersFromGroups.py b/ldap3/extend/novell/removeMembersFromGroups.py
index c46c275..5736079 100644
--- a/ldap3/extend/novell/removeMembersFromGroups.py
+++ b/ldap3/extend/novell/removeMembersFromGroups.py
@@ -75,7 +75,11 @@ def edir_remove_members_from_groups(connection,
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ response = connection.response
+ result = connection.result
if not result['description'] == 'success':
raise LDAPInvalidDnError(member + ' not found')
@@ -100,7 +104,10 @@ def edir_remove_members_from_groups(connection,
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
if result['description'] != 'success':
error = True
break
@@ -113,7 +120,11 @@ def edir_remove_members_from_groups(connection,
if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response, result = connection.response, connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ response = connection.response
+ result = connection.result
if not result['description'] == 'success':
raise LDAPInvalidDnError(group + ' not found')
@@ -139,7 +150,10 @@ def edir_remove_members_from_groups(connection,
if not connection.strategy.sync:
_, result = connection.get_response(result)
else:
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, _, _ = result
+ else:
+ result = connection.result
if result['description'] != 'success':
error = True
break
diff --git a/ldap3/extend/operation.py b/ldap3/extend/operation.py
index c1d478c..fcfdad4 100644
--- a/ldap3/extend/operation.py
+++ b/ldap3/extend/operation.py
@@ -52,10 +52,14 @@ class ExtendedOperation(object):
resp = self.connection.extended(self.request_name, self.request_value, self.controls)
if not self.connection.strategy.sync:
- _, self.result = self.connection.get_response(resp)
+ _, result = self.connection.get_response(resp)
else:
- self.result = self.connection.result
- self.decode_response()
+ if self.connection.strategy.thread_safe:
+ _, result, _, _ = resp
+ else:
+ result = self.connection.result
+ self.result = result
+ self.decode_response(result)
self.populate_result()
self.set_response()
return self.response_value
@@ -63,29 +67,32 @@ class ExtendedOperation(object):
def populate_result(self):
pass
- def decode_response(self):
- if not self.result:
+ def decode_response(self, response=None):
+ if not response:
+ response = self.result
+ if not response:
return None
- if self.result['result'] not in [RESULT_SUCCESS]:
+ if response['result'] not in [RESULT_SUCCESS]:
if self.connection.raise_exceptions:
- raise LDAPExtensionError('extended operation error: ' + self.result['description'] + ' - ' + self.result['message'])
+ raise LDAPExtensionError('extended operation error: ' + response['description'] + ' - ' + response['message'])
else:
return None
- if not self.response_name or self.result['responseName'] == self.response_name:
- if self.result['responseValue']:
+ if not self.response_name or response['responseName'] == self.response_name:
+ if response['responseValue']:
if self.asn1_spec is not None:
- decoded, unprocessed = decoder.decode(self.result['responseValue'], asn1Spec=self.asn1_spec)
+ decoded, unprocessed = decoder.decode(response['responseValue'], asn1Spec=self.asn1_spec)
if unprocessed:
raise LDAPExtensionError('error decoding extended response value')
self.decoded_response = decoded
else:
- self.decoded_response = self.result['responseValue']
+ self.decoded_response = response['responseValue']
else:
raise LDAPExtensionError('invalid response name received')
def set_response(self):
self.response_value = self.result[self.response_attribute] if self.result and self.response_attribute in self.result else None
- self.connection.response = self.response_value
+ if not self.connection.strategy.thread_safe:
+ self.connection.response = self.response_value
def config(self):
pass
diff --git a/ldap3/extend/standard/PagedSearch.py b/ldap3/extend/standard/PagedSearch.py
index f8bc7e6..a0f986e 100644
--- a/ldap3/extend/standard/PagedSearch.py
+++ b/ldap3/extend/standard/PagedSearch.py
@@ -67,11 +67,14 @@ def paged_search_generator(connection,
paged_criticality,
None if cookie is True else cookie)
- if not isinstance(result, bool):
+ if not connection.strategy.sync:
response, result = connection.get_response(result)
else:
- response = connection.response
- result = connection.result
+ if connection.strategy.thread_safe:
+ _, result, response, _ = result
+ else:
+ response = connection.response
+ result = connection.result
if result['referrals'] and original_auto_referrals: # if rererrals are returned start over the loop with a new connection to the referral
if not original_connection:
diff --git a/ldap3/operation/search.py b/ldap3/operation/search.py
index b78d86d..e12c13c 100644
--- a/ldap3/operation/search.py
+++ b/ldap3/operation/search.py
@@ -38,7 +38,6 @@ from ..operation.bind import referrals_to_list
from ..protocol.convert import ava_to_dict, attributes_to_list, search_refs_to_list, validate_assertion_value, prepare_filter_for_sending, search_refs_to_list_fast
from ..protocol.formatters.standard import format_attribute_values
from ..utils.conv import to_unicode, to_raw
-from pyasn1.error import PyAsn1UnicodeDecodeError
ROOT = 0
AND = 1
@@ -381,7 +380,7 @@ def search_operation(search_base,
def decode_vals(vals):
try:
return [str(val) for val in vals if val] if vals else None
- except PyAsn1UnicodeDecodeError:
+ except UnicodeDecodeError:
return decode_raw_vals(vals)
def decode_vals_fast(vals):
diff --git a/ldap3/protocol/formatters/validators.py b/ldap3/protocol/formatters/validators.py
index 3ab300d..bbc459f 100644
--- a/ldap3/protocol/formatters/validators.py
+++ b/ldap3/protocol/formatters/validators.py
@@ -368,7 +368,7 @@ def validate_uuid(input_value):
def validate_uuid_le(input_value):
- """
+ r"""
Active Directory stores objectGUID in uuid_le format, follows RFC4122 and MS-DTYP:
"{07039e68-4373-264d-a0a7-07039e684373}": string representation big endian, converted to little endian (with or without brace curles)
"689e030773434d26a7a007039e684373": packet representation, already in little endian
@@ -401,9 +401,7 @@ def validate_uuid_le(input_value):
error = True
elif '\\' in element:
try:
- uuid = UUID(bytes_le=ldap_escape_to_bytes(element)).bytes_le
- uuid = escape_bytes(uuid)
- valid_values.append(uuid) # byte representation, value in little endian
+ valid_values.append(UUID(bytes_le=ldap_escape_to_bytes(element)).bytes_le) # byte representation, value in little endian
changed = True
except ValueError:
error = True
diff --git a/ldap3/protocol/rfc2849.py b/ldap3/protocol/rfc2849.py
index 953be33..588af9e 100644
--- a/ldap3/protocol/rfc2849.py
+++ b/ldap3/protocol/rfc2849.py
@@ -30,10 +30,11 @@ from .. import STRING_TYPES
from ..core.exceptions import LDAPLDIFError, LDAPExtensionError
from ..protocol.persistentSearch import EntryChangeNotificationControl
from ..utils.asn1 import decoder
+from ..utils.config import get_config_parameter
# LDIF converter RFC 2849 compliant
-LDIF_LINE_LENGTH = 78
+conf_ldif_line_length = get_config_parameter('LDIF_LINE_LENGTH')
def safe_ldif_string(bytes_value):
@@ -233,8 +234,8 @@ def operation_to_ldif(operation_type, entries, all_base64=False, sort_order=None
# check max line length and split as per note 2 of RFC 2849
for line in lines:
if line:
- ldif_record.append(line[0:LDIF_LINE_LENGTH])
- ldif_record.extend([' ' + line[i: i + LDIF_LINE_LENGTH - 1] for i in range(LDIF_LINE_LENGTH, len(line), LDIF_LINE_LENGTH - 1)] if len(line) > LDIF_LINE_LENGTH else [])
+ ldif_record.append(line[0:conf_ldif_line_length])
+ ldif_record.extend([' ' + line[i: i + conf_ldif_line_length - 1] for i in range(conf_ldif_line_length, len(line), conf_ldif_line_length - 1)] if len(line) > conf_ldif_line_length else [])
else:
ldif_record.append('')
diff --git a/ldap3/protocol/sasl/digestMd5.py b/ldap3/protocol/sasl/digestMd5.py
index 48235d6..520be3c 100644
--- a/ldap3/protocol/sasl/digestMd5.py
+++ b/ldap3/protocol/sasl/digestMd5.py
@@ -94,7 +94,7 @@ def sasl_digest_md5(connection, controls):
authz_id = connection.sasl_credentials[3].encode(charset) if connection.sasl_credentials[3] else b''
nonce = server_directives['nonce'].encode(charset)
cnonce = random_hex_string(16).encode(charset)
- uri = b'ldap/'
+ uri = b'ldap/' + connection.server.host.encode(charset)
qop = b'auth'
digest_response = b'username="' + user + b'",'
@@ -110,7 +110,7 @@ def sasl_digest_md5(connection, controls):
a0 = md5_h(b':'.join([user, realm, password]))
a1 = b':'.join([a0, nonce, cnonce, authz_id]) if authz_id else b':'.join([a0, nonce, cnonce])
- a2 = b'AUTHENTICATE:' + uri + (':00000000000000000000000000000000' if qop in [b'auth-int', b'auth-conf'] else b'')
+ a2 = b'AUTHENTICATE:' + uri + (b':00000000000000000000000000000000' if qop in [b'auth-int', b'auth-conf'] else b'')
digest_response += b'response="' + md5_hex(md5_kd(md5_hex(md5_h(a1)), b':'.join([nonce, b'00000001', cnonce, qop, md5_hex(md5_h(a2))]))) + b'"'
diff --git a/ldap3/protocol/sasl/kerberos.py b/ldap3/protocol/sasl/kerberos.py
index 07db583..14a307e 100644
--- a/ldap3/protocol/sasl/kerberos.py
+++ b/ldap3/protocol/sasl/kerberos.py
@@ -1,123 +1,150 @@
-"""
-"""
-
-# Created on 2015.04.08
-#
-# Author: Giovanni Cannata
-#
-# Copyright 2015 - 2020 Giovanni Cannata
-#
-# This file is part of ldap3.
-#
-# ldap3 is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ldap3 is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with ldap3 in the COPYING and COPYING.LESSER files.
-# If not, see <http://www.gnu.org/licenses/>.
-
-# original code by Hugh Cole-Baker, modified by Peter Foley
-# it needs the gssapi package
-import socket
-
-from ...core.exceptions import LDAPPackageUnavailableError, LDAPCommunicationError
-
-try:
- # noinspection PyPackageRequirements,PyUnresolvedReferences
- import gssapi
-except ImportError:
- raise LDAPPackageUnavailableError('package gssapi missing')
-
-from .sasl import send_sasl_negotiation, abort_sasl_negotiation
-
-NO_SECURITY_LAYER = 1
-INTEGRITY_PROTECTION = 2
-CONFIDENTIALITY_PROTECTION = 4
-
-
-def sasl_gssapi(connection, controls):
- """
- Performs a bind using the Kerberos v5 ("GSSAPI") SASL mechanism
- from RFC 4752. Does not support any security layers, only authentication!
-
- sasl_credentials can be empty or a tuple with one or two elements.
- The first element determines which service principal to request a ticket for and can be one of the following:
-
- - None or False, to use the hostname from the Server object
- - True to perform a reverse DNS lookup to retrieve the canonical hostname for the hosts IP address
- - A string containing the hostname
-
- The optional second element is what authorization ID to request.
-
- - If omitted or None, the authentication ID is used as the authorization ID
- - If a string, the authorization ID to use. Should start with "dn:" or "user:".
-
- The optional third element is a raw gssapi credentials structure which can be used over
- the implicit use of a krb ccache.
- """
- target_name = None
- authz_id = b""
- raw_creds = None
- creds = None
- if connection.sasl_credentials:
- if len(connection.sasl_credentials) >= 1 and connection.sasl_credentials[0]:
- if connection.sasl_credentials[0] is True:
- hostname = socket.gethostbyaddr(connection.socket.getpeername()[0])[0]
- target_name = gssapi.Name('ldap@' + hostname, gssapi.NameType.hostbased_service)
- else:
- target_name = gssapi.Name('ldap@' + connection.sasl_credentials[0], gssapi.NameType.hostbased_service)
- if len(connection.sasl_credentials) >= 2 and connection.sasl_credentials[1]:
- authz_id = connection.sasl_credentials[1].encode("utf-8")
- if len(connection.sasl_credentials) >= 3 and connection.sasl_credentials[2]:
- raw_creds = connection.sasl_credentials[2]
- if target_name is None:
- target_name = gssapi.Name('ldap@' + connection.server.host, gssapi.NameType.hostbased_service)
-
- if raw_creds is not None:
- creds = gssapi.Credentials(base=raw_creds, usage='initiate', store=connection.cred_store)
- else:
- creds = gssapi.Credentials(name=gssapi.Name(connection.user), usage='initiate', store=connection.cred_store) if connection.user else None
- ctx = gssapi.SecurityContext(name=target_name, mech=gssapi.MechType.kerberos, creds=creds)
- in_token = None
- try:
- while True:
- out_token = ctx.step(in_token)
- if out_token is None:
- out_token = ''
- result = send_sasl_negotiation(connection, controls, out_token)
- in_token = result['saslCreds']
- try:
- # This raised an exception in gssapi<1.1.2 if the context was
- # incomplete, but was fixed in
- # https://github.com/pythongssapi/python-gssapi/pull/70
- if ctx.complete:
- break
- except gssapi.exceptions.MissingContextError:
- pass
-
- unwrapped_token = ctx.unwrap(in_token)
- if len(unwrapped_token.message) != 4:
- raise LDAPCommunicationError("Incorrect response from server")
-
- server_security_layers = unwrapped_token.message[0]
- if not isinstance(server_security_layers, int):
- server_security_layers = ord(server_security_layers)
- if server_security_layers in (0, NO_SECURITY_LAYER):
- if unwrapped_token.message[1:] != '\x00\x00\x00':
- raise LDAPCommunicationError("Server max buffer size must be 0 if no security layer")
- if not (server_security_layers & NO_SECURITY_LAYER):
- raise LDAPCommunicationError("Server requires a security layer, but this is not implemented")
-
- client_security_layers = bytearray([NO_SECURITY_LAYER, 0, 0, 0])
- out_token = ctx.wrap(bytes(client_security_layers)+authz_id, False)
- return send_sasl_negotiation(connection, controls, out_token.message)
- except (gssapi.exceptions.GSSError, LDAPCommunicationError):
- abort_sasl_negotiation(connection, controls)
- raise
+"""
+"""
+
+# Created on 2015.04.08
+#
+# Author: Giovanni Cannata
+#
+# Copyright 2015 - 2020 Giovanni Cannata
+#
+# This file is part of ldap3.
+#
+# ldap3 is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ldap3 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with ldap3 in the COPYING and COPYING.LESSER files.
+# If not, see <http://www.gnu.org/licenses/>.
+
+# original code by Hugh Cole-Baker, modified by Peter Foley
+# it needs the gssapi package
+import socket
+
+from ...core.exceptions import LDAPPackageUnavailableError, LDAPCommunicationError
+
+try:
+ # noinspection PyPackageRequirements,PyUnresolvedReferences
+ import gssapi
+ from gssapi.raw import ChannelBindings
+except ImportError:
+ raise LDAPPackageUnavailableError('package gssapi missing')
+
+from .sasl import send_sasl_negotiation, abort_sasl_negotiation
+
+
+NO_SECURITY_LAYER = 1
+INTEGRITY_PROTECTION = 2
+CONFIDENTIALITY_PROTECTION = 4
+
+
+def get_channel_bindings(ssl_socket):
+ try:
+ server_certificate = ssl_socket.getpeercert(True)
+ except:
+ # it is not SSL socket
+ return None
+ try:
+ from cryptography import x509
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import hashes
+ except ImportError:
+ raise LDAPPackageUnavailableError('package cryptography missing')
+ cert = x509.load_der_x509_certificate(server_certificate, default_backend())
+ hash_algorithm = cert.signature_hash_algorithm
+ # According to https://tools.ietf.org/html/rfc5929#section-4.1, we have to convert the the hash function for md5 and sha1
+ if hash_algorithm.name in ('md5', 'sha1'):
+ digest = hashes.Hash(hashes.SHA256(), default_backend())
+ else:
+ digest = hashes.Hash(hash_algorithm, default_backend())
+ digest.update(server_certificate)
+ application_data = b'tls-server-end-point:' + digest.finalize()
+ return ChannelBindings(application_data=application_data)
+
+
+def sasl_gssapi(connection, controls):
+ """
+ Performs a bind using the Kerberos v5 ("GSSAPI") SASL mechanism
+ from RFC 4752. Does not support any security layers, only authentication!
+
+ sasl_credentials can be empty or a tuple with one or two elements.
+ The first element determines which service principal to request a ticket for and can be one of the following:
+
+ - None or False, to use the hostname from the Server object
+ - True to perform a reverse DNS lookup to retrieve the canonical hostname for the hosts IP address
+ - A string containing the hostname
+
+ The optional second element is what authorization ID to request.
+
+ - If omitted or None, the authentication ID is used as the authorization ID
+ - If a string, the authorization ID to use. Should start with "dn:" or "user:".
+
+ The optional third element is a raw gssapi credentials structure which can be used over
+ the implicit use of a krb ccache.
+ """
+ target_name = None
+ authz_id = b""
+ raw_creds = None
+ creds = None
+ if connection.sasl_credentials:
+ if len(connection.sasl_credentials) >= 1 and connection.sasl_credentials[0]:
+ if connection.sasl_credentials[0] is True:
+ hostname = socket.gethostbyaddr(connection.socket.getpeername()[0])[0]
+ target_name = gssapi.Name('ldap@' + hostname, gssapi.NameType.hostbased_service)
+ else:
+ target_name = gssapi.Name('ldap@' + connection.sasl_credentials[0], gssapi.NameType.hostbased_service)
+ if len(connection.sasl_credentials) >= 2 and connection.sasl_credentials[1]:
+ authz_id = connection.sasl_credentials[1].encode("utf-8")
+ if len(connection.sasl_credentials) >= 3 and connection.sasl_credentials[2]:
+ raw_creds = connection.sasl_credentials[2]
+ if target_name is None:
+ target_name = gssapi.Name('ldap@' + connection.server.host, gssapi.NameType.hostbased_service)
+
+ if raw_creds is not None:
+ creds = gssapi.Credentials(base=raw_creds, usage='initiate', store=connection.cred_store)
+ else:
+ creds = gssapi.Credentials(name=gssapi.Name(connection.user), usage='initiate', store=connection.cred_store) if connection.user else None
+
+ ctx = gssapi.SecurityContext(name=target_name, mech=gssapi.MechType.kerberos, creds=creds, channel_bindings=get_channel_bindings(connection.socket))
+ in_token = None
+ try:
+ while True:
+ out_token = ctx.step(in_token)
+ if out_token is None:
+ out_token = ''
+ result = send_sasl_negotiation(connection, controls, out_token)
+ in_token = result['saslCreds']
+ try:
+ # This raised an exception in gssapi<1.1.2 if the context was
+ # incomplete, but was fixed in
+ # https://github.com/pythongssapi/python-gssapi/pull/70
+ if ctx.complete:
+ break
+ except gssapi.exceptions.MissingContextError:
+ pass
+
+ unwrapped_token = ctx.unwrap(in_token)
+ if len(unwrapped_token.message) != 4:
+ raise LDAPCommunicationError("Incorrect response from server")
+
+ server_security_layers = unwrapped_token.message[0]
+ if not isinstance(server_security_layers, int):
+ server_security_layers = ord(server_security_layers)
+ if server_security_layers in (0, NO_SECURITY_LAYER):
+ if unwrapped_token.message[1:] != '\x00\x00\x00':
+ raise LDAPCommunicationError("Server max buffer size must be 0 if no security layer")
+ if not (server_security_layers & NO_SECURITY_LAYER):
+ raise LDAPCommunicationError("Server requires a security layer, but this is not implemented")
+
+ client_security_layers = bytearray([NO_SECURITY_LAYER, 0, 0, 0])
+ out_token = ctx.wrap(bytes(client_security_layers)+authz_id, False)
+ return send_sasl_negotiation(connection, controls, out_token.message)
+ except (gssapi.exceptions.GSSError, LDAPCommunicationError):
+ abort_sasl_negotiation(connection, controls)
+ raise
diff --git a/ldap3/strategy/base.py b/ldap3/strategy/base.py
index 568459e..1acc732 100644
--- a/ldap3/strategy/base.py
+++ b/ldap3/strategy/base.py
@@ -24,15 +24,20 @@
# If not, see <http://www.gnu.org/licenses/>.
import socket
+try: # try to discover if unix sockets are available for LDAP over IPC (ldapi:// scheme)
+ # noinspection PyUnresolvedReferences
+ from socket import AF_UNIX
+ unix_socket_available = True
+except ImportError:
+ unix_socket_available = False
from struct import pack
from platform import system
-from time import sleep
from random import choice
from .. import SYNC, ANONYMOUS, get_config_parameter, BASE, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, NO_ATTRIBUTES
from ..core.results import DO_NOT_RAISE_EXCEPTIONS, RESULT_REFERRAL
from ..core.exceptions import LDAPOperationResult, LDAPSASLBindInProgressError, LDAPSocketOpenError, LDAPSessionTerminatedByServerError,\
- LDAPUnknownResponseError, LDAPUnknownRequestError, LDAPReferralError, communication_exception_factory, \
+ LDAPUnknownResponseError, LDAPUnknownRequestError, LDAPReferralError, communication_exception_factory, LDAPStartTLSError, \
LDAPSocketSendError, LDAPExceptionError, LDAPControlError, LDAPResponseTimeoutError, LDAPTransactionError
from ..utils.uri import parse_uri
from ..protocol.rfc4511 import LDAPMessage, ProtocolOp, MessageID, SearchResultEntry
@@ -78,6 +83,7 @@ class BaseStrategy(object):
self.pooled = None # Indicates a connection with a connection pool
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()
self.referral_cache = {}
+ self.thread_safe = False # Indicates that connection can be used in a multithread application
if log_enabled(BASIC):
log(BASIC, 'instantiated <%s>: <%s>', self.__class__.__name__, self)
@@ -141,9 +147,6 @@ class BaseStrategy(object):
if log_enabled(ERROR):
log(ERROR, 'unable to open socket for <%s>', self.connection)
raise LDAPSocketOpenError('unable to open socket', exception_history)
- if log_enabled(ERROR):
- log(ERROR, 'unable to open socket for <%s>', self.connection)
- raise LDAPSocketOpenError('unable to open socket', exception_history)
elif not self.connection.server.current_address:
if log_enabled(ERROR):
log(ERROR, 'invalid server address for <%s>', self.connection)
@@ -151,7 +154,6 @@ class BaseStrategy(object):
self.connection._deferred_open = False
self._start_listen()
- # self.connection.do_auto_bind()
if log_enabled(NETWORK):
log(NETWORK, 'connection open for <%s>', self.connection)
@@ -199,7 +201,6 @@ class BaseStrategy(object):
log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
# raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error)
raise communication_exception_factory(LDAPSocketOpenError, type(e)(str(e)))(self.connection.last_error)
-
# Try to bind the socket locally before connecting to the remote address
# We go through our connection's source ports and try to bind our socket to our connection's source address
# with them.
@@ -210,25 +211,26 @@ class BaseStrategy(object):
# issue when no source address/port is specified if the system checking server availability is running
# as a very unprivileged user.
last_bind_exc = None
- socket_bind_succeeded = False
- for source_port in self.connection.source_port_list:
- try:
- self.connection.socket.bind((self.connection.source_address, source_port))
- socket_bind_succeeded = True
- break
- except Exception as bind_ex:
- last_bind_exc = bind_ex
- # we'll always end up logging at error level if we cannot bind any ports to the address locally.
- # but if some work and some don't you probably don't want the ones that don't at ERROR level
- if log_enabled(NETWORK):
- log(NETWORK, 'Unable to bind to local address <%s> with source port <%s> due to <%s>',
- self.connection.source_address, source_port, bind_ex)
- if not socket_bind_succeeded:
- self.connection.last_error = 'socket connection error while locally binding: ' + str(last_bind_exc)
- if log_enabled(ERROR):
- log(ERROR, 'Unable to locally bind to local address <%s> with any of the source ports <%s> for connection <%s due to <%s>',
- self.connection.source_address, self.connection.source_port_list, self.connection, last_bind_exc)
- raise communication_exception_factory(LDAPSocketOpenError, type(last_bind_exc)(str(last_bind_exc)))(last_bind_exc)
+ if unix_socket_available and self.connection.socket.family != socket.AF_UNIX:
+ socket_bind_succeeded = False
+ for source_port in self.connection.source_port_list:
+ try:
+ self.connection.socket.bind((self.connection.source_address, source_port))
+ socket_bind_succeeded = True
+ break
+ except Exception as bind_ex:
+ last_bind_exc = bind_ex
+ # we'll always end up logging at error level if we cannot bind any ports to the address locally.
+ # but if some work and some don't you probably don't want the ones that don't at ERROR level
+ if log_enabled(NETWORK):
+ log(NETWORK, 'Unable to bind to local address <%s> with source port <%s> due to <%s>',
+ self.connection.source_address, source_port, bind_ex)
+ if not socket_bind_succeeded:
+ self.connection.last_error = 'socket connection error while locally binding: ' + str(last_bind_exc)
+ if log_enabled(ERROR):
+ log(ERROR, 'Unable to locally bind to local address <%s> with any of the source ports <%s> for connection <%s due to <%s>',
+ self.connection.source_address, self.connection.source_port_list, self.connection, last_bind_exc)
+ raise communication_exception_factory(LDAPSocketOpenError, type(last_bind_exc)(str(last_bind_exc)))(last_bind_exc)
try: # set socket timeout for opening connection
if self.connection.server.connect_timeout:
@@ -273,7 +275,6 @@ class BaseStrategy(object):
self.connection.last_error = 'socket ssl wrapping error: ' + str(e)
if log_enabled(ERROR):
log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
- # raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error)
raise communication_exception_factory(LDAPSocketOpenError, type(e)(str(e)))(self.connection.last_error)
if self.connection.usage:
self.connection._usage.open_sockets += 1
@@ -693,13 +694,20 @@ class BaseStrategy(object):
search_scope=BASE,
dereference_aliases=request['dereferenceAlias'],
attributes=[attr_type + ';range=' + str(int(high_range) + 1) + '-*'])
- if isinstance(result, bool):
- if result:
- current_response = self.connection.response[0]
+ if self.connection.strategy.thread_safe:
+ status, result, _response, _ = result
+ else:
+ status = result
+ result = self.connection.result
+ _response = self.connection.response
+
+ if self.connection.strategy.sync:
+ if status:
+ current_response = _response[0]
else:
done = True
else:
- current_response, _ = self.get_response(result)
+ current_response, _ = self.get_response(status)
current_response = current_response[0]
if not done:
@@ -778,7 +786,12 @@ class BaseStrategy(object):
referral_connection.open()
referral_connection.strategy._referrals = self._referrals
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
- referral_connection.start_tls()
+ if not referral_connection.start_tls():
+ error = 'start_tls in referral not successful' + (' - ' + referral_connection.last_error if referral_connection.last_error else '')
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', error, self)
+ self.unbind()
+ raise LDAPStartTLSError(error)
if self.connection.bound:
referral_connection.bind()
diff --git a/ldap3/strategy/restartable.py b/ldap3/strategy/restartable.py
index d739f41..bd689f9 100644
--- a/ldap3/strategy/restartable.py
+++ b/ldap3/strategy/restartable.py
@@ -1,255 +1,260 @@
-"""
-"""
-
-# Created on 2014.03.04
-#
-# Author: Giovanni Cannata
-#
-# Copyright 2014 - 2020 Giovanni Cannata
-#
-# This file is part of ldap3.
-#
-# ldap3 is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ldap3 is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with ldap3 in the COPYING and COPYING.LESSER files.
-# If not, see <http://www.gnu.org/licenses/>.
-
-from time import sleep
-import socket
-
-from .. import get_config_parameter
-from .sync import SyncStrategy
-from ..core.exceptions import LDAPSocketOpenError, LDAPOperationResult, LDAPMaximumRetriesError
-from ..utils.log import log, log_enabled, ERROR, BASIC
-
-
-# noinspection PyBroadException,PyProtectedMember
-class RestartableStrategy(SyncStrategy):
- def __init__(self, ldap_connection):
- SyncStrategy.__init__(self, ldap_connection)
- self.sync = True
- self.no_real_dsa = False
- self.pooled = False
- self.can_stream = False
- self.restartable_sleep_time = get_config_parameter('RESTARTABLE_SLEEPTIME')
- self.restartable_tries = get_config_parameter('RESTARTABLE_TRIES')
- self._restarting = False
- self._last_bind_controls = None
- self._current_message_type = None
- self._current_request = None
- self._current_controls = None
- self._restart_tls = None
- self.exception_history = []
-
- def open(self, reset_usage=False, read_server_info=True):
- SyncStrategy.open(self, reset_usage, read_server_info)
-
- def _open_socket(self, address, use_ssl=False, unix_socket=False):
- """
- Try to open and connect a socket to a Server
- raise LDAPExceptionError if unable to open or connect socket
- if connection is restartable tries for the number of restarting requested or forever
- """
- try:
- SyncStrategy._open_socket(self, address, use_ssl, unix_socket) # try to open socket using SyncWait
- self._reset_exception_history()
- return
- except Exception as e: # machinery for restartable connection
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
-
- if not self._restarting: # if not already performing a restart
- self._restarting = True
- counter = self.restartable_tries
- while counter > 0: # includes restartable_tries == True
- if log_enabled(BASIC):
- log(BASIC, 'try #%d to open Restartable connection <%s>', self.restartable_tries - counter, self.connection)
- sleep(self.restartable_sleep_time)
- if not self.connection.closed:
- try: # resetting connection
- self.connection.unbind()
- except (socket.error, LDAPSocketOpenError): # don't trace catch socket errors because socket could already be closed
- pass
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- try: # reissuing same operation
- if self.connection.server_pool:
- new_server = self.connection.server_pool.get_server(self.connection) # get a server from the server_pool if available
- if self.connection.server != new_server:
- self.connection.server = new_server
- if self.connection.usage:
- self.connection._usage.servers_from_pool += 1
- SyncStrategy._open_socket(self, address, use_ssl, unix_socket) # calls super (not restartable) _open_socket()
- if self.connection.usage:
- self.connection._usage.restartable_successes += 1
- self.connection.closed = False
- self._restarting = False
- self._reset_exception_history()
- return
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- if self.connection.usage:
- self.connection._usage.restartable_failures += 1
- if not isinstance(self.restartable_tries, bool):
- counter -= 1
- self._restarting = False
- self.connection.last_error = 'restartable connection strategy failed while opening socket'
- if log_enabled(ERROR):
- log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
- raise LDAPMaximumRetriesError(self.connection.last_error, self.exception_history, self.restartable_tries)
-
- def send(self, message_type, request, controls=None):
- self._current_message_type = message_type
- self._current_request = request
- self._current_controls = controls
- if not self._restart_tls: # RFCs doesn't define how to stop tls once started
- self._restart_tls = self.connection.tls_started
- if message_type == 'bindRequest': # stores controls used in bind operation to be used again when restarting the connection
- self._last_bind_controls = controls
-
- try:
- message_id = SyncStrategy.send(self, message_type, request, controls) # tries to send using SyncWait
- self._reset_exception_history()
- return message_id
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- if not self._restarting: # machinery for restartable connection
- self._restarting = True
- counter = self.restartable_tries
- while counter > 0:
- if log_enabled(BASIC):
- log(BASIC, 'try #%d to send in Restartable connection <%s>', self.restartable_tries - counter, self.connection)
- sleep(self.restartable_sleep_time)
- if not self.connection.closed:
- try: # resetting connection
- self.connection.unbind()
- except (socket.error, LDAPSocketOpenError): # don't trace socket errors because socket could already be closed
- pass
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- failure = False
- try: # reopening connection
- self.connection.open(reset_usage=False, read_server_info=False)
- if self._restart_tls: # restart tls if start_tls was previously used
- self.connection.start_tls(read_server_info=False)
- if message_type != 'bindRequest':
- self.connection.bind(read_server_info=False, controls=self._last_bind_controls) # binds with previously used controls unless the request is already a bindRequest
- if not self.connection.server.schema and not self.connection.server.info:
- self.connection.refresh_server_info()
- else:
- self.connection._fire_deferred(read_info=False) # in case of lazy connection, not open by the refresh_server_info
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- failure = True
-
- if not failure:
- try: # reissuing same operation
- ret_value = self.connection.send(message_type, request, controls)
- if self.connection.usage:
- self.connection._usage.restartable_successes += 1
- self._restarting = False
- self._reset_exception_history()
- return ret_value # successful send
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- failure = True
-
- if failure and self.connection.usage:
- self.connection._usage.restartable_failures += 1
-
- if not isinstance(self.restartable_tries, bool):
- counter -= 1
-
- self._restarting = False
-
- self.connection.last_error = 'restartable connection failed to send'
- if log_enabled(ERROR):
- log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
- raise LDAPMaximumRetriesError(self.connection.last_error, self.exception_history, self.restartable_tries)
-
- def post_send_single_response(self, message_id):
- try:
- ret_value = SyncStrategy.post_send_single_response(self, message_id)
- self._reset_exception_history()
- return ret_value
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
-
- # if an LDAPExceptionError is raised then resend the request
- try:
- ret_value = SyncStrategy.post_send_single_response(self, self.send(self._current_message_type, self._current_request, self._current_controls))
- self._reset_exception_history()
- return ret_value
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- if not isinstance(e, LDAPOperationResult):
- self.connection.last_error = 'restartable connection strategy failed in post_send_single_response'
- if log_enabled(ERROR):
- log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
- raise
-
- def post_send_search(self, message_id):
- try:
- ret_value = SyncStrategy.post_send_search(self, message_id)
- self._reset_exception_history()
- return ret_value
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
-
- # if an LDAPExceptionError is raised then resend the request
- try:
- ret_value = SyncStrategy.post_send_search(self, self.connection.send(self._current_message_type, self._current_request, self._current_controls))
- self._reset_exception_history()
- return ret_value
- except Exception as e:
- if log_enabled(ERROR):
- log(ERROR, '<%s> while restarting <%s>', e, self.connection)
- self._add_exception_to_history(type(e)(str(e)))
- if not isinstance(e, LDAPOperationResult):
- self.connection.last_error = e.args
- if log_enabled(ERROR):
- log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
- raise e
-
- def _add_exception_to_history(self, exc):
- if not isinstance(self.restartable_tries, bool): # doesn't accumulate when restarting forever
- if not isinstance(exc, LDAPMaximumRetriesError): # doesn't add the LDAPMaximumRetriesError exception
- self.exception_history.append(exc)
-
- def _reset_exception_history(self):
- if self.exception_history:
- self.exception_history = []
-
- def get_stream(self):
- raise NotImplementedError
-
- def set_stream(self, value):
- raise NotImplementedError
+"""
+"""
+
+# Created on 2014.03.04
+#
+# Author: Giovanni Cannata
+#
+# Copyright 2014 - 2020 Giovanni Cannata
+#
+# This file is part of ldap3.
+#
+# ldap3 is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ldap3 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with ldap3 in the COPYING and COPYING.LESSER files.
+# If not, see <http://www.gnu.org/licenses/>.
+
+from time import sleep
+import socket
+
+from .. import get_config_parameter
+from .sync import SyncStrategy
+from ..core.exceptions import LDAPSocketOpenError, LDAPOperationResult, LDAPMaximumRetriesError, LDAPStartTLSError
+from ..utils.log import log, log_enabled, ERROR, BASIC
+
+
+# noinspection PyBroadException,PyProtectedMember
+class RestartableStrategy(SyncStrategy):
+ def __init__(self, ldap_connection):
+ SyncStrategy.__init__(self, ldap_connection)
+ self.sync = True
+ self.no_real_dsa = False
+ self.pooled = False
+ self.can_stream = False
+ self.restartable_sleep_time = get_config_parameter('RESTARTABLE_SLEEPTIME')
+ self.restartable_tries = get_config_parameter('RESTARTABLE_TRIES')
+ self._restarting = False
+ self._last_bind_controls = None
+ self._current_message_type = None
+ self._current_request = None
+ self._current_controls = None
+ self._restart_tls = None
+ self.exception_history = []
+
+ def open(self, reset_usage=False, read_server_info=True):
+ SyncStrategy.open(self, reset_usage, read_server_info)
+
+ def _open_socket(self, address, use_ssl=False, unix_socket=False):
+ """
+ Try to open and connect a socket to a Server
+ raise LDAPExceptionError if unable to open or connect socket
+ if connection is restartable tries for the number of restarting requested or forever
+ """
+ try:
+ SyncStrategy._open_socket(self, address, use_ssl, unix_socket) # try to open socket using SyncWait
+ self._reset_exception_history()
+ return
+ except Exception as e: # machinery for restartable connection
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+
+ if not self._restarting: # if not already performing a restart
+ self._restarting = True
+ counter = self.restartable_tries
+ while counter > 0: # includes restartable_tries == True
+ if log_enabled(BASIC):
+ log(BASIC, 'try #%d to open Restartable connection <%s>', self.restartable_tries - counter, self.connection)
+ sleep(self.restartable_sleep_time)
+ if not self.connection.closed:
+ try: # resetting connection
+ self.connection.unbind()
+ except (socket.error, LDAPSocketOpenError): # don't trace catch socket errors because socket could already be closed
+ pass
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ try: # reissuing same operation
+ if self.connection.server_pool:
+ new_server = self.connection.server_pool.get_server(self.connection) # get a server from the server_pool if available
+ if self.connection.server != new_server:
+ self.connection.server = new_server
+ if self.connection.usage:
+ self.connection._usage.servers_from_pool += 1
+ SyncStrategy._open_socket(self, address, use_ssl, unix_socket) # calls super (not restartable) _open_socket()
+ if self.connection.usage:
+ self.connection._usage.restartable_successes += 1
+ self.connection.closed = False
+ self._restarting = False
+ self._reset_exception_history()
+ return
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ if self.connection.usage:
+ self.connection._usage.restartable_failures += 1
+ if not isinstance(self.restartable_tries, bool):
+ counter -= 1
+ self._restarting = False
+ self.connection.last_error = 'restartable connection strategy failed while opening socket'
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
+ raise LDAPMaximumRetriesError(self.connection.last_error, self.exception_history, self.restartable_tries)
+
+ def send(self, message_type, request, controls=None):
+ self._current_message_type = message_type
+ self._current_request = request
+ self._current_controls = controls
+ if not self._restart_tls: # RFCs doesn't define how to stop tls once started
+ self._restart_tls = self.connection.tls_started
+ if message_type == 'bindRequest': # stores controls used in bind operation to be used again when restarting the connection
+ self._last_bind_controls = controls
+
+ try:
+ message_id = SyncStrategy.send(self, message_type, request, controls) # tries to send using SyncWait
+ self._reset_exception_history()
+ return message_id
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ if not self._restarting: # machinery for restartable connection
+ self._restarting = True
+ counter = self.restartable_tries
+ while counter > 0:
+ if log_enabled(BASIC):
+ log(BASIC, 'try #%d to send in Restartable connection <%s>', self.restartable_tries - counter, self.connection)
+ sleep(self.restartable_sleep_time)
+ if not self.connection.closed:
+ try: # resetting connection
+ self.connection.unbind()
+ except (socket.error, LDAPSocketOpenError): # don't trace socket errors because socket could already be closed
+ pass
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ failure = False
+ try: # reopening connection
+ self.connection.open(reset_usage=False, read_server_info=False)
+ if self._restart_tls: # restart tls if start_tls was previously used
+ if self.connection.start_tls(read_server_info=False):
+ error = 'restart tls in restartable not successful' + (' - ' + self.connection.last_error if self.connection.last_error else '')
+ if log_enabled(ERROR):
+ log(ERROR, '%s for <%s>', error, self)
+ self.connection.unbind()
+ raise LDAPStartTLSError(error)
+ if message_type != 'bindRequest':
+ self.connection.bind(read_server_info=False, controls=self._last_bind_controls) # binds with previously used controls unless the request is already a bindRequest
+ if not self.connection.server.schema and not self.connection.server.info:
+ self.connection.refresh_server_info()
+ else:
+ self.connection._fire_deferred(read_info=False) # in case of lazy connection, not open by the refresh_server_info
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ failure = True
+
+ if not failure:
+ try: # reissuing same operation
+ ret_value = self.connection.send(message_type, request, controls)
+ if self.connection.usage:
+ self.connection._usage.restartable_successes += 1
+ self._restarting = False
+ self._reset_exception_history()
+ return ret_value # successful send
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ failure = True
+
+ if failure and self.connection.usage:
+ self.connection._usage.restartable_failures += 1
+
+ if not isinstance(self.restartable_tries, bool):
+ counter -= 1
+
+ self._restarting = False
+
+ self.connection.last_error = 'restartable connection failed to send'
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
+ raise LDAPMaximumRetriesError(self.connection.last_error, self.exception_history, self.restartable_tries)
+
+ def post_send_single_response(self, message_id):
+ try:
+ ret_value = SyncStrategy.post_send_single_response(self, message_id)
+ self._reset_exception_history()
+ return ret_value
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+
+ # if an LDAPExceptionError is raised then resend the request
+ try:
+ ret_value = SyncStrategy.post_send_single_response(self, self.send(self._current_message_type, self._current_request, self._current_controls))
+ self._reset_exception_history()
+ return ret_value
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ if not isinstance(e, LDAPOperationResult):
+ self.connection.last_error = 'restartable connection strategy failed in post_send_single_response'
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
+ raise
+
+ def post_send_search(self, message_id):
+ try:
+ ret_value = SyncStrategy.post_send_search(self, message_id)
+ self._reset_exception_history()
+ return ret_value
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+
+ # if an LDAPExceptionError is raised then resend the request
+ try:
+ ret_value = SyncStrategy.post_send_search(self, self.connection.send(self._current_message_type, self._current_request, self._current_controls))
+ self._reset_exception_history()
+ return ret_value
+ except Exception as e:
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> while restarting <%s>', e, self.connection)
+ self._add_exception_to_history(type(e)(str(e)))
+ if not isinstance(e, LDAPOperationResult):
+ self.connection.last_error = e.args
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
+ raise e
+
+ def _add_exception_to_history(self, exc):
+ if not isinstance(self.restartable_tries, bool): # doesn't accumulate when restarting forever
+ if not isinstance(exc, LDAPMaximumRetriesError): # doesn't add the LDAPMaximumRetriesError exception
+ self.exception_history.append(exc)
+
+ def _reset_exception_history(self):
+ if self.exception_history:
+ self.exception_history = []
+
+ def get_stream(self):
+ raise NotImplementedError
+
+ def set_stream(self, value):
+ raise NotImplementedError
diff --git a/ldap3/strategy/safeSync.py b/ldap3/strategy/safeSync.py
new file mode 100644
index 0000000..dcd2b92
--- /dev/null
+++ b/ldap3/strategy/safeSync.py
@@ -0,0 +1,32 @@
+"""
+"""
+
+# Created on 2020.07.12
+#
+# Author: Giovanni Cannata
+#
+# Copyright 2013 - 2020 Giovanni Cannata
+#
+# This file is part of ldap3.
+#
+# ldap3 is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ldap3 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with ldap3 in the COPYING and COPYING.LESSER files.
+# If not, see <http://www.gnu.org/licenses/>.
+
+from .sync import SyncStrategy
+
+
+class SafeSyncStrategy(SyncStrategy):
+ def __init__(self, ldap_connection):
+ SyncStrategy.__init__(self, ldap_connection)
+ self.thread_safe = True
diff --git a/ldap3/strategy/sync.py b/ldap3/strategy/sync.py
index fdb1441..19883a6 100644
--- a/ldap3/strategy/sync.py
+++ b/ldap3/strategy/sync.py
@@ -54,7 +54,7 @@ class SyncStrategy(BaseStrategy):
def open(self, reset_usage=True, read_server_info=True):
BaseStrategy.open(self, reset_usage, read_server_info)
- if read_server_info:
+ if read_server_info and not self.connection._deferred_open:
try:
self.connection.refresh_server_info()
except LDAPOperationResult: # catch errors from server if raise_exception = True
diff --git a/ldap3/utils/config.py b/ldap3/utils/config.py
index e3edbf8..6e3aa97 100644
--- a/ldap3/utils/config.py
+++ b/ldap3/utils/config.py
@@ -91,6 +91,7 @@ _ADDITIONAL_SERVER_ENCODINGS = ['latin-1', 'koi8-r'] # some broken LDAP impleme
_ADDITIONAL_CLIENT_ENCODINGS = ['utf-8']
_IGNORE_MALFORMED_SCHEMA = False # some flaky LDAP servers returns malformed schema. If True no expection is raised and schema is thrown away
_DEFAULT_SERVER_ENCODING = 'utf-8' # should always be utf-8
+_LDIF_LINE_LENGTH = 78 # as stated in RFC 2849
if stdin and hasattr(stdin, 'encoding') and stdin.encoding:
_DEFAULT_CLIENT_ENCODING = stdin.encoding
@@ -124,7 +125,8 @@ PARAMETERS = ['CASE_INSENSITIVE_ATTRIBUTE_NAMES',
'ADDITIONAL_CLIENT_ENCODINGS',
'IGNORE_MALFORMED_SCHEMA',
'ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF',
- 'IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF'
+ 'IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF',
+ 'LDIF_LINE_LENGTH'
]
@@ -205,6 +207,8 @@ def get_config_parameter(parameter):
return _IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF
else:
return [_IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF]
+ elif parameter == 'LDIF_LINE_LENGTH': # Integer
+ return _LDIF_LINE_LENGTH
raise LDAPConfigurationParameterError('configuration parameter %s not valid' % parameter)
@@ -288,5 +292,8 @@ def set_config_parameter(parameter, value):
elif parameter == 'IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF':
global _IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF
_IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF = value
+ elif parameter == 'LDIF_LINE_LENGTH':
+ global _LDIF_LINE_LENGTH
+ _LDIF_LINE_LENGTH = value
else:
raise LDAPConfigurationParameterError('unable to set configuration parameter %s' % parameter)
diff --git a/ldap3/utils/conv.py b/ldap3/utils/conv.py
index b000e30..514faad 100644
--- a/ldap3/utils/conv.py
+++ b/ldap3/utils/conv.py
@@ -183,7 +183,7 @@ def json_hook(obj):
# noinspection PyProtectedMember
-def format_json(obj):
+def format_json(obj, iso_format=False):
if isinstance(obj, CaseInsensitiveDict):
return obj._store
@@ -194,6 +194,8 @@ def format_json(obj):
return obj
if isinstance(obj, datetime.timedelta):
+ if iso_format:
+ return obj.isoformat()
return str(obj)
if str is bytes: # Python 2
diff --git a/ldap3/utils/ntlm.py b/ldap3/utils/ntlm.py
index f91776d..5271669 100644
--- a/ldap3/utils/ntlm.py
+++ b/ldap3/utils/ntlm.py
@@ -493,5 +493,13 @@ class NtlmClient(object):
# The specified password is an LM:NTLM hash
password_digest = binascii.unhexlify(passparts[1])
else:
- password_digest = hashlib.new('MD4', self._password.encode('utf-16-le')).digest()
+ try:
+ password_digest = hashlib.new('MD4', self._password.encode('utf-16-le')).digest()
+ except ValueError as e:
+ try:
+ from Crypto.Hash import MD4 # try with the Crypto library if present
+ password_digest = MD4.new(self._password.encode('utf-16-le')).digest()
+ except ImportError:
+ raise e # raise original exception
+
return hmac.new(password_digest, (self.user_name.upper() + self.user_domain).encode('utf-16-le'), digestmod=hashlib.md5).digest()
diff --git a/ldap3/utils/port_validators.py b/ldap3/utils/port_validators.py
index a35e13e..c46b752 100644
--- a/ldap3/utils/port_validators.py
+++ b/ldap3/utils/port_validators.py
@@ -17,7 +17,7 @@ def check_port_and_port_list(port, port_list):
Return an error message indicating what is invalid if something isn't valid.
"""
if port is not None and port_list is not None:
- return'Cannot specify both a source port and a source port list'
+ return 'Cannot specify both a source port and a source port list'
elif port is not None:
if isinstance(port, int):
if port not in range(0, 65535):
diff --git a/ldap3/utils/uri.py b/ldap3/utils/uri.py
index 02c8e5a..3e56674 100644
--- a/ldap3/utils/uri.py
+++ b/ldap3/utils/uri.py
@@ -1,118 +1,118 @@
-"""
-"""
-
-# Created on 2014.09.08
-#
-# Author: Giovanni Cannata
-#
-# Copyright 2014 - 2020 Giovanni Cannata
-#
-# This file is part of ldap3.
-#
-# ldap3 is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ldap3 is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with ldap3 in the COPYING and COPYING.LESSER files.
-# If not, see <http://www.gnu.org/licenses/>.
-
-try:
- from urllib.parse import unquote # Python3
-except ImportError:
- from urllib import unquote # Python 2
-
-from .. import SUBTREE, BASE, LEVEL
-
-
-def parse_uri(uri):
- """
- Decode LDAP URI as specified in RFC 4516 relaxing specifications
- permitting 'ldaps' as scheme for ssl-ldap
- """
-
- # ldapurl = scheme COLON SLASH SLASH [host [COLON port]]
- # [SLASH dn [QUESTION [attributes]
- # [QUESTION [scope] [QUESTION [filter]
- # [QUESTION extensions]]]]]
- # ; <host> and <port> are defined
- # ; in Sections 3.2.2 and 3.2.3
- # ; of [RFC3986].
- # ; <filter> is from Section 3 of
- # ; [RFC4515], subject to the
- # ; provisions of the
- # ; "Percent-Encoding" section
- # ; below.
- #
- # scheme = "ldap" / "ldaps" <== not RFC4516 compliant (original is 'scheme = "ldap"')
- # dn = distinguishedName ; From Section 3 of [RFC4514],
- # ; subject to the provisions of
- # ; the "Percent-Encoding"
- # ; section below.
- #
- # attributes = attrdesc *(COMMA attrdesc)
- # attrdesc = selector *(COMMA selector)
- # selector = attributeSelector ; From Section 4.5.1 of
- # ; [RFC4511], subject to the
- # ; provisions of the
- # ; "Percent-Encoding" section
- # ; below.
- #
- # scope = "base" / "one" / "sub"
- # extensions = extension *(COMMA extension)
- # extension = [EXCLAMATION] extype [EQUALS exvalue]
- # extype = oid ; From section 1.4 of [RFC4512].
- #
- # exvalue = LDAPString ; From section 4.1.2 of
- # ; [RFC4511], subject to the
- # ; provisions of the
- # ; "Percent-Encoding" section
- # ; below.
- #
- # EXCLAMATION = %x21 ; exclamation mark ("!")
- # SLASH = %x2F ; forward slash ("/")
- # COLON = %x3A ; colon (":")
- # QUESTION = %x3F ; question mark ("?")
-
- uri_components = dict()
- parts = unquote(uri).split('?') # encoding defaults to utf-8 in Python 3
- scheme, sep, remain = parts[0].partition('://')
- if sep != '://' or scheme not in ['ldap', 'ldaps']:
- return None
-
- address, _, uri_components['base'] = remain.partition('/')
-
- uri_components['ssl'] = True if scheme == 'ldaps' else False
- uri_components['host'], sep, uri_components['port'] = address.partition(':')
- if sep != ':':
- if uri_components['ssl']:
- uri_components['port'] = 636
- else:
- uri_components['port'] = None
- else:
- if not uri_components['port'].isdigit() or not (0 < int(uri_components['port']) < 65536):
- return None
- else:
- uri_components['port'] = int(uri_components['port'])
-
- uri_components['attributes'] = parts[1].split(',') if len(parts) > 1 else None
- uri_components['scope'] = parts[2] if len(parts) > 2 else None
- if uri_components['scope'] == 'base':
- uri_components['scope'] = BASE
- elif uri_components['scope'] == 'sub':
- uri_components['scope'] = SUBTREE
- elif uri_components['scope'] == 'one':
- uri_components['scope'] = LEVEL
- elif uri_components['scope']:
- return None
-
- uri_components['filter'] = parts[3] if len(parts) > 3 else None
- uri_components['extensions'] = parts[4].split(',') if len(parts) > 4 else None
-
- return uri_components
+"""
+"""
+
+# Created on 2014.09.08
+#
+# Author: Giovanni Cannata
+#
+# Copyright 2014 - 2020 Giovanni Cannata
+#
+# This file is part of ldap3.
+#
+# ldap3 is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ldap3 is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with ldap3 in the COPYING and COPYING.LESSER files.
+# If not, see <http://www.gnu.org/licenses/>.
+
+try:
+ from urllib.parse import unquote # Python3
+except ImportError:
+ from urllib import unquote # Python 2
+
+from .. import SUBTREE, BASE, LEVEL
+
+
+def parse_uri(uri):
+ """
+ Decode LDAP URI as specified in RFC 4516 relaxing specifications
+ permitting 'ldaps' as scheme for ssl-ldap
+ """
+
+ # ldapurl = scheme COLON SLASH SLASH [host [COLON port]]
+ # [SLASH dn [QUESTION [attributes]
+ # [QUESTION [scope] [QUESTION [filter]
+ # [QUESTION extensions]]]]]
+ # ; <host> and <port> are defined
+ # ; in Sections 3.2.2 and 3.2.3
+ # ; of [RFC3986].
+ # ; <filter> is from Section 3 of
+ # ; [RFC4515], subject to the
+ # ; provisions of the
+ # ; "Percent-Encoding" section
+ # ; below.
+ #
+ # scheme = "ldap" / "ldaps" <== not RFC4516 compliant (original is 'scheme = "ldap"')
+ # dn = distinguishedName ; From Section 3 of [RFC4514],
+ # ; subject to the provisions of
+ # ; the "Percent-Encoding"
+ # ; section below.
+ #
+ # attributes = attrdesc *(COMMA attrdesc)
+ # attrdesc = selector *(COMMA selector)
+ # selector = attributeSelector ; From Section 4.5.1 of
+ # ; [RFC4511], subject to the
+ # ; provisions of the
+ # ; "Percent-Encoding" section
+ # ; below.
+ #
+ # scope = "base" / "one" / "sub"
+ # extensions = extension *(COMMA extension)
+ # extension = [EXCLAMATION] extype [EQUALS exvalue]
+ # extype = oid ; From section 1.4 of [RFC4512].
+ #
+ # exvalue = LDAPString ; From section 4.1.2 of
+ # ; [RFC4511], subject to the
+ # ; provisions of the
+ # ; "Percent-Encoding" section
+ # ; below.
+ #
+ # EXCLAMATION = %x21 ; exclamation mark ("!")
+ # SLASH = %x2F ; forward slash ("/")
+ # COLON = %x3A ; colon (":")
+ # QUESTION = %x3F ; question mark ("?")
+
+ uri_components = dict()
+ parts = unquote(uri).split('?') # encoding defaults to utf-8 in Python 3
+ scheme, sep, remain = parts[0].partition('://')
+ if sep != '://' or scheme not in ['ldap', 'ldaps']:
+ return None
+
+ address, _, uri_components['base'] = remain.partition('/')
+
+ uri_components['ssl'] = True if scheme == 'ldaps' else False
+ uri_components['host'], sep, uri_components['port'] = address.partition(':')
+ if sep != ':':
+ if uri_components['ssl']:
+ uri_components['port'] = 636
+ else:
+ uri_components['port'] = None
+ else:
+ if not uri_components['port'].isdigit() or not (0 < int(uri_components['port']) < 65536):
+ return None
+ else:
+ uri_components['port'] = int(uri_components['port'])
+
+ uri_components['attributes'] = parts[1].split(',') if len(parts) > 1 and parts[1] else None
+ uri_components['scope'] = parts[2] if len(parts) > 2 else None
+ if uri_components['scope'] == 'base':
+ uri_components['scope'] = BASE
+ elif uri_components['scope'] == 'sub':
+ uri_components['scope'] = SUBTREE
+ elif uri_components['scope'] == 'one':
+ uri_components['scope'] = LEVEL
+ elif uri_components['scope']:
+ return None
+
+ uri_components['filter'] = parts[3] if len(parts) > 3 else None
+ uri_components['extensions'] = parts[4].split(',') if len(parts) > 4 else None
+
+ return uri_components
diff --git a/ldap3/version.py b/ldap3/version.py
index 531a578..d9ef535 100644
--- a/ldap3/version.py
+++ b/ldap3/version.py
@@ -1,9 +1,9 @@
# THIS FILE IS AUTO-GENERATED. PLEASE DO NOT MODIFY# version file for ldap3
-# generated on 2020-03-01 22:10:42.830758
-# on system uname_result(system='Windows', node='ELITE10GC', release='10', version='10.0.18362', machine='AMD64', processor='Intel64 Family 6 Model 58 Stepping 9, GenuineIntel')
-# with Python 3.8.1 - ('tags/v3.8.1:1b293b6', 'Dec 18 2019 23:11:46') - MSC v.1916 64 bit (AMD64)
+# generated on 2020-09-07 08:48:35.409733
+# on system uname_result(system='Windows', node='ELITE10GC', release='10', version='10.0.19041', machine='AMD64', processor='Intel64 Family 6 Model 58 Stepping 9, GenuineIntel')
+# with Python 3.8.5 - ('tags/v3.8.5:580fbb0', 'Jul 20 2020 15:57:54') - MSC v.1924 64 bit (AMD64)
#
-__version__ = '2.7'
+__version__ = '2.8.1'
__author__ = 'Giovanni Cannata'
__email__ = 'cannatag@gmail.com'
__url__ = 'https://github.com/cannatag/ldap3'