1# Copyright (c) 2016 CORE Security Technologies
2#
3# This software is provided under under a slightly modified version
4# of the Apache Software License. See the accompanying LICENSE file
5# for more information.
6#
7# Authors: Alberto Solino (@agsolino)
8#          Kacper Nowak (@kacpern)
9#
10# Description:
11#   RFC 4511 Minimalistic implementation. We don't need much functionality yet
12#   If we need more complex use cases we might opt to use a third party implementation
13#   Keep in mind the APIs are still unstable, might require to re-write your scripts
14#   as we change them.
15#   Adding [MS-ADTS] specific functionality
16#
17# ToDo:
18# [x] Implement Paging Search, especially important for big requests
19#
20
21import os
22import re
23import socket
24from binascii import unhexlify
25
26from pyasn1.codec.ber import encoder, decoder
27from pyasn1.error import SubstrateUnderrunError
28from pyasn1.type.univ import noValue
29
30from impacket import LOG
31from impacket.ldap.ldapasn1 import *
32from impacket.ntlm import getNTLMSSPType1, getNTLMSSPType3
33from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
34
35try:
36    import OpenSSL
37    from OpenSSL import SSL, crypto
38except:
39    LOG.critical("pyOpenSSL is not installed, can't continue")
40    raise
41
42__all__ = [
43    'LDAPConnection', 'LDAPFilterSyntaxError', 'LDAPFilterInvalidException', 'LDAPSessionError', 'LDAPSearchError',
44    'Control', 'SimplePagedResultsControl', 'ResultCode', 'Scope', 'DerefAliases', 'Operation',
45    'CONTROL_PAGEDRESULTS', 'KNOWN_CONTROLS', 'NOTIFICATION_DISCONNECT', 'KNOWN_NOTIFICATIONS',
46]
47
48# https://tools.ietf.org/search/rfc4515#section-3
49DESCRIPTION = r'(?:[a-z][a-z0-9\-]*)'
50NUMERIC_OID = r'(?:(?:\d|[1-9]\d+)(?:\.(?:\d|[1-9]\d+))*)'
51OID = r'(?:%s|%s)' % (DESCRIPTION, NUMERIC_OID)
52OPTIONS = r'(?:(?:;[a-z0-9\-]+)*)'
53ATTRIBUTE = r'(%s%s)' % (OID, OPTIONS)
54DN = r'(:dn)'
55MATCHING_RULE = r'(?::(%s))' % OID
56
57RE_OPERATOR = re.compile(r'([:<>~]?=)')
58RE_ATTRIBUTE = re.compile(r'^%s$' % ATTRIBUTE, re.I)
59RE_EX_ATTRIBUTE_1 = re.compile(r'^%s%s?%s?$' % (ATTRIBUTE, DN, MATCHING_RULE), re.I)
60RE_EX_ATTRIBUTE_2 = re.compile(r'^(){0}%s?%s$' % (DN, MATCHING_RULE), re.I)
61
62
63class LDAPConnection:
64    def __init__(self, url, baseDN='', dstIp=None):
65        """
66        LDAPConnection class
67
68        :param string url:
69        :param string baseDN:
70        :param string dstIp:
71
72        :return: a LDAP instance, if not raises a LDAPSessionError exception
73        """
74        self._SSL = False
75        self._dstPort = 0
76        self._dstHost = 0
77        self._socket = None
78        self._baseDN = baseDN
79        self._messageId = 1
80        self._dstIp = dstIp
81
82        if url.startswith('ldap://'):
83            self._dstPort = 389
84            self._SSL = False
85            self._dstHost = url[7:]
86        elif url.startswith('ldaps://'):
87            self._dstPort = 636
88            self._SSL = True
89            self._dstHost = url[8:]
90        elif url.startswith('gc://'):
91            self._dstPort = 3268
92            self._SSL = False
93            self._dstHost = url[5:]
94        else:
95            raise LDAPSessionError(errorString="Unknown URL prefix: '%s'" % url)
96
97        # Try to connect
98        if self._dstIp is not None:
99            targetHost = self._dstIp
100        else:
101            targetHost = self._dstHost
102
103        LOG.debug('Connecting to %s, port %d, SSL %s' % (targetHost, self._dstPort, self._SSL))
104        try:
105            af, socktype, proto, _, sa = socket.getaddrinfo(targetHost, self._dstPort, 0, socket.SOCK_STREAM)[0]
106            self._socket = socket.socket(af, socktype, proto)
107        except socket.error as e:
108            raise socket.error('Connection error (%s:%d)' % (targetHost, 88), e)
109
110        if self._SSL is False:
111            self._socket.connect(sa)
112        else:
113            # Switching to TLS now
114            ctx = SSL.Context(SSL.TLSv1_METHOD)
115            # ctx.set_cipher_list('RC4')
116            self._socket = SSL.Connection(ctx, self._socket)
117            self._socket.connect(sa)
118            self._socket.do_handshake()
119
120    def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None,
121                      TGS=None, useCache=True):
122        """
123        logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
124
125        :param string user: username
126        :param string password: password for the user
127        :param string domain: domain where the account is valid for (required)
128        :param string lmhash: LMHASH used to authenticate using hashes (password is not used)
129        :param string nthash: NTHASH used to authenticate using hashes (password is not used)
130        :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
131        :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
132        :param struct TGT: If there's a TGT available, send the structure here and it will be used
133        :param struct TGS: same for TGS. See smb3.py for the format
134        :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
135
136        :return: True, raises a LDAPSessionError if error.
137        """
138
139        if lmhash != '' or nthash != '':
140            if len(lmhash) % 2:
141                lmhash = '0' + lmhash
142            if len(nthash) % 2:
143                nthash = '0' + nthash
144            try:  # just in case they were converted already
145                lmhash = unhexlify(lmhash)
146                nthash = unhexlify(nthash)
147            except TypeError:
148                pass
149
150        # Importing down here so pyasn1 is not required if kerberos is not used.
151        from impacket.krb5.ccache import CCache
152        from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
153        from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
154        from impacket.krb5 import constants
155        from impacket.krb5.types import Principal, KerberosTime, Ticket
156        import datetime
157
158        if TGT is not None or TGS is not None:
159            useCache = False
160
161        if useCache:
162            try:
163                ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
164            except:
165                # No cache present
166                pass
167            else:
168                # retrieve domain information from CCache file if needed
169                if domain == '':
170                    domain = ccache.principal.realm['data']
171                    LOG.debug('Domain retrieved from CCache: %s' % domain)
172
173                LOG.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
174                principal = 'ldap/%s@%s' % (self._dstHost.upper(), domain.upper())
175                creds = ccache.getCredential(principal)
176                if creds is None:
177                    # Let's try for the TGT and go from there
178                    principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
179                    creds = ccache.getCredential(principal)
180                    if creds is not None:
181                        TGT = creds.toTGT()
182                        LOG.debug('Using TGT from cache')
183                    else:
184                        LOG.debug('No valid credentials found in cache')
185                else:
186                    TGS = creds.toTGS(principal)
187                    LOG.debug('Using TGS from cache')
188
189                # retrieve user information from CCache file if needed
190                if user == '' and creds is not None:
191                    user = creds['client'].prettyPrint().split('@')[0]
192                    LOG.debug('Username retrieved from CCache: %s' % user)
193                elif user == '' and len(ccache.principal.components) > 0:
194                    user = ccache.principal.components[0]['data']
195                    LOG.debug('Username retrieved from CCache: %s' % user)
196
197        # First of all, we need to get a TGT for the user
198        userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
199        if TGT is None:
200            if TGS is None:
201                tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash,
202                                                                        aesKey, kdcHost)
203        else:
204            tgt = TGT['KDC_REP']
205            cipher = TGT['cipher']
206            sessionKey = TGT['sessionKey']
207
208        if TGS is None:
209            serverName = Principal('ldap/%s' % self._dstHost, type=constants.PrincipalNameType.NT_SRV_INST.value)
210            tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher,
211                                                                    sessionKey)
212        else:
213            tgs = TGS['KDC_REP']
214            cipher = TGS['cipher']
215            sessionKey = TGS['sessionKey']
216
217            # Let's build a NegTokenInit with a Kerberos REQ_AP
218
219        blob = SPNEGO_NegTokenInit()
220
221        # Kerberos
222        blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
223
224        # Let's extract the ticket from the TGS
225        tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
226        ticket = Ticket()
227        ticket.from_asn1(tgs['ticket'])
228
229        # Now let's build the AP_REQ
230        apReq = AP_REQ()
231        apReq['pvno'] = 5
232        apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
233
234        opts = []
235        apReq['ap-options'] = constants.encodeFlags(opts)
236        seq_set(apReq, 'ticket', ticket.to_asn1)
237
238        authenticator = Authenticator()
239        authenticator['authenticator-vno'] = 5
240        authenticator['crealm'] = domain
241        seq_set(authenticator, 'cname', userName.components_to_asn1)
242        now = datetime.datetime.utcnow()
243
244        authenticator['cusec'] = now.microsecond
245        authenticator['ctime'] = KerberosTime.to_asn1(now)
246
247        encodedAuthenticator = encoder.encode(authenticator)
248
249        # Key Usage 11
250        # AP-REQ Authenticator (includes application authenticator
251        # subkey), encrypted with the application session key
252        # (Section 5.5.1)
253        encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)
254
255        apReq['authenticator'] = noValue
256        apReq['authenticator']['etype'] = cipher.enctype
257        apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
258
259        blob['MechToken'] = encoder.encode(apReq)
260
261        # Done with the Kerberos saga, now let's get into LDAP
262
263        bindRequest = BindRequest()
264        bindRequest['version'] = 3
265        bindRequest['name'] = user
266        bindRequest['authentication']['sasl']['mechanism'] = 'GSS-SPNEGO'
267        bindRequest['authentication']['sasl']['credentials'] = blob.getData()
268
269        response = self.sendReceive(bindRequest)[0]['protocolOp']
270
271        if response['bindResponse']['resultCode'] != ResultCode('success'):
272            raise LDAPSessionError(
273                errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(),
274                                                                response['bindResponse']['diagnosticMessage'])
275            )
276
277        return True
278
279    def login(self, user='', password='', domain='', lmhash='', nthash='', authenticationChoice='sicilyNegotiate'):
280        """
281        logins into the target system
282
283        :param string user: username
284        :param string password: password for the user
285        :param string domain: domain where the account is valid for
286        :param string lmhash: LMHASH used to authenticate using hashes (password is not used)
287        :param string nthash: NTHASH used to authenticate using hashes (password is not used)
288        :param string authenticationChoice: type of authentication protocol to use (default NTLM)
289
290        :return: True, raises a LDAPSessionError if error.
291        """
292        bindRequest = BindRequest()
293        bindRequest['version'] = 3
294
295        if authenticationChoice == 'simple':
296            if '.' in domain:
297                bindRequest['name'] = user + '@' + domain
298            elif domain:
299                bindRequest['name'] = domain + '\\' + user
300            else:
301                bindRequest['name'] = user
302            bindRequest['authentication']['simple'] = password
303            response = self.sendReceive(bindRequest)[0]['protocolOp']
304        elif authenticationChoice == 'sicilyPackageDiscovery':
305            bindRequest['name'] = user
306            bindRequest['authentication']['sicilyPackageDiscovery'] = ''
307            response = self.sendReceive(bindRequest)[0]['protocolOp']
308        elif authenticationChoice == 'sicilyNegotiate':
309            # Deal with NTLM Authentication
310            if lmhash != '' or nthash != '':
311                if len(lmhash) % 2:
312                    lmhash = '0' + lmhash
313                if len(nthash) % 2:
314                    nthash = '0' + nthash
315                try:  # just in case they were converted already
316                    lmhash = unhexlify(lmhash)
317                    nthash = unhexlify(nthash)
318                except TypeError:
319                    pass
320
321            bindRequest['name'] = user
322
323            # NTLM Negotiate
324            negotiate = getNTLMSSPType1('', domain)
325            bindRequest['authentication']['sicilyNegotiate'] = negotiate
326            response = self.sendReceive(bindRequest)[0]['protocolOp']
327
328            # NTLM Challenge
329            type2 = response['bindResponse']['matchedDN']
330
331            # NTLM Auth
332            type3, exportedSessionKey = getNTLMSSPType3(negotiate, str(type2), user, password, domain, lmhash, nthash)
333            bindRequest['authentication']['sicilyResponse'] = type3
334            response = self.sendReceive(bindRequest)[0]['protocolOp']
335        else:
336            raise LDAPSessionError(errorString="Unknown authenticationChoice: '%s'" % authenticationChoice)
337
338        if response['bindResponse']['resultCode'] != ResultCode('success'):
339            raise LDAPSessionError(
340                errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(),
341                                                                response['bindResponse']['diagnosticMessage'])
342            )
343
344        return True
345
346    def search(self, searchBase=None, scope=None, derefAliases=None, sizeLimit=0, timeLimit=0, typesOnly=False,
347               searchFilter='(objectClass=*)', attributes=None, searchControls=None, perRecordCallback=None):
348        if searchBase is None:
349            searchBase = self._baseDN
350        if scope is None:
351            scope = Scope('wholeSubtree')
352        if derefAliases is None:
353            derefAliases = DerefAliases('neverDerefAliases')
354        if attributes is None:
355            attributes = []
356
357        searchRequest = SearchRequest()
358        searchRequest['baseObject'] = searchBase
359        searchRequest['scope'] = scope
360        searchRequest['derefAliases'] = derefAliases
361        searchRequest['sizeLimit'] = sizeLimit
362        searchRequest['timeLimit'] = timeLimit
363        searchRequest['typesOnly'] = typesOnly
364        searchRequest['filter'] = self._parseFilter(searchFilter)
365        searchRequest['attributes'].setComponents(*attributes)
366
367        done = False
368        answers = []
369        # We keep asking records until we get a SearchResultDone packet and all controls are handled
370        while not done:
371            response = self.sendReceive(searchRequest, searchControls)
372            for message in response:
373                searchResult = message['protocolOp'].getComponent()
374                if searchResult.isSameTypeWith(SearchResultDone()):
375                    if searchResult['resultCode'] == ResultCode('success'):
376                        done = self._handleControls(searchControls, message['controls'])
377                    else:
378                        raise LDAPSearchError(
379                            error=int(searchResult['resultCode']),
380                            errorString='Error in searchRequest -> %s: %s' % (searchResult['resultCode'].prettyPrint(),
381                                                                              searchResult['diagnosticMessage']),
382                            answers=answers
383                        )
384                else:
385                    if perRecordCallback is None:
386                        answers.append(searchResult)
387                    else:
388                        perRecordCallback(searchResult)
389
390        return answers
391
392    def _handleControls(self, requestControls, responseControls):
393        done = True
394        if requestControls is not None:
395            for requestControl in requestControls:
396                if responseControls is not None:
397                    for responseControl in responseControls:
398                        if requestControl['controlType'] == CONTROL_PAGEDRESULTS:
399                            if responseControl['controlType'] == CONTROL_PAGEDRESULTS:
400                                if hasattr(responseControl, 'getCookie') is not True:
401                                    responseControl = decoder.decode(encoder.encode(responseControl),
402                                                                 asn1Spec=KNOWN_CONTROLS[CONTROL_PAGEDRESULTS]())[0]
403                                if responseControl.getCookie():
404                                    done = False
405                                requestControl.setCookie(responseControl.getCookie())
406                                break
407                        else:
408                            # handle different controls here
409                            pass
410        return done
411
412    def close(self):
413        if self._socket is not None:
414            self._socket.close()
415
416    def send(self, request, controls=None):
417        message = LDAPMessage()
418        message['messageID'] = self._messageId
419        message['protocolOp'].setComponentByType(request.getTagSet(), request)
420        if controls is not None:
421            message['controls'].setComponents(*controls)
422
423        data = encoder.encode(message)
424
425        return self._socket.sendall(data)
426
427    def recv(self):
428        REQUEST_SIZE = 8192
429        data = ''
430        done = False
431        while not done:
432            recvData = self._socket.recv(REQUEST_SIZE)
433            if len(recvData) < REQUEST_SIZE:
434                done = True
435            data += recvData
436
437        response = []
438        while len(data) > 0:
439            try:
440                message, remaining = decoder.decode(data, asn1Spec=LDAPMessage())
441            except SubstrateUnderrunError:
442                # We need more data
443                remaining = data + self._socket.recv(REQUEST_SIZE)
444            else:
445                if message['messageID'] == 0:  # unsolicited notification
446                    name = message['protocolOp']['extendedResp']['responseName'] or message['responseName']
447                    notification = KNOWN_NOTIFICATIONS.get(name, "Unsolicited Notification '%s'" % name)
448                    if name == NOTIFICATION_DISCONNECT:  # Server has disconnected
449                        self.close()
450                    raise LDAPSessionError(
451                        error=int(message['protocolOp']['extendedResp']['resultCode']),
452                        errorString='%s -> %s: %s' % (notification,
453                                                      message['protocolOp']['extendedResp']['resultCode'].prettyPrint(),
454                                                      message['protocolOp']['extendedResp']['diagnosticMessage'])
455                    )
456                response.append(message)
457            data = remaining
458
459        self._messageId += 1
460        return response
461
462    def sendReceive(self, request, controls=None):
463        self.send(request, controls)
464        return self.recv()
465
466    def _parseFilter(self, filterStr):
467        try:
468            filterList = list(reversed(unicode(filterStr)))
469        except UnicodeDecodeError:
470            filterList = list(reversed(filterStr))
471        searchFilter = self._consumeCompositeFilter(filterList)
472        if filterList:  # we have not consumed the whole filter string
473            raise LDAPFilterSyntaxError("unexpected token: '%s'" % filterList[-1])
474        return searchFilter
475
476    def _consumeCompositeFilter(self, filterList):
477        try:
478            c = filterList.pop()
479        except IndexError:
480            raise LDAPFilterSyntaxError('EOL while parsing search filter')
481        if c != '(':  # filter must start with a '('
482            filterList.append(c)
483            raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)
484
485        try:
486            operator = filterList.pop()
487        except IndexError:
488            raise LDAPFilterSyntaxError('EOL while parsing search filter')
489        if operator not in ['!', '&', '|']:  # must be simple filter in this case
490            filterList.extend([operator, c])
491            return self._consumeSimpleFilter(filterList)
492
493        filters = []
494        while True:
495            try:
496                filters.append(self._consumeCompositeFilter(filterList))
497            except LDAPFilterSyntaxError:
498                break
499
500        try:
501            c = filterList.pop()
502        except IndexError:
503            raise LDAPFilterSyntaxError('EOL while parsing search filter')
504        if c != ')':  # filter must end with a ')'
505            filterList.append(c)
506            raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)
507
508        return self._compileCompositeFilter(operator, filters)
509
510    def _consumeSimpleFilter(self, filterList):
511        try:
512            c = filterList.pop()
513        except IndexError:
514            raise LDAPFilterSyntaxError('EOL while parsing search filter')
515        if c != '(':  # filter must start with a '('
516            filterList.append(c)
517            raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)
518
519        filter = []
520        while True:
521            try:
522                c = filterList.pop()
523            except IndexError:
524                raise LDAPFilterSyntaxError('EOL while parsing search filter')
525            if c == ')':  # we pop till we find a ')'
526                break
527            elif c == '(':  # should be no unencoded parenthesis
528                filterList.append(c)
529                raise LDAPFilterSyntaxError("unexpected token: '('")
530            else:
531                filter.append(c)
532
533        filterStr = ''.join(filter)
534        try:
535            # https://tools.ietf.org/search/rfc4515#section-3
536            attribute, operator, value = RE_OPERATOR.split(filterStr, 1)
537        except ValueError:
538            raise LDAPFilterInvalidException("invalid filter: '(%s)'" % filterStr)
539
540        return self._compileSimpleFilter(attribute, operator, value)
541
542    @staticmethod
543    def _compileCompositeFilter(operator, filters):
544        searchFilter = Filter()
545        if operator == '!':
546            if len(filters) != 1:
547                raise LDAPFilterInvalidException("'not' filter must have exactly one element")
548            searchFilter['not'].setComponents(*filters)
549        elif operator == '&':
550            if len(filters) == 0:
551                raise LDAPFilterInvalidException("'and' filter must have at least one element")
552            searchFilter['and'].setComponents(*filters)
553        elif operator == '|':
554            if len(filters) == 0:
555                raise LDAPFilterInvalidException("'or' filter must have at least one element")
556            searchFilter['or'].setComponents(*filters)
557
558        return searchFilter
559
560    @staticmethod
561    def _compileSimpleFilter(attribute, operator, value):
562        searchFilter = Filter()
563        if operator == ':=':  # extensibleMatch
564            match = RE_EX_ATTRIBUTE_1.match(attribute) or RE_EX_ATTRIBUTE_2.match(attribute)
565            if not match:
566                raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute)
567            attribute, dn, matchingRule = match.groups()
568            if attribute:
569                searchFilter['extensibleMatch']['type'] = attribute
570            if dn:
571                searchFilter['extensibleMatch']['dnAttributes'] = bool(dn)
572            if matchingRule:
573                searchFilter['extensibleMatch']['matchingRule'] = matchingRule
574            searchFilter['extensibleMatch']['matchValue'] = value
575        else:
576            if not RE_ATTRIBUTE.match(attribute):
577                raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute)
578            if value == '*' and operator == '=':  # present
579                searchFilter['present'] = attribute
580            elif '*' in value and operator == '=':  # substring
581                assertions = value.split('*')
582                choice = searchFilter['substrings']['substrings'].getComponentType()
583                substrings = []
584                if assertions[0]:
585                    substrings.append(choice.clone().setComponentByName('initial', assertions[0]))
586                for assertion in assertions[1:-1]:
587                    substrings.append(choice.clone().setComponentByName('any', assertion))
588                if assertions[-1]:
589                    substrings.append(choice.clone().setComponentByName('final', assertions[-1]))
590                searchFilter['substrings']['type'] = attribute
591                searchFilter['substrings']['substrings'].setComponents(*substrings)
592            elif '*' not in value:  # simple
593                if operator == '=':
594                    searchFilter['equalityMatch'].setComponents(attribute, value)
595                elif operator == '~=':
596                    searchFilter['approxMatch'].setComponents(attribute, value)
597                elif operator == '>=':
598                    searchFilter['greaterOrEqual'].setComponents(attribute, value)
599                elif operator == '<=':
600                    searchFilter['lessOrEqual'].setComponents(attribute, value)
601            else:
602                raise LDAPFilterInvalidException("invalid filter '(%s%s%s)'" % (attribute, operator, value))
603
604        return searchFilter
605
606
607class LDAPFilterSyntaxError(SyntaxError):
608    pass
609
610
611class LDAPFilterInvalidException(Exception):
612    pass
613
614
615class LDAPSessionError(Exception):
616    """
617    This is the exception every client should catch
618    """
619
620    def __init__(self, error=0, packet=0, errorString=''):
621        Exception.__init__(self)
622        self.error = error
623        self.packet = packet
624        self.errorString = errorString
625
626    def getErrorCode(self):
627        return self.error
628
629    def getErrorPacket(self):
630        return self.packet
631
632    def getErrorString(self):
633        return self.errorString
634
635    def __str__(self):
636        return self.errorString
637
638
639class LDAPSearchError(LDAPSessionError):
640    def __init__(self, error=0, packet=0, errorString='', answers=None):
641        LDAPSessionError.__init__(self, error, packet, errorString)
642        if answers is None:
643            answers = []
644        self.answers = answers
645
646    def getAnswers(self):
647        return self.answers
648