diff options
Diffstat (limited to 'ldap3/core/server.py')
-rw-r--r-- | ldap3/core/server.py | 1235 |
1 files changed, 663 insertions, 572 deletions
diff --git a/ldap3/core/server.py b/ldap3/core/server.py index 811baf6..43189ef 100644 --- a/ldap3/core/server.py +++ b/ldap3/core/server.py @@ -1,572 +1,663 @@ -"""
-"""
-
-# Created on 2014.05.31
-#
-# Author: Giovanni Cannata
-#
-# Copyright 2014 - 2018 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/>.
-
-import socket
-from threading import Lock
-from datetime import datetime, MINYEAR
-
-from .. import DSA, SCHEMA, ALL, BASE, get_config_parameter, OFFLINE_EDIR_8_8_8, OFFLINE_AD_2012_R2, OFFLINE_SLAPD_2_4, OFFLINE_DS389_1_3_3, SEQUENCE_TYPES, IP_SYSTEM_DEFAULT, IP_V4_ONLY, IP_V6_ONLY, IP_V4_PREFERRED, IP_V6_PREFERRED, STRING_TYPES
-from .exceptions import LDAPInvalidServerError, LDAPDefinitionError, LDAPInvalidPortError, LDAPInvalidTlsSpecificationError, LDAPSocketOpenError
-from ..protocol.formatters.standard import format_attribute_values
-from ..protocol.rfc4511 import LDAP_MAX_INT
-from ..protocol.rfc4512 import SchemaInfo, DsaInfo
-from .tls import Tls
-from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL
-from ..utils.conv import to_unicode
-
-try:
- from urllib.parse import unquote # Python 3
-except ImportError:
- from urllib import unquote # Python 2
-
-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
-
-
-class Server(object):
- """
- LDAP Server definition class
-
- Allowed_referral_hosts can be None (default), or a list of tuples of
- allowed servers ip address or names to contact while redirecting
- search to referrals.
-
- The second element of the tuple is a boolean to indicate if
- authentication to that server is allowed; if False only anonymous
- bind will be used.
-
- Per RFC 4516. Use [('*', False)] to allow any host with anonymous
- bind, use [('*', True)] to allow any host with same authentication of
- Server.
- """
-
- _message_counter = 0
- _message_id_lock = Lock() # global lock for message_id shared by all Server objects
-
-
- def __init__(self,
- host,
- port=None,
- use_ssl=False,
- allowed_referral_hosts=None,
- get_info=SCHEMA,
- tls=None,
- formatter=None,
- connect_timeout=None,
- mode=IP_V6_PREFERRED,
- validator=None):
-
- self.ipc = False
- url_given = False
- host = host.strip()
- if host.lower().startswith('ldap://'):
- self.host = host[7:]
- use_ssl = False
- url_given = True
- elif host.lower().startswith('ldaps://'):
- self.host = host[8:]
- use_ssl = True
- url_given = True
- elif host.lower().startswith('ldapi://') and unix_socket_available:
- self.ipc = True
- use_ssl = False
- url_given = True
- elif host.lower().startswith('ldapi://') and not unix_socket_available:
- raise LDAPSocketOpenError('LDAP over IPC not available - UNIX sockets non present')
- else:
- self.host = host
-
- if self.ipc:
- if str is bytes: # Python 2
- self.host = unquote(host[7:]).decode('utf-8')
- else: # Python 3
- self.host = unquote(host[7:]) # encoding defaults to utf-8 in python3
- self.port = None
- elif ':' in self.host and self.host.count(':') == 1:
- hostname, _, hostport = self.host.partition(':')
- try:
- port = int(hostport) or port
- except ValueError:
- if log_enabled(ERROR):
- log(ERROR, 'port <%s> must be an integer', port)
- raise LDAPInvalidPortError('port must be an integer')
- self.host = hostname
- elif url_given and self.host.startswith('['):
- hostname, sep, hostport = self.host[1:].partition(']')
- if sep != ']' or not self._is_ipv6(hostname):
- if log_enabled(ERROR):
- log(ERROR, 'invalid IPv6 server address for <%s>', self.host)
- raise LDAPInvalidServerError()
- if len(hostport):
- if not hostport.startswith(':'):
- if log_enabled(ERROR):
- log(ERROR, 'invalid URL in server name for <%s>', self.host)
- raise LDAPInvalidServerError('invalid URL in server name')
- if not hostport[1:].isdecimal():
- if log_enabled(ERROR):
- log(ERROR, 'port must be an integer for <%s>', self.host)
- raise LDAPInvalidPortError('port must be an integer')
- port = int(hostport[1:])
- self.host = hostname
- elif not url_given and self._is_ipv6(self.host):
- pass
- elif self.host.count(':') > 1:
- if log_enabled(ERROR):
- log(ERROR, 'invalid server address for <%s>', self.host)
- raise LDAPInvalidServerError()
-
- if not self.ipc:
- self.host.rstrip('/')
- if not use_ssl and not port:
- port = 389
- elif use_ssl and not port:
- port = 636
-
- if isinstance(port, int):
- if port in range(0, 65535):
- self.port = port
- else:
- if log_enabled(ERROR):
- log(ERROR, 'port <%s> must be in range from 0 to 65535', port)
- raise LDAPInvalidPortError('port must in range from 0 to 65535')
- else:
- if log_enabled(ERROR):
- log(ERROR, 'port <%s> must be an integer', port)
- raise LDAPInvalidPortError('port must be an integer')
-
- if allowed_referral_hosts is None: # defaults to any server with authentication
- allowed_referral_hosts = [('*', True)]
-
- if isinstance(allowed_referral_hosts, SEQUENCE_TYPES):
- self.allowed_referral_hosts = []
- for referral_host in allowed_referral_hosts:
- if isinstance(referral_host, tuple):
- if isinstance(referral_host[1], bool):
- self.allowed_referral_hosts.append(referral_host)
- elif isinstance(allowed_referral_hosts, tuple):
- if isinstance(allowed_referral_hosts[1], bool):
- self.allowed_referral_hosts = [allowed_referral_hosts]
- else:
- self.allowed_referral_hosts = []
-
- self.ssl = True if use_ssl else False
- if tls and not isinstance(tls, Tls):
- if log_enabled(ERROR):
- log(ERROR, 'invalid tls specification: <%s>', tls)
- raise LDAPInvalidTlsSpecificationError('invalid Tls object')
-
- self.tls = Tls() if self.ssl and not tls else tls
-
- if not self.ipc:
- if self._is_ipv6(self.host):
- self.name = ('ldaps' if self.ssl else 'ldap') + '://[' + self.host + ']:' + str(self.port)
- else:
- self.name = ('ldaps' if self.ssl else 'ldap') + '://' + self.host + ':' + str(self.port)
- else:
- self.name = host
-
- self.get_info = get_info
- self._dsa_info = None
- self._schema_info = None
- self.dit_lock = Lock()
- self.custom_formatter = formatter
- self.custom_validator = validator
- self._address_info = [] # property self.address_info resolved at open time (or when check_availability is called)
- self._address_info_resolved_time = datetime(MINYEAR, 1, 1) # smallest date ever
- self.current_address = None
- self.connect_timeout = connect_timeout
- self.mode = mode
-
- self.get_info_from_server(None) # load offline schema if needed
-
- if log_enabled(BASIC):
- log(BASIC, 'instantiated Server: <%r>', self)
-
- @staticmethod
- def _is_ipv6(host):
- try:
- socket.inet_pton(socket.AF_INET6, host)
- except (socket.error, AttributeError, ValueError):
- return False
- return True
-
- def __str__(self):
- if self.host:
- s = self.name + (' - ssl' if self.ssl else ' - cleartext') + (' - unix socket' if self.ipc else '')
- else:
- s = object.__str__(self)
- return s
-
- def __repr__(self):
- r = 'Server(host={0.host!r}, port={0.port!r}, use_ssl={0.ssl!r}'.format(self)
- r += '' if not self.allowed_referral_hosts else ', allowed_referral_hosts={0.allowed_referral_hosts!r}'.format(self)
- r += '' if self.tls is None else ', tls={0.tls!r}'.format(self)
- r += '' if not self.get_info else ', get_info={0.get_info!r}'.format(self)
- r += '' if not self.connect_timeout else ', connect_timeout={0.connect_timeout!r}'.format(self)
- r += '' if not self.mode else ', mode={0.mode!r}'.format(self)
- r += ')'
-
- return r
-
- @property
- def address_info(self):
- conf_refresh_interval = get_config_parameter('ADDRESS_INFO_REFRESH_TIME')
- if not self._address_info or (datetime.now() - self._address_info_resolved_time).seconds > conf_refresh_interval:
- # converts addresses tuple to list and adds a 6th parameter for availability (None = not checked, True = available, False=not available) and a 7th parameter for the checking time
- addresses = None
- try:
- if self.ipc:
- addresses = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, None, self.host, None)]
- else:
- addresses = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED)
- except (socket.gaierror, AttributeError):
- pass
-
- if not addresses: # if addresses not found or raised an exception (for example for bad flags) tries again without flags
- try:
- addresses = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP)
- except socket.gaierror:
- pass
-
- if addresses:
- self._address_info = [list(address) + [None, None] for address in addresses]
- self._address_info_resolved_time = datetime.now()
- else:
- self._address_info = []
- self._address_info_resolved_time = datetime(MINYEAR, 1, 1) # smallest date
-
- if log_enabled(BASIC):
- for address in self._address_info:
- log(BASIC, 'address for <%s> resolved as <%r>', self, address[:-2])
- return self._address_info
-
- def update_availability(self, address, available):
- cont = 0
- while cont < len(self._address_info):
- if self.address_info[cont] == address:
- self._address_info[cont][5] = True if available else False
- self._address_info[cont][6] = datetime.now()
- break
- cont += 1
-
- def reset_availability(self):
- for address in self._address_info:
- address[5] = None
- address[6] = None
-
- def check_availability(self):
- """
- Tries to open, connect and close a socket to specified address
- and port to check availability. Timeout in seconds is specified in CHECK_AVAILABITY_TIMEOUT if not specified in
- the Server object
- """
- conf_availability_timeout = get_config_parameter('CHECK_AVAILABILITY_TIMEOUT')
- available = False
- self.reset_availability()
- for address in self.candidate_addresses():
- available = True
- try:
- temp_socket = socket.socket(*address[:3])
- if self.connect_timeout:
- temp_socket.settimeout(self.connect_timeout)
- else:
- temp_socket.settimeout(conf_availability_timeout) # set timeout for checking availability to default
- try:
- temp_socket.connect(address[4])
- except socket.error:
- available = False
- finally:
- try:
- temp_socket.shutdown(socket.SHUT_RDWR)
- except socket.error:
- available = False
- finally:
- temp_socket.close()
- except socket.gaierror:
- available = False
-
- if available:
- if log_enabled(BASIC):
- log(BASIC, 'server <%s> available at <%r>', self, address)
- self.update_availability(address, True)
- break # if an available address is found exits immediately
- else:
- self.update_availability(address, False)
- if log_enabled(ERROR):
- log(ERROR, 'server <%s> not available at <%r>', self, address)
-
- return available
-
- @staticmethod
- def next_message_id():
- """
- LDAP messageId is unique for all connections to same server
- """
- with Server._message_id_lock:
- Server._message_counter += 1
- if Server._message_counter >= LDAP_MAX_INT:
- Server._message_counter = 1
- if log_enabled(PROTOCOL):
- log(PROTOCOL, 'new message id <%d> generated', Server._message_counter)
-
- return Server._message_counter
-
- def _get_dsa_info(self, connection):
- """
- Retrieve DSE operational attribute as per RFC4512 (5.1).
- """
- if connection.strategy.no_real_dsa: # do not try for mock strategies
- return
-
- if not connection.strategy.pooled: # in pooled strategies get_dsa_info is performed by the worker threads
- result = connection.search(search_base='',
- search_filter='(objectClass=*)',
- search_scope=BASE,
- attributes=['altServer', # requests specific dsa info attributes
- 'namingContexts',
- 'supportedControl',
- 'supportedExtension',
- 'supportedFeatures',
- 'supportedCapabilities',
- 'supportedLdapVersion',
- 'supportedSASLMechanisms',
- 'vendorName',
- 'vendorVersion',
- 'subschemaSubentry',
- '*',
- '+'], # requests all remaining attributes (other),
- get_operational_attributes=True)
-
- 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 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'])
-
- if log_enabled(BASIC):
- log(BASIC, 'DSA info read for <%s> via <%s>', self, connection)
-
- def _get_schema_info(self, connection, entry=''):
- """
- Retrieve schema from subschemaSubentry DSE attribute, per RFC
- 4512 (4.4 and 5.1); entry = '' means DSE.
- """
- if connection.strategy.no_real_dsa: # do not try for mock strategies
- return
-
- schema_entry = None
- if self._dsa_info and entry == '': # subschemaSubentry already present in dsaInfo
- if isinstance(self._dsa_info.schema_entry, SEQUENCE_TYPES):
- schema_entry = self._dsa_info.schema_entry[0] if self._dsa_info.schema_entry else None
- else:
- 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]
- else: # asynchronous request, must check if subschemaSubentry in attributes
- results, _ = connection.get_response(result)
- 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]
-
- if schema_entry and not connection.strategy.pooled: # in pooled strategies get_schema_info is performed by the worker threads
- if isinstance(schema_entry, bytes) and str is not bytes: # Python 3
- schema_entry = to_unicode(schema_entry, from_server=True)
- result = connection.search(schema_entry,
- search_filter='(objectClass=subschema)',
- search_scope=BASE,
- attributes=['objectClasses', # requests specific subschema attributes
- 'attributeTypes',
- 'ldapSyntaxes',
- 'matchingRules',
- 'matchingRuleUse',
- 'dITContentRules',
- 'dITStructureRules',
- 'nameForms',
- 'createTimestamp',
- 'modifyTimestamp',
- '*'], # requests all remaining attributes (other)
- get_operational_attributes=True
- )
- 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
- else: # asynchronous request, must check if attributes in response
- results, result = connection.get_response(result)
- 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
- self._schema_info = None
- if self._schema_info: # if schema is valid tries to apply formatter to the "other" dict with raw values for schema and info
- for attribute in self._schema_info.other:
- self._schema_info.other[attribute] = format_attribute_values(self._schema_info, attribute, self._schema_info.raw[attribute], self.custom_formatter)
- if self._dsa_info: # try to apply formatter to the "other" dict with dsa info raw values
- for attribute in self._dsa_info.other:
- self._dsa_info.other[attribute] = format_attribute_values(self._schema_info, attribute, self._dsa_info.raw[attribute], self.custom_formatter)
- if log_enabled(BASIC):
- log(BASIC, 'schema read for <%s> via <%s>', self, connection)
-
- def get_info_from_server(self, connection):
- """
- reads info from DSE and from subschema
- """
- if connection and not connection.closed:
- if self.get_info in [DSA, ALL]:
- self._get_dsa_info(connection)
- if self.get_info in [SCHEMA, ALL]:
- self._get_schema_info(connection)
- elif self.get_info == OFFLINE_EDIR_8_8_8:
- from ..protocol.schemas.edir888 import edir_8_8_8_schema, edir_8_8_8_dsa_info
- self.attach_schema_info(SchemaInfo.from_json(edir_8_8_8_schema))
- self.attach_dsa_info(DsaInfo.from_json(edir_8_8_8_dsa_info))
- elif self.get_info == OFFLINE_AD_2012_R2:
- from ..protocol.schemas.ad2012R2 import ad_2012_r2_schema, ad_2012_r2_dsa_info
- self.attach_schema_info(SchemaInfo.from_json(ad_2012_r2_schema))
- self.attach_dsa_info(DsaInfo.from_json(ad_2012_r2_dsa_info))
- elif self.get_info == OFFLINE_SLAPD_2_4:
- from ..protocol.schemas.slapd24 import slapd_2_4_schema, slapd_2_4_dsa_info
- self.attach_schema_info(SchemaInfo.from_json(slapd_2_4_schema))
- self.attach_dsa_info(DsaInfo.from_json(slapd_2_4_dsa_info))
- elif self.get_info == OFFLINE_DS389_1_3_3:
- from ..protocol.schemas.ds389 import ds389_1_3_3_schema, ds389_1_3_3_dsa_info
- self.attach_schema_info(SchemaInfo.from_json(ds389_1_3_3_schema))
- self.attach_dsa_info(DsaInfo.from_json(ds389_1_3_3_dsa_info))
-
- def attach_dsa_info(self, dsa_info=None):
- if isinstance(dsa_info, DsaInfo):
- self._dsa_info = dsa_info
- if log_enabled(BASIC):
- log(BASIC, 'attached DSA info to Server <%s>', self)
-
- def attach_schema_info(self, dsa_schema=None):
- if isinstance(dsa_schema, SchemaInfo):
- self._schema_info = dsa_schema
- if log_enabled(BASIC):
- log(BASIC, 'attached schema info to Server <%s>', self)
-
- @property
- def info(self):
- return self._dsa_info
-
- @property
- def schema(self):
- return self._schema_info
-
- @staticmethod
- def from_definition(host, dsa_info, dsa_schema, port=None, use_ssl=False, formatter=None, validator=None):
- """
- Define a dummy server with preloaded schema and info
- :param host: host name
- :param dsa_info: DsaInfo preloaded object or a json formatted string or a file name
- :param dsa_schema: SchemaInfo preloaded object or a json formatted string or a file name
- :param port: dummy port
- :param use_ssl: use_ssl
- :param formatter: custom formatter
- :return: Server object
- """
- if isinstance(host, SEQUENCE_TYPES):
- dummy = Server(host=host[0], port=port, use_ssl=use_ssl, formatter=formatter, validator=validator, tget_info=ALL) # for ServerPool object
- else:
- dummy = Server(host=host, port=port, use_ssl=use_ssl, formatter=formatter, validator=validator, get_info=ALL)
- if isinstance(dsa_info, DsaInfo):
- dummy._dsa_info = dsa_info
- elif isinstance(dsa_info, STRING_TYPES):
- try:
- dummy._dsa_info = DsaInfo.from_json(dsa_info) # tries to use dsa_info as a json configuration string
- except Exception:
- dummy._dsa_info = DsaInfo.from_file(dsa_info) # tries to use dsa_info as a file name
-
- if not dummy.info:
- if log_enabled(ERROR):
- log(ERROR, 'invalid DSA info for %s', host)
- raise LDAPDefinitionError('invalid dsa info')
-
- if isinstance(dsa_schema, SchemaInfo):
- dummy._schema_info = dsa_schema
- elif isinstance(dsa_schema, STRING_TYPES):
- try:
- dummy._schema_info = SchemaInfo.from_json(dsa_schema)
- except Exception:
- dummy._schema_info = SchemaInfo.from_file(dsa_schema)
-
- if not dummy.schema:
- if log_enabled(ERROR):
- log(ERROR, 'invalid schema info for %s', host)
- raise LDAPDefinitionError('invalid schema info')
-
- if log_enabled(BASIC):
- log(BASIC, 'created server <%s> from definition', dummy)
-
- return dummy
-
- def candidate_addresses(self):
- conf_reset_availability_timeout = get_config_parameter('RESET_AVAILABILITY_TIMEOUT')
- if self.ipc:
- candidates = self.address_info
- if log_enabled(BASIC):
- log(BASIC, 'candidate address for <%s>: <%s> with mode UNIX_SOCKET', self, self.name)
- else:
- # checks reset availability timeout
- for address in self.address_info:
- if address[6] and ((datetime.now() - address[6]).seconds > conf_reset_availability_timeout):
- address[5] = None
- address[6] = None
-
- # selects server address based on server mode and availability (in address[5])
- addresses = self.address_info[:] # copy to avoid refreshing while searching candidates
- candidates = []
- if addresses:
- if self.mode == IP_SYSTEM_DEFAULT:
- candidates.append(addresses[0])
- elif self.mode == IP_V4_ONLY:
- candidates = [address for address in addresses if address[0] == socket.AF_INET and (address[5] or address[5] is None)]
- elif self.mode == IP_V6_ONLY:
- candidates = [address for address in addresses if address[0] == socket.AF_INET6 and (address[5] or address[5] is None)]
- elif self.mode == IP_V4_PREFERRED:
- candidates = [address for address in addresses if address[0] == socket.AF_INET and (address[5] or address[5] is None)]
- candidates += [address for address in addresses if address[0] == socket.AF_INET6 and (address[5] or address[5] is None)]
- elif self.mode == IP_V6_PREFERRED:
- candidates = [address for address in addresses if address[0] == socket.AF_INET6 and (address[5] or address[5] is None)]
- candidates += [address for address in addresses if address[0] == socket.AF_INET and (address[5] or address[5] is None)]
- else:
- if log_enabled(ERROR):
- log(ERROR, 'invalid server mode for <%s>', self)
- raise LDAPInvalidServerError('invalid server mode')
-
- if log_enabled(BASIC):
- for candidate in candidates:
- log(BASIC, 'obtained candidate address for <%s>: <%r> with mode %s', self, candidate[:-2], self.mode)
- return candidates
+""" +""" + +# Created on 2014.05.31 +# +# 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/>. + +import socket +from threading import Lock +from datetime import datetime, MINYEAR + +from .. import DSA, SCHEMA, ALL, BASE, get_config_parameter, OFFLINE_EDIR_8_8_8, OFFLINE_EDIR_9_1_4, OFFLINE_AD_2012_R2, OFFLINE_SLAPD_2_4, OFFLINE_DS389_1_3_3, SEQUENCE_TYPES, IP_SYSTEM_DEFAULT, IP_V4_ONLY, IP_V6_ONLY, IP_V4_PREFERRED, IP_V6_PREFERRED, STRING_TYPES +from .exceptions import LDAPInvalidServerError, LDAPDefinitionError, LDAPInvalidPortError, LDAPInvalidTlsSpecificationError, LDAPSocketOpenError, LDAPInfoError +from ..protocol.formatters.standard import format_attribute_values +from ..protocol.rfc4511 import LDAP_MAX_INT +from ..protocol.rfc4512 import SchemaInfo, DsaInfo +from .tls import Tls +from ..utils.log import log, log_enabled, ERROR, BASIC, PROTOCOL, NETWORK +from ..utils.conv import to_unicode +from ..utils.port_validators import check_port, check_port_and_port_list + +try: + from urllib.parse import unquote # Python 3 +except ImportError: + from urllib import unquote # Python 2 + +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 + + +class Server(object): + """ + LDAP Server definition class + + Allowed_referral_hosts can be None (default), or a list of tuples of + allowed servers ip address or names to contact while redirecting + search to referrals. + + The second element of the tuple is a boolean to indicate if + authentication to that server is allowed; if False only anonymous + bind will be used. + + Per RFC 4516. Use [('*', False)] to allow any host with anonymous + bind, use [('*', True)] to allow any host with same authentication of + Server. + """ + + _message_counter = 0 + _message_id_lock = Lock() # global lock for message_id shared by all Server objects + + def __init__(self, + host, + port=None, + use_ssl=False, + allowed_referral_hosts=None, + get_info=SCHEMA, + tls=None, + formatter=None, + connect_timeout=None, + mode=IP_V6_PREFERRED, + validator=None): + + self.ipc = False + url_given = False + host = host.strip() + if host.lower().startswith('ldap://'): + self.host = host[7:] + use_ssl = False + url_given = True + elif host.lower().startswith('ldaps://'): + self.host = host[8:] + use_ssl = True + url_given = True + elif host.lower().startswith('ldapi://') and unix_socket_available: + self.ipc = True + use_ssl = False + url_given = True + elif host.lower().startswith('ldapi://') and not unix_socket_available: + raise LDAPSocketOpenError('LDAP over IPC not available - UNIX sockets non present') + else: + self.host = host + + if self.ipc: + if str is bytes: # Python 2 + self.host = unquote(host[7:]).decode('utf-8') + else: # Python 3 + self.host = unquote(host[7:]) # encoding defaults to utf-8 in python3 + self.port = None + elif ':' in self.host and self.host.count(':') == 1: + hostname, _, hostport = self.host.partition(':') + try: + port = int(hostport) or port + except ValueError: + if log_enabled(ERROR): + log(ERROR, 'port <%s> must be an integer', port) + raise LDAPInvalidPortError('port must be an integer') + self.host = hostname + elif url_given and self.host.startswith('['): + hostname, sep, hostport = self.host[1:].partition(']') + if sep != ']' or not self._is_ipv6(hostname): + if log_enabled(ERROR): + log(ERROR, 'invalid IPv6 server address for <%s>', self.host) + raise LDAPInvalidServerError() + if len(hostport): + if not hostport.startswith(':'): + if log_enabled(ERROR): + log(ERROR, 'invalid URL in server name for <%s>', self.host) + raise LDAPInvalidServerError('invalid URL in server name') + if not hostport[1:].isdecimal(): + if log_enabled(ERROR): + log(ERROR, 'port must be an integer for <%s>', self.host) + raise LDAPInvalidPortError('port must be an integer') + port = int(hostport[1:]) + self.host = hostname + elif not url_given and self._is_ipv6(self.host): + pass + elif self.host.count(':') > 1: + if log_enabled(ERROR): + log(ERROR, 'invalid server address for <%s>', self.host) + raise LDAPInvalidServerError() + + if not self.ipc: + self.host.rstrip('/') + if not use_ssl and not port: + port = 389 + elif use_ssl and not port: + port = 636 + + port_err = check_port(port) + if port_err: + if log_enabled(ERROR): + log(ERROR, port_err) + raise LDAPInvalidPortError(port_err) + self.port = port + + if allowed_referral_hosts is None: # defaults to any server with authentication + allowed_referral_hosts = [('*', True)] + + if isinstance(allowed_referral_hosts, SEQUENCE_TYPES): + self.allowed_referral_hosts = [] + for referral_host in allowed_referral_hosts: + if isinstance(referral_host, tuple): + if isinstance(referral_host[1], bool): + self.allowed_referral_hosts.append(referral_host) + elif isinstance(allowed_referral_hosts, tuple): + if isinstance(allowed_referral_hosts[1], bool): + self.allowed_referral_hosts = [allowed_referral_hosts] + else: + self.allowed_referral_hosts = [] + + self.ssl = True if use_ssl else False + if tls and not isinstance(tls, Tls): + if log_enabled(ERROR): + log(ERROR, 'invalid tls specification: <%s>', tls) + raise LDAPInvalidTlsSpecificationError('invalid Tls object') + + self.tls = Tls() if self.ssl and not tls else tls + + if not self.ipc: + if self._is_ipv6(self.host): + self.name = ('ldaps' if self.ssl else 'ldap') + '://[' + self.host + ']:' + str(self.port) + else: + self.name = ('ldaps' if self.ssl else 'ldap') + '://' + self.host + ':' + str(self.port) + else: + self.name = host + + self.get_info = get_info + self._dsa_info = None + self._schema_info = None + self.dit_lock = Lock() + self.custom_formatter = formatter + self.custom_validator = validator + self._address_info = [] # property self.address_info resolved at open time (or when check_availability is called) + self._address_info_resolved_time = datetime(MINYEAR, 1, 1) # smallest date ever + self.current_address = None + self.connect_timeout = connect_timeout + self.mode = mode + + self.get_info_from_server(None) # load offline schema if needed + + if log_enabled(BASIC): + log(BASIC, 'instantiated Server: <%r>', self) + + @staticmethod + def _is_ipv6(host): + try: + socket.inet_pton(socket.AF_INET6, host) + except (socket.error, AttributeError, ValueError): + return False + return True + + def __str__(self): + if self.host: + s = self.name + (' - ssl' if self.ssl else ' - cleartext') + (' - unix socket' if self.ipc else '') + else: + s = object.__str__(self) + return s + + def __repr__(self): + r = 'Server(host={0.host!r}, port={0.port!r}, use_ssl={0.ssl!r}'.format(self) + r += '' if not self.allowed_referral_hosts else ', allowed_referral_hosts={0.allowed_referral_hosts!r}'.format(self) + r += '' if self.tls is None else ', tls={0.tls!r}'.format(self) + r += '' if not self.get_info else ', get_info={0.get_info!r}'.format(self) + r += '' if not self.connect_timeout else ', connect_timeout={0.connect_timeout!r}'.format(self) + r += '' if not self.mode else ', mode={0.mode!r}'.format(self) + r += ')' + + return r + + @property + def address_info(self): + conf_refresh_interval = get_config_parameter('ADDRESS_INFO_REFRESH_TIME') + if not self._address_info or (datetime.now() - self._address_info_resolved_time).seconds > conf_refresh_interval: + # converts addresses tuple to list and adds a 6th parameter for availability (None = not checked, True = available, False=not available) and a 7th parameter for the checking time + addresses = None + try: + if self.ipc: + addresses = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, None, self.host, None)] + else: + if self.mode == IP_V4_ONLY: + addresses = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) + elif self.mode == IP_V6_ONLY: + addresses = socket.getaddrinfo(self.host, self.port, socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) + else: + addresses = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) + except (socket.gaierror, AttributeError): + pass + + if not addresses: # if addresses not found or raised an exception (for example for bad flags) tries again without flags + try: + if self.mode == IP_V4_ONLY: + addresses = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + elif self.mode == IP_V6_ONLY: + addresses = socket.getaddrinfo(self.host, self.port, socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP) + else: + addresses = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP) + except socket.gaierror: + pass + + if addresses: + self._address_info = [list(address) + [None, None] for address in addresses] + self._address_info_resolved_time = datetime.now() + else: + self._address_info = [] + self._address_info_resolved_time = datetime(MINYEAR, 1, 1) # smallest date + + if log_enabled(BASIC): + for address in self._address_info: + log(BASIC, 'address for <%s> resolved as <%r>', self, address[:-2]) + return self._address_info + + def update_availability(self, address, available): + cont = 0 + while cont < len(self._address_info): + if self.address_info[cont] == address: + self._address_info[cont][5] = True if available else False + self._address_info[cont][6] = datetime.now() + break + cont += 1 + + def reset_availability(self): + for address in self._address_info: + address[5] = None + address[6] = None + + def check_availability(self, source_address=None, source_port=None, source_port_list=None): + """ + Tries to open, connect and close a socket to specified address and port to check availability. + Timeout in seconds is specified in CHECK_AVAILABITY_TIMEOUT if not specified in + the Server object. + If specified, use a specific address, port, or list of possible ports, when attempting to check availability. + NOTE: This will only consider multiple ports from the source port list if the first ones we try to bind to are + already in use. This will not attempt using different ports in the list if the server is unavailable, + as that could result in the runtime of check_availability significantly exceeding the connection timeout. + """ + source_port_err = check_port_and_port_list(source_port, source_port_list) + if source_port_err: + if log_enabled(ERROR): + log(ERROR, source_port_err) + raise LDAPInvalidPortError(source_port_err) + + # using an empty string to bind a socket means "use the default as if this wasn't provided" because socket + # binding requires that you pass something for the ip if you want to pass a specific port + bind_address = source_address if source_address is not None else '' + # using 0 as the source port to bind a socket means "use the default behavior of picking a random port from + # all ports as if this wasn't provided" because socket binding requires that you pass something for the port + # if you want to pass a specific ip + candidate_bind_ports = [0] + + # if we have either a source port or source port list, convert that into our candidate list + if source_port is not None: + candidate_bind_ports = [source_port] + elif source_port_list is not None: + candidate_bind_ports = source_port_list[:] + + conf_availability_timeout = get_config_parameter('CHECK_AVAILABILITY_TIMEOUT') + available = False + self.reset_availability() + for address in self.candidate_addresses(): + available = True + try: + temp_socket = socket.socket(*address[:3]) + + # Go through our candidate bind ports and try to bind our socket to our source address with them. + # if no source address or ports were specified, this will have the same success/fail result as if we + # tried to connect to the remote server without binding locally first. + # This is actually a little bit better, as it lets us distinguish the case of "issue binding the socket + # locally" from "remote server is unavailable" with more clarity, though this will only really be an + # 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 bind_port in candidate_bind_ports: + try: + temp_socket.bind((bind_address, bind_port)) + socket_bind_succeeded = True + break + except Exception as bind_ex: + last_bind_exc = bind_ex + if log_enabled(NETWORK): + log(NETWORK, 'Unable to bind to local address <%s> with source port <%s> due to <%s>', + bind_address, bind_port, bind_ex) + if not socket_bind_succeeded: + if log_enabled(ERROR): + log(ERROR, 'Unable to locally bind to local address <%s> with any of the source ports <%s> due to <%s>', + bind_address, candidate_bind_ports, last_bind_exc) + raise LDAPSocketOpenError('Unable to bind socket locally to address {} with any of the source ports {} due to {}' + .format(bind_address, candidate_bind_ports, last_bind_exc)) + + if self.connect_timeout: + temp_socket.settimeout(self.connect_timeout) + else: + temp_socket.settimeout(conf_availability_timeout) # set timeout for checking availability to default + try: + temp_socket.connect(address[4]) + except socket.error: + available = False + finally: + try: + temp_socket.shutdown(socket.SHUT_RDWR) + except socket.error: + available = False + finally: + temp_socket.close() + except socket.gaierror: + available = False + + if available: + if log_enabled(BASIC): + log(BASIC, 'server <%s> available at <%r>', self, address) + self.update_availability(address, True) + break # if an available address is found exits immediately + else: + self.update_availability(address, False) + if log_enabled(ERROR): + log(ERROR, 'server <%s> not available at <%r>', self, address) + + return available + + @staticmethod + def next_message_id(): + """ + LDAP messageId is unique for all connections to same server + """ + with Server._message_id_lock: + Server._message_counter += 1 + if Server._message_counter >= LDAP_MAX_INT: + Server._message_counter = 1 + if log_enabled(PROTOCOL): + log(PROTOCOL, 'new message id <%d> generated', Server._message_counter) + + return Server._message_counter + + def _get_dsa_info(self, connection): + """ + Retrieve DSE operational attribute as per RFC4512 (5.1). + """ + if connection.strategy.no_real_dsa: # do not try for mock strategies + return + + if not connection.strategy.pooled: # in pooled strategies get_dsa_info is performed by the worker threads + result = connection.search(search_base='', + search_filter='(objectClass=*)', + search_scope=BASE, + attributes=['altServer', # requests specific dsa info attributes + 'namingContexts', + 'supportedControl', + 'supportedExtension', + 'supportedFeatures', + 'supportedCapabilities', + 'supportedLdapVersion', + 'supportedSASLMechanisms', + 'vendorName', + 'vendorVersion', + 'subschemaSubentry', + '*', + '+'], # requests all remaining attributes (other), + get_operational_attributes=True) + + 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 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']) + + if log_enabled(BASIC): + log(BASIC, 'DSA info read for <%s> via <%s>', self, connection) + + def _get_schema_info(self, connection, entry=''): + """ + Retrieve schema from subschemaSubentry DSE attribute, per RFC + 4512 (4.4 and 5.1); entry = '' means DSE. + """ + if connection.strategy.no_real_dsa: # do not try for mock strategies + return + + schema_entry = None + if self._dsa_info and entry == '': # subschemaSubentry already present in dsaInfo + if isinstance(self._dsa_info.schema_entry, SEQUENCE_TYPES): + schema_entry = self._dsa_info.schema_entry[0] if self._dsa_info.schema_entry else None + else: + 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] + else: # asynchronous request, must check if subschemaSubentry in attributes + results, _ = connection.get_response(result) + 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] + + if schema_entry and not connection.strategy.pooled: # in pooled strategies get_schema_info is performed by the worker threads + if isinstance(schema_entry, bytes) and str is not bytes: # Python 3 + schema_entry = to_unicode(schema_entry, from_server=True) + result = connection.search(schema_entry, + search_filter='(objectClass=subschema)', + search_scope=BASE, + attributes=['objectClasses', # requests specific subschema attributes + 'attributeTypes', + 'ldapSyntaxes', + 'matchingRules', + 'matchingRuleUse', + 'dITContentRules', + 'dITStructureRules', + 'nameForms', + 'createTimestamp', + 'modifyTimestamp', + '*'], # requests all remaining attributes (other) + get_operational_attributes=True + ) + 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 + else: # asynchronous request, must check if attributes in response + results, result = connection.get_response(result) + 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 + self._schema_info = None + if self._schema_info: # if schema is valid tries to apply formatter to the "other" dict with raw values for schema and info + for attribute in self._schema_info.other: + self._schema_info.other[attribute] = format_attribute_values(self._schema_info, attribute, self._schema_info.raw[attribute], self.custom_formatter) + if self._dsa_info: # try to apply formatter to the "other" dict with dsa info raw values + for attribute in self._dsa_info.other: + self._dsa_info.other[attribute] = format_attribute_values(self._schema_info, attribute, self._dsa_info.raw[attribute], self.custom_formatter) + if log_enabled(BASIC): + log(BASIC, 'schema read for <%s> via <%s>', self, connection) + + def get_info_from_server(self, connection): + """ + reads info from DSE and from subschema + """ + if connection and not connection.closed: + if self.get_info in [DSA, ALL]: + self._get_dsa_info(connection) + if self.get_info in [SCHEMA, ALL]: + self._get_schema_info(connection) + elif self.get_info == OFFLINE_EDIR_8_8_8: + from ..protocol.schemas.edir888 import edir_8_8_8_schema, edir_8_8_8_dsa_info + self.attach_schema_info(SchemaInfo.from_json(edir_8_8_8_schema)) + self.attach_dsa_info(DsaInfo.from_json(edir_8_8_8_dsa_info)) + elif self.get_info == OFFLINE_EDIR_9_1_4: + from ..protocol.schemas.edir914 import edir_9_1_4_schema, edir_9_1_4_dsa_info + self.attach_schema_info(SchemaInfo.from_json(edir_9_1_4_schema)) + self.attach_dsa_info(DsaInfo.from_json(edir_9_1_4_dsa_info)) + elif self.get_info == OFFLINE_AD_2012_R2: + from ..protocol.schemas.ad2012R2 import ad_2012_r2_schema, ad_2012_r2_dsa_info + self.attach_schema_info(SchemaInfo.from_json(ad_2012_r2_schema)) + self.attach_dsa_info(DsaInfo.from_json(ad_2012_r2_dsa_info)) + elif self.get_info == OFFLINE_SLAPD_2_4: + from ..protocol.schemas.slapd24 import slapd_2_4_schema, slapd_2_4_dsa_info + self.attach_schema_info(SchemaInfo.from_json(slapd_2_4_schema)) + self.attach_dsa_info(DsaInfo.from_json(slapd_2_4_dsa_info)) + elif self.get_info == OFFLINE_DS389_1_3_3: + from ..protocol.schemas.ds389 import ds389_1_3_3_schema, ds389_1_3_3_dsa_info + self.attach_schema_info(SchemaInfo.from_json(ds389_1_3_3_schema)) + self.attach_dsa_info(DsaInfo.from_json(ds389_1_3_3_dsa_info)) + + def attach_dsa_info(self, dsa_info=None): + if isinstance(dsa_info, DsaInfo): + self._dsa_info = dsa_info + if log_enabled(BASIC): + log(BASIC, 'attached DSA info to Server <%s>', self) + + def attach_schema_info(self, dsa_schema=None): + if isinstance(dsa_schema, SchemaInfo): + self._schema_info = dsa_schema + if log_enabled(BASIC): + log(BASIC, 'attached schema info to Server <%s>', self) + + @property + def info(self): + return self._dsa_info + + @property + def schema(self): + return self._schema_info + + @staticmethod + def from_definition(host, dsa_info, dsa_schema, port=None, use_ssl=False, formatter=None, validator=None): + """ + Define a dummy server with preloaded schema and info + :param host: host name + :param dsa_info: DsaInfo preloaded object or a json formatted string or a file name + :param dsa_schema: SchemaInfo preloaded object or a json formatted string or a file name + :param port: fake port + :param use_ssl: use_ssl + :param formatter: custom formatters + :return: Server object + """ + if isinstance(host, SEQUENCE_TYPES): + dummy = Server(host=host[0], port=port, use_ssl=use_ssl, formatter=formatter, validator=validator, get_info=ALL) # for ServerPool object + else: + dummy = Server(host=host, port=port, use_ssl=use_ssl, formatter=formatter, validator=validator, get_info=ALL) + if isinstance(dsa_info, DsaInfo): + dummy._dsa_info = dsa_info + elif isinstance(dsa_info, STRING_TYPES): + try: + dummy._dsa_info = DsaInfo.from_json(dsa_info) # tries to use dsa_info as a json configuration string + except Exception: + dummy._dsa_info = DsaInfo.from_file(dsa_info) # tries to use dsa_info as a file name + + if not dummy.info: + if log_enabled(ERROR): + log(ERROR, 'invalid DSA info for %s', host) + raise LDAPDefinitionError('invalid dsa info') + + if isinstance(dsa_schema, SchemaInfo): + dummy._schema_info = dsa_schema + elif isinstance(dsa_schema, STRING_TYPES): + try: + dummy._schema_info = SchemaInfo.from_json(dsa_schema) + except Exception: + dummy._schema_info = SchemaInfo.from_file(dsa_schema) + + if not dummy.schema: + if log_enabled(ERROR): + log(ERROR, 'invalid schema info for %s', host) + raise LDAPDefinitionError('invalid schema info') + + if log_enabled(BASIC): + log(BASIC, 'created server <%s> from definition', dummy) + + return dummy + + def candidate_addresses(self): + conf_reset_availability_timeout = get_config_parameter('RESET_AVAILABILITY_TIMEOUT') + if self.ipc: + candidates = self.address_info + if log_enabled(BASIC): + log(BASIC, 'candidate address for <%s>: <%s> with mode UNIX_SOCKET', self, self.name) + else: + # checks reset availability timeout + for address in self.address_info: + if address[6] and ((datetime.now() - address[6]).seconds > conf_reset_availability_timeout): + address[5] = None + address[6] = None + + # selects server address based on server mode and availability (in address[5]) + addresses = self.address_info[:] # copy to avoid refreshing while searching candidates + candidates = [] + if addresses: + if self.mode == IP_SYSTEM_DEFAULT: + candidates.append(addresses[0]) + elif self.mode == IP_V4_ONLY: + candidates = [address for address in addresses if address[0] == socket.AF_INET and (address[5] or address[5] is None)] + elif self.mode == IP_V6_ONLY: + candidates = [address for address in addresses if address[0] == socket.AF_INET6 and (address[5] or address[5] is None)] + elif self.mode == IP_V4_PREFERRED: + candidates = [address for address in addresses if address[0] == socket.AF_INET and (address[5] or address[5] is None)] + candidates += [address for address in addresses if address[0] == socket.AF_INET6 and (address[5] or address[5] is None)] + elif self.mode == IP_V6_PREFERRED: + candidates = [address for address in addresses if address[0] == socket.AF_INET6 and (address[5] or address[5] is None)] + candidates += [address for address in addresses if address[0] == socket.AF_INET and (address[5] or address[5] is None)] + else: + if log_enabled(ERROR): + log(ERROR, 'invalid server mode for <%s>', self) + raise LDAPInvalidServerError('invalid server mode') + + if log_enabled(BASIC): + for candidate in candidates: + log(BASIC, 'obtained candidate address for <%s>: <%r> with mode %s', self, candidate[:-2], self.mode) + return candidates + + def _check_info_property(self, kind, name): + if not self._dsa_info: + raise LDAPInfoError('server info not loaded') + + if kind == 'control': + properties = self.info.supported_controls + elif kind == 'extension': + properties = self.info.supported_extensions + elif kind == 'feature': + properties = self.info.supported_features + else: + raise LDAPInfoError('invalid info category') + + for prop in properties: + if name == prop[0] or (prop[2] and name.lower() == prop[2].lower()): # checks oid and description + return True + + return False + + def has_control(self, control): + return self._check_info_property('control', control) + + def has_extension(self, extension): + return self._check_info_property('extension', extension) + + def has_feature(self, feature): + return self._check_info_property('feature', feature) + + + |