summaryrefslogtreecommitdiff
path: root/ldap3/strategy/base.py
diff options
context:
space:
mode:
authorBrian May <bam@debian.org>2017-03-18 16:25:55 +1100
committerBrian May <bam@debian.org>2017-03-18 16:25:55 +1100
commitaa444baccb08ec3a32c3c7ecf749c4bda7d77a5b (patch)
treedf15b1327af334b87f452b7ebe3a097af8bb70aa /ldap3/strategy/base.py
parent193a0093002de7f6b0acbdcfcee2f83953c1936b (diff)
New upstream version 2.2.2
Diffstat (limited to 'ldap3/strategy/base.py')
-rw-r--r--ldap3/strategy/base.py174
1 files changed, 109 insertions, 65 deletions
diff --git a/ldap3/strategy/base.py b/ldap3/strategy/base.py
index 4db59c0..f2c95d3 100644
--- a/ldap3/strategy/base.py
+++ b/ldap3/strategy/base.py
@@ -5,7 +5,7 @@
#
# Author: Giovanni Cannata
#
-# Copyright 2015 Giovanni Cannata
+# Copyright 2013, 2014, 2015, 2016, 2017 Giovanni Cannata
#
# This file is part of ldap3.
#
@@ -17,7 +17,7 @@
# 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.
+# GNU Lesser General Public License for more dectails.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
@@ -31,16 +31,18 @@ from time import sleep
from random import choice
from datetime import datetime
-from .. import SESSION_TERMINATED_BY_SERVER, SYNC, ANONYMOUS, get_config_parameter, DO_NOT_RAISE_EXCEPTIONS, RESULT_REFERRAL, RESPONSE_COMPLETE, BASE
+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, \
- LDAPSocketSendError, LDAPExceptionError, LDAPControlsError, LDAPResponseTimeoutError
+ LDAPSocketSendError, LDAPExceptionError, LDAPControlError, LDAPResponseTimeoutError, LDAPTransactionError
from ..utils.uri import parse_uri
-from ..protocol.rfc4511 import LDAPMessage, ProtocolOp, MessageID
+from ..protocol.rfc4511 import LDAPMessage, ProtocolOp, MessageID, SearchResultEntry
from ..operation.add import add_response_to_dict, add_request_to_dict
from ..operation.modify import modify_request_to_dict, modify_response_to_dict
from ..operation.search import search_result_reference_response_to_dict, search_result_done_response_to_dict,\
- search_result_entry_response_to_dict, search_request_to_dict, search_result_entry_response_to_dict_fast, search_result_reference_response_to_dict_fast
+ search_result_entry_response_to_dict, search_request_to_dict, search_result_entry_response_to_dict_fast,\
+ search_result_reference_response_to_dict_fast, attributes_to_dict, attributes_to_dict_fast
from ..operation.bind import bind_response_to_dict, bind_request_to_dict, sicily_bind_response_to_dict, bind_response_to_dict_fast, \
sicily_bind_response_to_dict_fast
from ..operation.compare import compare_response_to_dict, compare_request_to_dict
@@ -57,6 +59,10 @@ from ..protocol.microsoft import DirSyncControlResponseValue
from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, NETWORK, EXTENDED, format_ldap_message
from ..utils.asn1 import encoder, decoder, ldap_result_to_dict_fast, decode_sequence
+SESSION_TERMINATED_BY_SERVER = 'TERMINATED_BY_SERVER'
+TRANSACTION_ERROR = 'TRANSACTION_ERROR'
+RESPONSE_COMPLETE = 'RESPONSE_FROM_SERVER_COMPLETE'
+
# noinspection PyProtectedMember
class BaseStrategy(object):
@@ -66,12 +72,13 @@ class BaseStrategy(object):
def __init__(self, ldap_connection):
self.connection = ldap_connection
- self._outstanding = dict()
+ self._outstanding = None
self._referrals = []
self.sync = None # indicates a synchronous connection
self.no_real_dsa = None # indicates a connection to a fake LDAP server
self.pooled = None # Indicates a connection with a connection pool
- self.can_stream = None # indicate 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.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 = {}
if log_enabled(BASIC):
log(BASIC, 'instantiated <%s>: <%s>', self.__class__.__name__, self)
@@ -96,7 +103,6 @@ class BaseStrategy(object):
self.connection.closed = False
if log_enabled(NETWORK):
log(NETWORK, 'deferring open connection for <%s>', self.connection)
-
else:
if not self.connection.closed and not self.connection._executing_deferred: # try to close connection if still open
self.close()
@@ -114,7 +120,7 @@ class BaseStrategy(object):
self.connection._usage.servers_from_pool += 1
exception_history = []
- if not self.no_real_dsa:
+ if not self.no_real_dsa: # tries to connect to a real server
for candidate_address in self.connection.server.candidate_addresses():
try:
if log_enabled(BASIC):
@@ -169,7 +175,7 @@ class BaseStrategy(object):
self.connection.request = None
self.connection.response = None
self.connection.tls_started = False
- self._outstanding = dict()
+ self._outstanding = None
self._referrals = []
if not self.connection.strategy.no_real_dsa:
@@ -214,8 +220,6 @@ class BaseStrategy(object):
if self.connection.server.connect_timeout:
self.connection.socket.settimeout(self.connection.server.connect_timeout)
self.connection.socket.connect(address[4])
- if self.connection.server.connect_timeout:
- self.connection.socket.settimeout(None) # disable socket timeout - socket is in blocking mode or in unblocking mode if receive_timeout is specifice in connection
except socket.error as e:
self.connection.last_error = 'socket connection error while opening: ' + str(e)
exc = e
@@ -239,6 +243,9 @@ class BaseStrategy(object):
log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
raise communication_exception_factory(LDAPSocketOpenError, exc)(self.connection.last_error)
+ if self.connection.server.connect_timeout:
+ self.connection.socket.settimeout(None) # disable socket connection timeout - socket is in blocking mode or in unblocking mode if receive_timeout is specified in connection
+
if self.connection.usage:
self.connection._usage.open_sockets += 1
@@ -288,8 +295,7 @@ class BaseStrategy(object):
message_controls = build_controls_list(controls)
if message_controls is not None:
ldap_message['controls'] = message_controls
- self.connection.request = BaseStrategy.decode_request(ldap_message)
- self.connection.request['controls'] = controls
+ self.connection.request = BaseStrategy.decode_request(message_type, request, controls)
self._outstanding[message_id] = self.connection.request
self.sending(ldap_message)
else:
@@ -300,7 +306,7 @@ class BaseStrategy(object):
return message_id
- def get_response(self, message_id, timeout=None):
+ def get_response(self, message_id, timeout=None, get_request=False):
"""
Get response LDAP messages
Responses are returned by the underlying connection strategy
@@ -313,6 +319,7 @@ class BaseStrategy(object):
timeout = get_config_parameter('RESPONSE_WAITING_TIMEOUT')
response = None
result = None
+ request = None
if self._outstanding and message_id in self._outstanding:
while timeout >= 0: # waiting for completed message to appear in responses
responses = self._get_response(message_id)
@@ -330,6 +337,11 @@ class BaseStrategy(object):
if log_enabled(ERROR):
log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
raise LDAPSessionTerminatedByServerError(self.connection.last_error)
+ elif responses == TRANSACTION_ERROR: # Novell LDAP Transaction unsolicited notification
+ self.connection.last_error = 'transaction error'
+ if log_enabled(ERROR):
+ log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection)
+ raise LDAPTransactionError(self.connection.last_error)
# if referral in response opens a new connection to resolve referrals if requested
@@ -383,19 +395,22 @@ class BaseStrategy(object):
for entry in response:
if entry['type'] == 'searchResEntry':
for attribute_type in self._outstanding[message_id]['attributes']:
- if attribute_type not in entry['raw_attributes']:
+ if attribute_type not in entry['raw_attributes'] and attribute_type not in (ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, NO_ATTRIBUTES):
entry['raw_attributes'][attribute_type] = list()
entry['attributes'][attribute_type] = list()
if log_enabled(PROTOCOL):
log(PROTOCOL, 'attribute value set to [] for missing attribute %s in %s', attribute_type, self)
- self._outstanding.pop(message_id)
+ request = self._outstanding.pop(message_id)
else:
if log_enabled(ERROR):
log(ERROR, 'message id not in outstanding queue for <%s>', self.connection)
raise(LDAPResponseTimeoutError('message id not in outstanding queue'))
- return response, result
+ if get_request:
+ return response, result, request
+ else:
+ return response, result
@staticmethod
def compute_ldap_message_size(data):
@@ -535,12 +550,16 @@ class BaseStrategy(object):
elif control_type == '1.2.840.113556.1.4.841': # DirSync AD
control_resp, unprocessed = decoder.decode(control_value, asn1Spec=DirSyncControlResponseValue())
control_value = dict()
- control_value['more_results'] = bool(control_resp['MoreResults']) # more_result if nonzero
+ control_value['more_results'] = bool(control_resp['MoreResults']) # more_result if nonzero
control_value['cookie'] = bytes(control_resp['CookieServer'])
+ elif control_type == '1.3.6.1.1.13.1' or control_type == '1.3.6.1.1.13.2': # Pre-Read control, Post-Read Control as per RFC 4527
+ control_resp, unprocessed = decoder.decode(control_value, asn1Spec=SearchResultEntry())
+ control_value = dict()
+ control_value['result'] = attributes_to_dict(control_resp['attributes'])
if unprocessed:
if log_enabled(ERROR):
log(ERROR, 'unprocessed control response in substrate')
- raise LDAPControlsError('unprocessed control response in substrate')
+ raise LDAPControlError('unprocessed control response in substrate')
return control_type, {'description': Oids.get(control_type, ''), 'criticality': criticality, 'value': control_value}
@staticmethod
@@ -567,12 +586,16 @@ class BaseStrategy(object):
control_value = dict()
control_value['more_results'] = True if control_resp[0][3][0][3] else False # more_result if nonzero
control_value['cookie'] = control_resp[0][3][2][3]
+ elif control_type == '1.3.6.1.1.13.1' or control_type == '1.3.6.1.1.13.2': # Pre-Read control, Post-Read Control as per RFC 4527
+ control_resp = decode_sequence(control_value, 0, len(control_value))
+ control_value = dict()
+ control_value['result'] = attributes_to_dict_fast(control_resp[0][3][1][3])
return control_type, {'description': Oids.get(control_type, ''), 'criticality': criticality, 'value': control_value}
@staticmethod
- def decode_request(ldap_message):
- message_type = ldap_message.getComponentByName('protocolOp').getName()
- component = ldap_message['protocolOp'].getComponent()
+ def decode_request(message_type, component, controls=None):
+ # message_type = ldap_message.getComponentByName('protocolOp').getName()
+ # component = ldap_message['protocolOp'].getComponent()
if message_type == 'bindRequest':
result = bind_request_to_dict(component)
elif message_type == 'unbindRequest':
@@ -598,6 +621,8 @@ class BaseStrategy(object):
log(ERROR, 'unknown request <%s>', message_type)
raise LDAPUnknownRequestError('unknown request')
result['type'] = message_type
+ result['controls'] = controls
+
return result
def valid_referral_list(self, referrals):
@@ -631,13 +656,17 @@ class BaseStrategy(object):
dereference_aliases=request['dereferenceAlias'],
attributes=[attr_type + ';range=' + str(int(high_range) + 1) + '-*'])
if isinstance(result, bool):
- current_response = self.connection.response[0]
+ if result:
+ current_response = self.connection.response[0]
+ else:
+ done = True
else:
current_response, _ = self.get_response(result)
current_response = current_response[0]
- attr_name = list(filter(lambda a: ';range=' in a, current_response['raw_attributes'].keys()))[0]
- continue
+ if not done:
+ attr_name = list(filter(lambda a: ';range=' in a, current_response['raw_attributes'].keys()))[0]
+ continue
done = True
@@ -658,45 +687,52 @@ class BaseStrategy(object):
preferred_referral_list = [referral for referral in valid_referral_list if referral['ssl'] == self.connection.server.ssl]
selected_referral = choice(preferred_referral_list) if preferred_referral_list else choice(valid_referral_list)
- referral_server = Server(host=selected_referral['host'],
- port=selected_referral['port'] or self.connection.server.port,
- use_ssl=selected_referral['ssl'],
- get_info=self.connection.server.get_info,
- formatter=self.connection.server.custom_formatter,
- connect_timeout=self.connection.server.connect_timeout,
- mode=self.connection.server.mode,
- allowed_referral_hosts=self.connection.server.allowed_referral_hosts,
- tls=Tls(local_private_key_file=self.connection.server.tls.private_key_file,
- local_certificate_file=self.connection.server.tls.certificate_file,
- validate=self.connection.server.tls.validate,
- version=self.connection.server.tls.version,
- ca_certs_file=self.connection.server.tls.ca_certs_file) if selected_referral['ssl'] else None)
-
- from ..core.connection import Connection
-
- referral_connection = Connection(server=referral_server,
- user=self.connection.user if not selected_referral['anonymousBindOnly'] else None,
- password=self.connection.password if not selected_referral['anonymousBindOnly'] else None,
- version=self.connection.version,
- authentication=self.connection.authentication if not selected_referral['anonymousBindOnly'] else ANONYMOUS,
- client_strategy=SYNC,
- auto_referrals=True,
- read_only=self.connection.read_only,
- check_names=self.connection.check_names,
- raise_exceptions=self.connection.raise_exceptions,
- fast_decoder=self.connection.fast_decoder,
- receive_timeout=self.connection.receive_timeout)
+ cachekey = (selected_referral['host'], selected_referral['port'] or self.connection.server.port, selected_referral['ssl'])
+ if self.connection.use_referral_cache and cachekey in self.referral_cache:
+ referral_connection = self.referral_cache[cachekey]
+ else:
+ referral_server = Server(host=selected_referral['host'],
+ port=selected_referral['port'] or self.connection.server.port,
+ use_ssl=selected_referral['ssl'],
+ get_info=self.connection.server.get_info,
+ formatter=self.connection.server.custom_formatter,
+ connect_timeout=self.connection.server.connect_timeout,
+ mode=self.connection.server.mode,
+ allowed_referral_hosts=self.connection.server.allowed_referral_hosts,
+ tls=Tls(local_private_key_file=self.connection.server.tls.private_key_file,
+ local_certificate_file=self.connection.server.tls.certificate_file,
+ validate=self.connection.server.tls.validate,
+ version=self.connection.server.tls.version,
+ ca_certs_file=self.connection.server.tls.ca_certs_file) if selected_referral['ssl'] else None)
+
+ from ..core.connection import Connection
+
+ referral_connection = Connection(server=referral_server,
+ user=self.connection.user if not selected_referral['anonymousBindOnly'] else None,
+ password=self.connection.password if not selected_referral['anonymousBindOnly'] else None,
+ version=self.connection.version,
+ authentication=self.connection.authentication if not selected_referral['anonymousBindOnly'] else ANONYMOUS,
+ client_strategy=SYNC,
+ auto_referrals=True,
+ read_only=self.connection.read_only,
+ check_names=self.connection.check_names,
+ raise_exceptions=self.connection.raise_exceptions,
+ fast_decoder=self.connection.fast_decoder,
+ receive_timeout=self.connection.receive_timeout)
- if self.connection.usage:
- self.connection._usage.referrals_followed += 1
+ if self.connection.usage:
+ self.connection._usage.referrals_connections += 1
- 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()
+ 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 self.connection.bound:
- referral_connection.bind()
+ if self.connection.bound:
+ referral_connection.bind()
+
+ if self.connection.usage:
+ self.connection._usage.referrals_followed += 1
if request['type'] == 'searchRequest':
referral_connection.search(selected_referral['base'] or request['base'],
@@ -706,7 +742,7 @@ class BaseStrategy(object):
selected_referral['attributes'] or request['attributes'],
request['sizeLimit'],
request['timeLimit'],
- request['typeOnly'],
+ request['typesOnly'],
controls=request['controls'])
elif request['type'] == 'addRequest':
referral_connection.add(selected_referral['base'] or request['entry'],
@@ -745,7 +781,10 @@ class BaseStrategy(object):
response = referral_connection.response
result = referral_connection.result
- referral_connection.unbind()
+ if self.connection.use_referral_cache:
+ self.referral_cache[cachekey] = referral_connection
+ else:
+ referral_connection.unbind()
else:
response = None
result = None
@@ -801,3 +840,8 @@ class BaseStrategy(object):
def set_stream(self, value):
raise NotImplementedError
+
+ def unbind_referral_cache(self):
+ while len(self.referral_cache) > 0:
+ cachekey, referral_connection = self.referral_cache.popitem()
+ referral_connection.unbind()