# Copyright (C) 2001 Tommi Virtanen
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library 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 this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""LDAP protocol client"""
from ldaptor.protocols import pureldap, pureber
from ldaptor.protocols.ldap import ldaperrors
from twisted.python import log
from twisted.internet import protocol, defer, ssl, reactor
[docs]class LDAPClientConnectionLostException(ldaperrors.LDAPException):
def __str__(self):
return 'Connection lost'
[docs]class LDAPStartTLSBusyError(ldaperrors.LDAPOperationsError):
def __init__(self, onwire, message=None):
self.onwire = onwire
ldaperrors.LDAPOperationsError.__init__(self, message=message)
def __str__(self):
return 'Cannot STARTTLS while operations on wire: %r' % self.onwire
[docs]class LDAPClient(protocol.Protocol):
"""An LDAP client"""
debug = False
def __init__(self):
self.onwire = {}
self.buffer = ''
self.connected = None
berdecoder = pureldap.LDAPBERDecoderContext_TopLevel(
inherit=pureldap.LDAPBERDecoderContext_LDAPMessage(
fallback=pureldap.LDAPBERDecoderContext(fallback=pureber.BERDecoderContext()),
inherit=pureldap.LDAPBERDecoderContext(fallback=pureber.BERDecoderContext())))
[docs] def dataReceived(self, recd):
self.buffer += recd
while 1:
try:
o, bytes = pureber.berDecodeObject(self.berdecoder, self.buffer)
except pureber.BERExceptionInsufficientData:
o, bytes = None, 0
self.buffer = self.buffer[bytes:]
if not o:
break
self.handle(o)
[docs] def connectionMade(self):
"""TCP connection has opened"""
self.connected = 1
[docs] def connectionLost(self, reason=protocol.connectionDone):
"""Called when TCP connection has been lost"""
self.connected = 0
# notify handlers of operations in flight
while self.onwire:
k, v = self.onwire.popitem()
d, _, _, _ = v
d.errback(reason)
def _send(self, op):
if not self.connected:
raise LDAPClientConnectionLostException()
msg=pureldap.LDAPMessage(op)
if self.debug:
log.msg('C->S %s' % repr(msg))
assert not self.onwire.has_key(msg.id)
return msg
def _cbSend(self, msg, d):
d.callback(msg)
return True
[docs] def send(self, op):
"""
Send an LDAP operation to the server.
@param op: the operation to send
@type op: LDAPProtocolRequest
@return: the response from server
@rtype: Deferred LDAPProtocolResponse
"""
msg = self._send(op)
assert op.needs_answer
d = defer.Deferred()
self.onwire[msg.id]=(d, None, None, None)
self.transport.write(str(msg))
return d
[docs] def send_multiResponse(self, op, handler, *args, **kwargs):
"""
Send an LDAP operation to the server, expecting one or more
responses.
@param op: the operation to send
@type op: LDAPProtocolRequest
@param handler: a callable that will be called for each
response. It should return a boolean, whether this was the
final response.
@param args: positional arguments to pass to handler
@param kwargs: keyword arguments to pass to handler
@return: the result from the last handler as a deferred that
completes when the last response has been received
@rtype: Deferred LDAPProtocolResponse
"""
msg = self._send(op)
assert op.needs_answer
d = defer.Deferred()
self.onwire[msg.id]=(d, handler, args, kwargs)
self.transport.write(str(msg))
return d
[docs] def send_noResponse(self, op):
"""
Send an LDAP operation to the server, with no response
expected.
@param op: the operation to send
@type op: LDAPProtocolRequest
"""
msg = self._send(op)
assert not op.needs_answer
self.transport.write(str(msg))
[docs] def unsolicitedNotification(self, msg):
log.msg("Got unsolicited notification: %s" % repr(msg))
[docs] def handle(self, msg):
assert isinstance(msg.value, pureldap.LDAPProtocolResponse)
if self.debug:
log.msg('C<-S %s' % repr(msg))
if msg.id==0:
self.unsolicitedNotification(msg.value)
else:
d, handler, args, kwargs = self.onwire[msg.id]
if handler is None:
assert args is None
assert kwargs is None
d.callback(msg.value)
del self.onwire[msg.id]
else:
assert args is not None
assert kwargs is not None
# Return true to mark request as fully handled
if handler(msg.value, *args, **kwargs):
del self.onwire[msg.id]
##Bind
[docs] def bind(self, dn='', auth=''):
"""
@depreciated: Use e.bind(auth).
@todo: Remove this method when there are no callers.
"""
if not self.connected:
raise LDAPClientConnectionLostException()
else:
r=pureldap.LDAPBindRequest(dn=dn, auth=auth)
d = self.send(r)
d.addCallback(self._handle_bind_msg)
return d
def _handle_bind_msg(self, msg):
assert isinstance(msg, pureldap.LDAPBindResponse)
assert msg.referral is None #TODO
if msg.resultCode!=ldaperrors.Success.resultCode:
raise ldaperrors.get(msg.resultCode, msg.errorMessage)
return (msg.matchedDN, msg.serverSaslCreds)
##Unbind
[docs] def unbind(self):
if not self.connected:
raise Exception("Not connected (TODO)") #TODO make this a real object
r=pureldap.LDAPUnbindRequest()
self.send_noResponse(r)
self.transport.loseConnection()
def _cbStartTLS(self, msg, ctx):
assert isinstance(msg, pureldap.LDAPExtendedResponse)
assert msg.referral is None #TODO
if msg.resultCode!=ldaperrors.Success.resultCode:
raise ldaperrors.get(msg.resultCode, msg.errorMessage)
self.transport.startTLS(ctx)
return self
[docs] def startTLS(self, ctx=None):
"""
Start Transport Layer Security.
It is the callers responsibility to make sure other things
are not happening at the same time.
@todo: server hostname check, see rfc2830 section 3.6.
"""
if ctx is None:
ctx = ssl.ClientContextFactory()
# we always delay by one event loop iteration to make
# sure the previous handler has exited and self.onwire
# has been cleaned up
d=defer.Deferred()
d.addCallback(self._startTLS)
reactor.callLater(0, d.callback, ctx)
return d
def _startTLS(self, ctx):
if not self.connected:
raise LDAPClientConnectionLostException
elif self.onwire:
raise LDAPStartTLSBusyError, self.onwire
else:
op=pureldap.LDAPStartTLSRequest()
d = self.send(op)
d.addCallback(self._cbStartTLS, ctx)
return d