????
Current Path : C:/opt/pgsql/pgAdmin 4/python/Lib/site-packages/ldap3/core/ |
Current File : C:/opt/pgsql/pgAdmin 4/python/Lib/site-packages/ldap3/core/server.py |
""" """ # 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) if connection.strategy.thread_safe: status, result, response, _ = result else: status = result result = connection.result response = connection.response with self.dit_lock: 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']) 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 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(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] 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 ) 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 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(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 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)