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