diff --git a/db.py b/db.py
index a1424cce5b70d4baa364679ee9076a718383f4bc..fb841923e878605bc840f3b09ba96c7a05ac5490 100644
--- a/db.py
+++ b/db.py
@@ -8,7 +8,7 @@ from sqlalchemy.orm import sessionmaker
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.ext.hybrid import hybrid_property
 from ldap import SearchScope, FilterAnd, FilterOr, FilterNot, FilterEqual, FilterPresent
-from server import LDAPRequestHandler, LDAPInvalidCredentials, LDAPInsufficientAccessRights, LDAPConfidentialityRequired
+from server import LDAPRequestHandler, LDAPInvalidCredentials, LDAPInsufficientAccessRights, LDAPConfidentialityRequired, LDAPNoSuchObject
 import socketserver
 from dn import parse_dn, build_dn
 
@@ -337,7 +337,7 @@ groupeval = SQLSearchEvaluator(
 ssl_context = SSLContext()
 ssl_context.load_cert_chain('devcert.crt', 'devcert.key')
 
-class RequestHandler(LDAPRequestHandler):
+class OldRequestHandler(LDAPRequestHandler):
 	ssl_context = ssl_context
 
 	def do_bind(self, name, password):
@@ -357,14 +357,6 @@ class RequestHandler(LDAPRequestHandler):
 			raise LDAPInvalidCredentials()
 		return user
 
-	def do_bind_sasl_external(self, authzid=None):
-		ucred = struct.Struct('III')
-		data = self.request.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, ucred.size)
-		print('EXTERNAL', data, authzid)
-		pid, uid, gid = ucred.unpack(data)
-		print('EXTERNAL', pid, uid, gid)
-		return None, None
-
 	def do_search(self, baseobj, scope, filter):
 		#if self.bind_object is None:
 		#	raise LDAPInsufficientAccessRights()
@@ -372,5 +364,65 @@ class RequestHandler(LDAPRequestHandler):
 		yield from usereval(baseobj, scope, filter)
 		yield from groupeval(baseobj, scope, filter)
 
-#socketserver.ForkingTCPServer(('127.0.0.1', 1337), RequestHandler).serve_forever()
-socketserver.UnixStreamServer('/tmp/ldapd.sock', RequestHandler).serve_forever()
+class RequestHandler(LDAPRequestHandler):
+	ssl_context = ssl_context
+
+	sasl_enable_plain = True
+
+	def do_bind_sasl_plain(self, identity, password, authzid=None):
+		if not isinstance(self.request, SSLSocket):
+			raise LDAPConfidentialityRequired()
+		if authzid is not None and authzid != identity:
+			raise LDAPProtocolError()
+		if identity.startswith('dn:'):
+			return self.do_bind_simple_authenticated(identity[3:], password.encode())
+		user = session.query(User).filter_by(loginname=identity).one_or_none()
+		if user is None or not user.check_password(password):
+			raise LDAPInvalidCredentials()
+		return user
+
+	def do_bind_simple_authenticated(self, dn, password):
+		if not isinstance(self.request, SSLSocket):
+			raise LDAPConfidentialityRequired()
+		try:
+			password = password.decode()
+		except UnicodeDecodeError:
+			raise LDAPInvalidCredentials()
+		try:
+			user = session.query(User).filter(usereval.filter_dn(dn, SearchScope.baseObject)).one_or_none()
+		except ValueError:
+			raise LDAPInvalidCredentials()
+		if user is None or not user.check_password(password):
+			raise LDAPInvalidCredentials()
+		return user
+
+	@property
+	def sasl_enable_external(self):
+		return self.request.family == socket.AF_UNIX
+
+	def do_bind_sasl_external(self, authzid=None):
+		ucred = struct.Struct('III')
+		data = self.request.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, ucred.size)
+		pid, uid, gid = ucred.unpack(data)
+		if not authzid:
+			return None
+		if uid != 0:
+			raise LDAPInvalidCredentials()
+		user = session.query(User).filter_by(loginname=authzid).one_or_none()
+		if user is None:
+			raise LDAPNoSuchObject()
+		return user
+
+	def do_whoami(self):
+		if self.bind_object is None:
+			return ''
+		return self.bind_object.loginname
+
+	def do_search(self, baseobj, scope, filter):
+		yield from super().do_search(baseobj, scope, filter)
+		if self.bind_object is not None:
+			yield from usereval(baseobj, scope, filter)
+			yield from groupeval(baseobj, scope, filter)
+
+socketserver.ForkingTCPServer(('127.0.0.1', 1337), RequestHandler).serve_forever()
+#socketserver.UnixStreamServer('/tmp/ldapd.sock', RequestHandler).serve_forever()
diff --git a/server.py b/server.py
index dc8050e93e7c1f4f9e4a200912c80d4494e051d1..a4dc39a3b9adedad8c5ab12dbef6a6273aeb35fc 100644
--- a/server.py
+++ b/server.py
@@ -1,10 +1,12 @@
 import traceback
 import hashlib
 import secrets
+import socket
+import ssl
 from socketserver import BaseRequestHandler
 
 import sasl
-from ldap import LDAPMessage, ShallowLDAPMessage, BindRequest, BindResponse, SearchRequest, SearchResultEntry, PartialAttribute, SearchResultDone, UnbindRequest, LDAPResultCode, IncompleteBERError, SimpleAuthentication, ModifyRequest, ModifyResponse, AddRequest, AddResponse, DelRequest, DelResponse, ModifyDNRequest, ModifyDNResponse, CompareRequest, CompareResponse, AbandonRequest, ExtendedRequest, ExtendedResponse, PasswdModifyRequestValue, PasswdModifyResponseValue, SaslCredentials
+from ldap import LDAPMessage, ShallowLDAPMessage, BindRequest, BindResponse, SearchRequest, SearchResultEntry, PartialAttribute, SearchResultDone, UnbindRequest, LDAPResultCode, IncompleteBERError, SimpleAuthentication, ModifyRequest, ModifyResponse, AddRequest, AddResponse, DelRequest, DelResponse, ModifyDNRequest, ModifyDNResponse, CompareRequest, CompareResponse, AbandonRequest, ExtendedRequest, ExtendedResponse, PasswdModifyRequestValue, PasswdModifyResponseValue, SaslCredentials, SearchScope, FilterPresent
 
 class LDAPError(Exception):
 	def __init__(self, code=LDAPResultCode.other, message=''):
@@ -39,6 +41,10 @@ class LDAPConfidentialityRequired(LDAPError):
 	def __init__(self, message=''):
 		super().__init__(LDAPResultCode.confidentialityRequired, message)
 
+class LDAPNoSuchObject(LDAPError):
+	def __init__(self, message=''):
+		super().__init__(LDAPResultCode.noSuchObject, message)
+
 def decode_msg(shallowmsg):
 	try:
 		return shallowmsg.decode()[0]
@@ -46,15 +52,94 @@ def decode_msg(shallowmsg):
 		traceback.print_exc()
 		raise LDAPProtocolError()
 
+def encode_attribute(value):
+	if isinstance(value, int):
+		value = str(value)
+	if isinstance(value, str):
+		value = value.encode()
+	return value
+
+class AttributeKey(str):
+	def __hash__(self):
+		return hash(self.lower())
+
+	def __eq__(self, value):
+		return self.lower() == value.lower()
+
+class AttributeDict(dict):
+	def __init__(self, *args, **kwargs):
+		if len(args) == 1 and isinstance(args[0], dict):
+			kwargs = {AttributeKey(k): v for k, v in args[0].items()}
+			args = []
+		else:
+			kwargs = {AttributeKey(k): v for k, v in kwargs.items()}
+			args = [(AttributeKey(k), v) for k, v in args]
+		super().__init__(*args, **kwargs)
+
+	def __contains__(self, key):
+		return super().__contains__(AttributeKey(key))
+
+	def __setitem__(self, key, value):
+		super().__setitem__(AttributeKey(key), value)
+
+	def __getitem__(self, key):
+		if key not in self:
+			self[key] = []
+		return super().__getitem__(AttributeKey(key))
+
+class RootDSE(AttributeDict):
+	def search(self, baseobj, scope, filter):
+		if baseobj or scope != SearchScope.baseObject:
+			return []
+		if not isinstance(filter, FilterPresent) or filter.attribute.lower() != 'objectclass':
+			return []
+		attrs = {}
+		for name, values in self.items():
+			if callable(values):
+				values = values()
+			if not isinstance(values, list):
+				values = [values]
+			if values:
+				attrs[name] = [encode_attribute(value) for value in values]
+		return [('', attrs)]
+
 class LDAPRequestHandler(BaseRequestHandler):
 	ssl_context = None
 
+	sasl_enable_anonymous = False
+	sasl_enable_plain = False
+	sasl_enable_external = False
+	sasl_enable_digest_md5 = False
+
 	def setup(self):
 		super().setup()
+		self.rootdse = RootDSE()
+		self.rootdse['objectClass'] = [b'top']
+		self.rootdse['supportedSASLMechanisms'] = self.get_sasl_mechanisms
+		self.rootdse['supportedExtension'] = self.get_extentions
+		self.rootdse['supportedLDAPVersion'] = [b'3']
 		self.keep_running = True
 		self.bind_object = None
 		self.bind_sasl_state = None
 
+	def get_extentions(self):
+		res = []
+		if self.ssl_context is not None and not isinstance(self.request, ssl.SSLSocket):
+			res.append(b'1.3.6.1.4.1.1466.20037')
+		return res
+
+	def get_sasl_mechanisms(self):
+		res = []
+		if self.sasl_enable_anonymous:
+			res.append(b'ANONYMOUS')
+		if self.sasl_enable_plain:
+			res.append(b'PLAIN')
+		if self.sasl_enable_external:
+			res.append(b'EXTERNAL')
+		if self.sasl_enable_digest_md5:
+			res.append(b'DIGEST-MD5')
+		return res
+
 	def handle_bind(self, req):
 		op = req.protocolOp
 		if op.version != 3:
@@ -77,7 +162,7 @@ class LDAPRequestHandler(BaseRequestHandler):
 		# If auth type or SASL method changed, abort SASL dialog
 		self.bind_sasl_state = None
 		if isinstance(auth, SimpleAuthentication):
-			self.bind_object = self.do_bind(op.name, auth.password)
+			self.bind_object = self.do_bind_simple(op.name, auth.password)
 			self.send_msg(LDAPMessage(req.messageID, BindResponse(LDAPResultCode.success)))
 		elif isinstance(auth, SaslCredentials):
 			ret = self.do_bind_sasl(auth.mechanism, auth.credentials)
@@ -114,7 +199,7 @@ class LDAPRequestHandler(BaseRequestHandler):
 			return self.do_bind_simple_anonymous()
 		if not password:
 			return self.do_bind_simple_unauthenticated(dn)
-		return self.do_bind_simple_unauthenticated(dn, password)
+		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.)
@@ -192,20 +277,20 @@ class LDAPRequestHandler(BaseRequestHandler):
 		if not mechanism:
 			# Request to abort current negotiation (RFC4513 5.2.1.2)
 			raise LDAPAuthMethodNotSupported()
-		if mechanism == 'ANONYMOUS':
+		if mechanism == 'ANONYMOUS' and self.sasl_enable_anonymous:
 			if credentials is not None:
 				credentials = credentials.decode()
 			return self.do_bind_sasl_anonymous(trace_info=credentials), None
-		if mechanism == 'PLAIN':
+		if mechanism == 'PLAIN' and self.sasl_enable_plain:
 			if credentials is None:
 				raise LDAPProtocolError('Unsupported protocol version')
 			authzid, authcid, password = credentials.split(b'\0', 2)
-			return do_bind_sasl_plain(authcid.decode(), password.decode(), authzid.decode() or None), None
-		if mechanism == 'EXTERNAL':
+			return self.do_bind_sasl_plain(authcid.decode(), password.decode(), authzid.decode() or None), None
+		if mechanism == 'EXTERNAL' and self.sasl_enable_external:
 			if credentials is not None:
 				credentials = credentials.decode()
 			return self.do_bind_sasl_external(authzid=credentials), None
-		if mechanism == 'DIGEST-MD5':
+		if mechanism == 'DIGEST-MD5' and self.sasl_enable_digest_md5:
 			return self.do_bind_sasl_digest_md5(credentials)
 		raise LDAPAuthMethodNotSupported()
 
@@ -338,7 +423,7 @@ class LDAPRequestHandler(BaseRequestHandler):
 		:returns: Iterable of dn, attributes tuples
 
 		The default implementation always returns an empty list.'''
-		return []
+		return self.rootdse.search(baseobj, scope, filter)
 
 	def handle_unbind(self, req):
 		self.keep_running = False
@@ -354,6 +439,10 @@ class LDAPRequestHandler(BaseRequestHandler):
 				sent_response = True
 			if not sent_response:
 				raise LDAPProtocolError()
+		elif op.requestName == '1.3.6.1.4.1.4203.1.11.3':
+			# "Who am I?" Operation (RFC 4532)
+			identity = (self.do_whoami() or '').encode()
+			self.send_msg(LDAPMessage(req.messageID, ExtendedResponse(LDAPResultCode.success, responseValue=identity)))
 		elif op.requestName == '1.3.6.1.4.1.4203.1.11.1':
 			# Password Modify Extended Operation (RFC 3062)
 			newpw = None
@@ -371,7 +460,7 @@ class LDAPRequestHandler(BaseRequestHandler):
 			raise LDAPProtocolError()
 
 	def do_starttls(self):
-		if self.ssl_context is None:
+		if self.ssl_context is None or isinstance(self.request, ssl.SSLSocket):
 			raise LDAPProtocolError()
 		yield None
 		try:
@@ -380,6 +469,15 @@ class LDAPRequestHandler(BaseRequestHandler):
 			traceback.print_exc()
 			self.keep_running = False
 
+	def do_whoami(self):
+		'''Do "Who am I" operation (RFC 4532)
+
+		:returns: Current authorization identity (authzid) or empty string for anonymous sessions
+		:rtype: str
+
+		The default implementation always returns an empty string.'''
+		return ''
+
 	def do_passwd(self, user=None, oldpasswd=None, newpasswd=None):
 		raise LDAPUnwillingToPerform('Password change is not supported')