import traceback
import ssl
import socketserver
import typing
import logging
import time
import random
import string
import itertools

from . import asn1, exceptions, ldap, schema, objects

def pop_control(controls, oid):
	result = None
	remaining_controls = []
	for control in controls or []:
		if control.controlType == oid:
			result = control
			break
		remaining_controls.append(control)
	return result, remaining_controls

def reject_critical_controls(controls=None):
	for control in controls or []:
		if control.criticality:
			raise exceptions.LDAPUnavailableCriticalExtension()

def mark_last(iterable):
	'''Yield (item, is_last) for all items in iterable

	is_last is True for the last items and False for other items.'''
	prev_item = None
	for item in iterable:
		if prev_item is not None:
			yield prev_item, False
		prev_item = item
	if prev_item is not None:
		yield prev_item, True

class RequestLogAdapter(logging.LoggerAdapter):
	def process(self, msg, kwargs):
		return self.extra['trace_id'] + ': ' + msg, kwargs

class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
	logger = logging.getLogger('ldapserver.server')

	def setup(self):
		super().setup()
		self.trace_id = ''.join([random.choice(string.ascii_letters) for _ in range(10)])
		self.logger = RequestLogAdapter(self.logger, {'trace_id': self.trace_id})
		self.keep_running = True

	def handle(self):
		time_connect = time.perf_counter()
		self.logger.info('Connection from %r', self.client_address)
		buf = b''
		while self.keep_running:
			try:
				shallowmsg, buf = ldap.ShallowLDAPMessage.from_ber(buf)
				for respmsg in self.handle_message(shallowmsg):
					self.request.sendall(ldap.LDAPMessage.to_ber(respmsg))
			except asn1.IncompleteBERError:
				chunk = self.request.recv(4096)
				if not chunk:
					self.keep_running = False
					self.request.close()
				else:
					buf += chunk
		self.request.close()
		time_disconnect = time.perf_counter()
		self.logger.info('Disconnected duration_seconds=%.3f', time_disconnect - time_connect)

	def handle_message(self, shallowmsg: ldap.ShallowLDAPMessage) -> typing.Iterable[ldap.LDAPMessage]:
		'''Handle an LDAP request foobar

		:param shallowmsg: Half-decoded LDAP message to handle
		:returns: Response messages
		'''
		msgtypes = {
			ldap.BindRequest: (self.handle_bind, ldap.BindResponse),
			ldap.UnbindRequest: (self.handle_unbind, None),
			ldap.SearchRequest: (self.handle_search, ldap.SearchResultDone),
			ldap.ModifyRequest: (self.handle_modify, ldap.ModifyResponse),
			ldap.AddRequest: (self.handle_add,  ldap.AddResponse),
			ldap.DelRequest: (self.handle_delete, ldap.DelResponse),
			ldap.ModifyDNRequest: (self.handle_modifydn, ldap.ModifyDNResponse),
			ldap.CompareRequest: (self.handle_compare, ldap.CompareResponse),
			ldap.AbandonRequest: (self.handle_abandon, None),
			ldap.ExtendedRequest: (self.handle_extended, ldap.ExtendedResponse),
		}
		handler, response_type = msgtypes.get(shallowmsg.protocolOpType, (None, None))
		try:
			if handler is None:
				raise exceptions.LDAPProtocolError()
			try:
				msg = shallowmsg.decode()[0]
			except ValueError as e:
				self.logger.error('Could not decode message %s, ignoring', shallowmsg)
				raise exceptions.LDAPProtocolError() from e
			for args in handler(msg.protocolOp, msg.controls):
				response, controls = args if isinstance(args, tuple) else (args, None)
				yield ldap.LDAPMessage(shallowmsg.messageID, response, controls)
		except exceptions.LDAPError as e:
			if response_type is not None:
				respmsg = ldap.LDAPMessage(shallowmsg.messageID, response_type(e.code, diagnosticMessage=e.message))
				yield respmsg
				self.logger.info('Operation aborted, responded with result code "%s" msg="%s"', e.code.name, e.message)
		except Exception as e: # pylint: disable=broad-except
			if response_type is not None:
				respmsg = ldap.LDAPMessage(shallowmsg.messageID, response_type(ldap.LDAPResultCode.other))
				yield respmsg
				self.logger.exception('Uncaught exception, responded with result code "other"')
			else:
				self.logger.exception('Uncaught exception, ignored request')

	def handle_bind(self, op: ldap.BindRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('BIND %s', op)
		reject_critical_controls(controls)
		raise exceptions.LDAPAuthMethodNotSupported()

	def handle_unbind(self, op: ldap.UnbindRequest, controls=None) -> typing.NoReturn:
		self.logger.info('UNBIND %s', op)
		reject_critical_controls(controls)
		self.keep_running = False
		return []

	def handle_search(self, op: ldap.SearchRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('SEARCH %s', op)
		reject_critical_controls(controls)
		yield ldap.SearchResultDone(ldap.LDAPResultCode.success)

	def handle_modify(self, op: ldap.ModifyRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('MODIFY %s', op)
		reject_critical_controls(controls)
		raise exceptions.LDAPInsufficientAccessRights()

	def handle_add(self, op: ldap.AddRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('ADD %s', op)
		reject_critical_controls(controls)
		raise exceptions.LDAPInsufficientAccessRights()

	def handle_delete(self, op: ldap.DelRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('DELETE %s', op)
		reject_critical_controls(controls)
		raise exceptions.LDAPInsufficientAccessRights()

	def handle_modifydn(self, op: ldap.ModifyDNRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('MODIFYDN %s', op)
		reject_critical_controls(controls)
		raise exceptions.LDAPInsufficientAccessRights()

	def handle_compare(self, op: ldap.CompareRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('COMPRAE %s', op)
		reject_critical_controls(controls)
		raise exceptions.LDAPInsufficientAccessRights()

	def handle_abandon(self, op: ldap.AbandonRequest, controls=None) -> typing.NoReturn:
		self.logger.info('ABANDON %s', op)
		reject_critical_controls(controls)

	def handle_extended(self, op: ldap.ExtendedRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
		self.logger.info('EXTENDED %s', op)
		reject_critical_controls(controls)
		raise exceptions.LDAPProtocolError()

class LDAPRequestHandler(BaseLDAPRequestHandler):
	'''
	.. py:attribute:: rootdse

		Special :any:`LDAPObject` that contains information
		about the server, such as supported extentions and SASL authentication
		mechansims. Attributes can be accessed in a dict-like fashion.
	'''

	subschema = objects.SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema', cn=['Subschema'])
	'''
	.. py:attribute:: subschema

		Special :any:`LDAPObject` that describes the schema.
		Per default the subschema includes standard syntaxes, standard matching
		rules and objectclasses/attributetypes for the rootdse and subschema.
		It does not include objectclasses/attributetypes for actual data
		(e.g. users and groups). See :any:`Subschema` for details.
	'''

	static_objects = tuple()

	def setup(self):
		super().setup()
		self.rootdse = self.subschema.RootDSE()
		self.rootdse['objectClass'] = ['top']
		if self.supports_starttls:
			self.rootdse['supportedExtension'].append(ldap.STARTTLS_OID)
		if self.supports_whoami:
			self.rootdse['supportedExtension'].append(ldap.WHOAMI_OID)
		if self.supports_password_modify:
			self.rootdse['supportedExtension'].append(ldap.PASSWORD_MODIFY_OID)
		if self.supports_paged_results:
			self.rootdse['supportedControl'].append(ldap.PAGED_RESULTS_OID)
		if self.supports_sasl_anonymous:
			self.rootdse['supportedSASLMechanisms'].append('ANONYMOUS')
		if self.supports_sasl_plain:
			self.rootdse['supportedSASLMechanisms'].append('PLAIN')
		if self.supports_sasl_external:
			self.rootdse['supportedSASLMechanisms'].append('EXTERNAL')
		self.rootdse['supportedLDAPVersion'] = ['3']
		self.bind_object = None
		self.bind_sasl_state = None
		self.__paged_searches = {} # pagination cookie -> (iterator, orig_op)
		self.__paged_cookie_counter = 0

	def handle_bind(self, op, controls=None):
		reject_critical_controls(controls)
		if op.version != 3:
			raise exceptions.LDAPProtocolError('Unsupported protocol version')
		auth = op.authentication
		# Resume ongoing SASL dialog
		if self.bind_sasl_state and isinstance(auth, ldap.SaslCredentials) \
				and auth.mechanism == self.bind_sasl_state[0]:
			mechanism, iterator = self.bind_sasl_state
			self.bind_sasl_state = None
			resp_code = ldap.LDAPResultCode.saslBindInProgress
			try:
				resp = iterator.send(auth.credentials)
				self.bind_sasl_state = (mechanism, iterator)
			except StopIteration as e:
				resp_code = ldap.LDAPResultCode.success
				self.bind_object, resp = e.value # pylint: disable=unpacking-non-sequence
			yield ldap.BindResponse(resp_code, serverSaslCreds=resp)
			return
		# If auth type or SASL method changed, abort SASL dialog
		self.bind_sasl_state = None
		if isinstance(auth, ldap.SimpleAuthentication):
			self.bind_object = self.do_bind_simple(op.name, auth.password)
			yield ldap.BindResponse(ldap.LDAPResultCode.success)
		elif isinstance(auth, ldap.SaslCredentials):
			ret = self.do_bind_sasl(auth.mechanism, auth.credentials)
			if isinstance(ret, tuple):
				self.bind_object, resp = ret
				yield ldap.BindResponse(ldap.LDAPResultCode.success, serverSaslCreds=resp)
				return
			iterator = iter(ret)
			resp_code = ldap.LDAPResultCode.saslBindInProgress
			try:
				resp = next(iterator)
				self.bind_sasl_state = (auth.mechanism, iterator)
			except StopIteration as e:
				resp_code = ldap.LDAPResultCode.success
				self.bind_object, resp = e.value # pylint: disable=unpacking-non-sequence
			yield ldap.BindResponse(resp_code, serverSaslCreds=resp)
		else:
			yield from super().handle_bind(op, controls) # pylint: disable=not-an-iterable

	def do_bind_simple(self, dn='', password=b''):
		'''Do LDAP BIND with simple authentication

		:param dn: Distinguished name of object to be authenticated or empty
		:type dn: str
		:param password: Password, may be empty
		:type password: bytes

		:returns: Bind object
		:rtype: obj

		Delegates implementation to :any:`do_bind_simple_anonymous`,
		:any:`do_bind_simple_unauthenticated` or :any:`do_bind_simple_authenticated`
		according to `RFC 4513`_.'''
		if not dn and not password:
			return self.do_bind_simple_anonymous()
		if not password:
			return self.do_bind_simple_unauthenticated(dn)
		return self.do_bind_simple_authenticated(dn, password)

	def do_bind_simple_anonymous(self):
		'''Do LDAP BIND with simple anonymous authentication (`RFC 4513 5.1.1.`_)

		:raises exceptions.LDAPError: if authentication failed

		:returns: Bind object on success
		:rtype: obj

		Calld by :any:`do_bind_simple`. Always returns None.'''
		return None

	def do_bind_simple_unauthenticated(self, dn):
		'''Do LDAP BIND with simple unauthenticated authentication (`RFC 4513 5.1.2.`_)

		:param dn: Distinguished name of the object to be authenticated
		:type dn: str

		:raises exceptions.LDAPError: if authentication failed

		:returns: Bind object on success
		:rtype: obj

		Calld by :any:`do_bind_simple`. The default implementation always raises an
		:any:`LDAPInvalidCredentials` exception.'''
		raise exceptions.LDAPInvalidCredentials()

	def do_bind_simple_authenticated(self, dn, password):
		'''Do LDAP BIND with simple name/password authentication (`RFC 4513 5.1.3.`_)

		:param dn: Distinguished name of the object to be authenticated
		:type dn: str
		:param password: Password for object
		:type dn: bytes

		:raises exceptions.LDAPError: if authentication failed

		:returns: Bind object on success
		:rtype: obj

		Calld by :any:`do_bind_simple`. The default implementation always raises an
		`LDAPInvalidCredentials` exception.'''
		raise exceptions.LDAPInvalidCredentials()

	def do_bind_sasl(self, mechanism, credentials=None, dn=None):
		'''Do LDAP BIND with SASL authentication (RFC 4513 and 4422)

		:param mechanism: Name of the selected SASL mechanism
		:type mechanism: str
		:param credentials: Initial client response
		:type credentials: bytes, optional
		:param dn: Distinguished name in LDAP BIND request, should be ignored for
		           SASL authentication
		:type dn: str, optional

		:returns: Bind object and final server challenge, only returns on success
		:rtype: Tuple (obj, bytes/None)

		The call only returns if authentication succeeded. In any other case,
		an appropriate :any:`exceptions.LDAPError` is raised.

		Some SASL methods require additional challenge-response round trips. These
		can be achieved with the `yield` statement:

		    client_response = yield server_challenge

		Generally all server challenges and client responses can always be absent
		(indicated by None), empty (empty bytes object) or consist of any number
		of bytes. Whether a challenge or response may or must be absent or present
		is defined by the individual SASL mechanism.

		IANA list of SASL mechansims: https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml
		'''
		if not mechanism:
			# Request to abort current negotiation (RFC4513 5.2.1.2)
			raise exceptions.LDAPAuthMethodNotSupported()
		if mechanism == 'ANONYMOUS' and self.supports_sasl_anonymous:
			if credentials is not None:
				credentials = credentials.decode()
			return self.do_bind_sasl_anonymous(trace_info=credentials), None
		if mechanism == 'PLAIN' and self.supports_sasl_plain:
			if credentials is None:
				raise exceptions.LDAPProtocolError('Unsupported protocol version')
			authzid, authcid, password = credentials.split(b'\0', 2)
			return self.do_bind_sasl_plain(authcid.decode(), password.decode(), authzid.decode() or None), None
		if mechanism == 'EXTERNAL' and self.supports_sasl_external:
			if credentials is not None:
				credentials = credentials.decode()
			return self.do_bind_sasl_external(authzid=credentials), None
		raise exceptions.LDAPAuthMethodNotSupported()

	supports_sasl_anonymous = False

	def do_bind_sasl_anonymous(self, trace_info=None):
		'''Do LDAP BIND with SASL "ANONYMOUS" mechanism (RFC 4505)

		:param trace_info: Trace information, either an email address or an
		                   opaque string that does not contain the '@' character
		:type trace_info: str, optional

		:raises exceptions.LDAPError: if authentication failed

		:returns: Bind object on success
		:rtype: obj

		Calld by :any:`do_bind_sasl`. The default implementation raises an
		:any:`LDAPAuthMethodNotSupported` exception.'''
		raise exceptions.LDAPAuthMethodNotSupported()

	supports_sasl_plain = False

	def do_bind_sasl_plain(self, identity, password, authzid=None):
		'''Do LDAP BIND with SASL "PLAIN" mechanism (RFC 4616)

		:param identity: Authentication identity (authcid)
		:type identity: str
		:param password: Password (passwd)
		:type password: str
		:param authzid: Authorization identity
		:type authzid: str, optional

		:raises exceptions.LDAPError: if authentication failed

		:returns: Bind object on success
		:rtype: obj

		Calld by :any:`do_bind_sasl`. The default implementation raises an
		:any:`LDAPAuthMethodNotSupported` exception.'''
		raise exceptions.LDAPAuthMethodNotSupported()

	supports_sasl_external = False

	def do_bind_sasl_external(self, authzid=None):
		'''Do LDAP BIND with SASL "EXTERNAL" mechanism (RFC 4422 and 4513)

		:param authzid: Authorization identity
		:type authzid: str, optional

		:raises exceptions.LDAPError: if authentication failed

		:returns: Bind object on success
		:rtype: obj

		EXTERNAL is commonly used for TLS client certificate authentication or
		system user based authentication on UNIX sockets.

		Calld by :any:`do_bind_sasl`. The default implementation raises an
		:any:`LDAPAuthMethodNotSupported` exception.'''
		raise exceptions.LDAPAuthMethodNotSupported()

	supports_paged_results = True

	def __handle_search_paged(self, op, paged_control, controls=None):
		def build_control(size=0, cookie=b''):
			value = ldap.PagedResultsValue(size=size, cookie=cookie)
			return ldap.Control(controlType=ldap.PAGED_RESULTS_OID,
			                    criticality=True, controlValue=bytes(value))

		# pylint: disable=no-member
		paged_control = ldap.PagedResultsValue.from_ber(paged_control.controlValue)[0]
		if not paged_control.cookie: # New paged search request
			results = self.do_search(op.baseObject, op.scope, op.filter)
			results = map(lambda obj: obj.search(op.baseObject, op.scope, op.filter, op.attributes, op.typesOnly), results)
			results = filter(None, results)
			iterator = iter(mark_last(results))
		else: # Continue existing paged search
			try:
				iterator, orig_op = self.__paged_searches.pop(paged_control.cookie)
			except KeyError as exc:
				raise exceptions.LDAPUnwillingToPerform('Invalid pagination cookie') from exc
			if ldap.ProtocolOp.to_ber(orig_op) != ldap.ProtocolOp.to_ber(op):
				raise exceptions.LDAPUnwillingToPerform('Search parameter mismatch')
			if not paged_control.size: # Cancel paged search
				yield ldap.SearchResultDone(ldap.LDAPResultCode.success), [build_control()]
				return
		is_last = True
		entries = 0
		time_start = time.perf_counter()
		for entry, is_last in itertools.islice(iterator, 0, paged_control.size):
			self.logger.debug('SEARCH entry %r', entry)
			entries += 1
			yield entry
		cookie = b''
		if not is_last:
			cookie = str(self.__paged_cookie_counter).encode()
			self.__paged_cookie_counter += 1
			self.__paged_searches[cookie] = iterator, op
		yield ldap.SearchResultDone(ldap.LDAPResultCode.success), [build_control(cookie=cookie)]
		time_end = time.perf_counter()
		self.logger.info('SEARCH dn=%r dn_scope=%s filter=%s attributes=%r page_cookie=%r entries=%d duration_seconds=%.3f',
		                 op.baseObject, op.scope.name, op.filter, ' '.join(op.attributes), cookie, entries, time_end - time_start)

	def handle_search(self, op, controls=None):
		self.logger.debug('SEARCH request dn=%r dn_scope=%s filter=%r attributes=%r',
		                  op.baseObject, op.scope.name, str(op.filter), ' '.join(op.attributes))
		paged_control = None
		if self.supports_paged_results:
			paged_control, controls = pop_control(controls, ldap.PAGED_RESULTS_OID)
		reject_critical_controls(controls)
		if paged_control:
			yield from self.__handle_search_paged(op, paged_control, controls)
			return
		entries = 0
		time_start = time.perf_counter()
		for obj in self.do_search(op.baseObject, op.scope, op.filter):
			entry = obj.search(op.baseObject, op.scope, op.filter, op.attributes, op.typesOnly)
			if entry:
				self.logger.debug('SEARCH entry %r', entry)
				entries += 1
				yield entry
		yield ldap.SearchResultDone(ldap.LDAPResultCode.success)
		time_end = time.perf_counter()
		self.logger.info('SEARCH dn=%r dn_scope=%s filter=\'%s\' attributes=%r entries=%d duration_seconds=%.3f',
		                 op.baseObject, op.scope.name, op.filter, ' '.join(op.attributes), entries, time_end - time_start)

	def do_search(self, baseobj, scope, filterobj):
		'''Do LDAP SEARCH operation

		:param baseobj: Distinguished name of the LDAP entry relative to which the
		                search is to be performed
		:type baseobj: str
		:param scope: Search scope
		:type scope: SearchScope
		:param filterobj: Filter object
		:type filterobj: Filter

		:raises exceptions.LDAPError: on error

		:returns: Iterable of dn, attributes tuples

		The default implementation returns matching objects from the root dse and
		the subschema.'''
		yield self.rootdse
		yield self.subschema
		yield from self.static_objects

	def handle_compare(self, op, controls=None):
		self.logger.info('COMPRAE request "%s" %s=%s', op.entry, op.ava.attributeDesc, repr(op.ava.assertionValue))
		obj = self.do_compare(op.entry, op.ava.attributeDesc, op.ava.assertionValue)
		if obj is None:
			raise exceptions.LDAPNoSuchObject()
		if obj.match_compare(op.ava.attributeDesc, op.ava.assertionValue):
			self.logger.info('COMPRAE result TRUE')
			return [ldap.CompareResponse(ldap.LDAPResultCode.compareTrue)]
		else:
			self.logger.info('COMPRAE result FALSE')
			return [ldap.CompareResponse(ldap.LDAPResultCode.compareFalse)]

	def do_compare(self, dn, attribute, value):
		'''Lookup object for COMPARE operation

		:param dn: Distinguished name of the LDAP entry
		:type dn: str
		:param attribute: Attribute type
		:type attribute: str
		:param value: Attribute value
		:type value: bytes

		:raises exceptions.LDAPError: on error

		:returns: `Object` or None

		The default implementation calls `do_search` and returns the first object
		with the right DN.'''
		objs = self.do_search(dn, ldap.SearchScope.baseObject, ldap.FilterPresent(attribute='objectClass'))
		for obj in objs:
			try:
				obj.compare(dn)
				return obj
			except exceptions.LDAPNoSuchObject:
				pass
			except exceptions.LDAPError:
				return obj
		raise exceptions.LDAPNoSuchObject()

	def handle_unbind(self, op, controls=None):
		self.logger.info('UNBIND')
		reject_critical_controls(controls)
		self.keep_running = False
		return []

	def handle_extended(self, op, controls=None):
		reject_critical_controls(controls)
		if op.requestName == ldap.STARTTLS_OID and self.supports_starttls:
			self.logger.info('EXTENDED STARTTLS')
			# StartTLS (RFC 4511)
			yield ldap.ExtendedResponse(ldap.LDAPResultCode.success, responseName=ldap.STARTTLS_OID)
			try:
				self.do_starttls()
			except Exception: # pylint: disable=broad-except
				traceback.print_exc()
				self.keep_running = False
			if ldap.STARTTLS_OID in self.rootdse['supportedExtension']:
				self.rootdse['supportedExtension'].remove(ldap.STARTTLS_OID)
		elif op.requestName == ldap.WHOAMI_OID and self.supports_whoami:
			self.logger.info('EXTENDED WHOAMI')
			# "Who am I?" Operation (RFC 4532)
			identity = (self.do_whoami() or '').encode()
			yield ldap.ExtendedResponse(ldap.LDAPResultCode.success, responseValue=identity)
		elif op.requestName == ldap.PASSWORD_MODIFY_OID and self.supports_password_modify:
			self.logger.info('EXTENDED PASSWORD_MODIFY')
			# Password Modify Extended Operation (RFC 3062)
			newpw = None
			if op.requestValue is None:
				newpw = self.do_password_modify()
			else:
				decoded, _ = ldap.PasswdModifyRequestValue.from_ber(op.requestValue)
				# pylint: disable=no-member
				newpw = self.do_password_modify(decoded.userIdentity, decoded.oldPasswd, decoded.newPasswd)
			if newpw is None:
				yield ldap.ExtendedResponse(ldap.LDAPResultCode.success)
			else:
				encoded = ldap.PasswdModifyResponseValue.to_ber(ldap.PasswdModifyResponseValue(newpw))
				yield ldap.ExtendedResponse(ldap.LDAPResultCode.success, responseValue=encoded)
		else:
			self.logger.warning('Unsupported or disabled EXTENDED operation %r', op.requestName)
			yield from super().handle_extended(op, controls) # pylint: disable=not-an-iterable

	#: :any:`ssl.SSLContext` for StartTLS
	ssl_context = None

	@property
	def supports_starttls(self):
		'''
		'''
		return self.ssl_context is not None and not isinstance(self.request, ssl.SSLSocket)

	def do_starttls(self):
		'''Do StartTLS extended operation (RFC 4511)

		Called by `handle_extended()` if :any:`supports_starttls` is True. The default
		implementation uses `ssl_context`.

		Note that the (success) response to the request is sent before this method
		is called. If a call to this method fails, the LDAP connection is
		immediately terminated.'''
		self.request = self.ssl_context.wrap_socket(self.request, server_side=True)

	#:
	supports_whoami = False

	def do_whoami(self):
		'''Do "Who am I?" extended operation (RFC 4532)

		:returns: Current authorization identity (authzid) or empty string for anonymous sessions
		:rtype: str

		Called by `handle_extended()` if `supports_whoami` is True. The default
		implementation always returns an empty string.'''
		return ''

	#:
	supports_password_modify = False

	def do_password_modify(self, user=None, old_password=None, new_password=None):
		'''Do password modify extended operation (RFC 3062)

		:param user: User the request relates to, may or may not be a
		             distinguished name. If absent, the request relates to the
		             user currently associated with the LDAP connection
		:type user: str, optional
		:param old_password: Current password of user
		:type old_password: bytes, optional
		:param new_password: Desired password for user
		:type new_password: bytes, optional

		Called by `handle_extended()` if :any:`supports_password_modify` is True. The
		default implementation always raises an :any:`LDAPUnwillingToPerform` error.'''
		raise exceptions.LDAPUnwillingToPerform()