Coverage for /root/GitHubProjects/impacket/impacket/ldap/ldap.py : 52%

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