Hide keyboard shortcuts

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 

28 

29from pyasn1.codec.ber import encoder, decoder 

30from pyasn1.error import SubstrateUnderrunError 

31from pyasn1.type.univ import noValue 

32 

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 

39 

40try: 

41 import OpenSSL 

42 from OpenSSL import SSL, crypto 

43except: 

44 LOG.critical("pyOpenSSL is not installed, can't continue") 

45 raise 

46 

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] 

52 

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 

61 

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) 

66 

67 

68class LDAPConnection: 

69 def __init__(self, url, baseDN='', dstIp=None): 

70 """ 

71 LDAPConnection class 

72 

73 :param string url: 

74 :param string baseDN: 

75 :param string dstIp: 

76 

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 

85 

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) 

100 

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 

106 

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) 

113 

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() 

123 

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. 

128 

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 

139 

140 :return: True, raises a LDAPSessionError if error. 

141 """ 

142 

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 

153 

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 

161 

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 

164 

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) 

176 

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') 

192 

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) 

200 

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'] 

211 

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'] 

220 

221 # Let's build a NegTokenInit with a Kerberos REQ_AP 

222 

223 blob = SPNEGO_NegTokenInit() 

224 

225 # Kerberos 

226 blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] 

227 

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']) 

232 

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) 

237 

238 opts = [] 

239 apReq['ap-options'] = constants.encodeFlags(opts) 

240 seq_set(apReq, 'ticket', ticket.to_asn1) 

241 

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() 

247 

248 authenticator['cusec'] = now.microsecond 

249 authenticator['ctime'] = KerberosTime.to_asn1(now) 

250 

251 encodedAuthenticator = encoder.encode(authenticator) 

252 

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) 

258 

259 apReq['authenticator'] = noValue 

260 apReq['authenticator']['etype'] = cipher.enctype 

261 apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator 

262 

263 blob['MechToken'] = encoder.encode(apReq) 

264 

265 # Done with the Kerberos saga, now let's get into LDAP 

266 

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() 

272 

273 response = self.sendReceive(bindRequest)[0]['protocolOp'] 

274 

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 ) 

280 

281 return True 

282 

283 def login(self, user='', password='', domain='', lmhash='', nthash='', authenticationChoice='sicilyNegotiate'): 

284 """ 

285 logins into the target system 

286 

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) 

293 

294 :return: True, raises a LDAPSessionError if error. 

295 """ 

296 bindRequest = BindRequest() 

297 bindRequest['version'] = 3 

298 

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 

324 

325 bindRequest['name'] = user 

326 

327 # NTLM Negotiate 

328 negotiate = getNTLMSSPType1('', domain) 

329 bindRequest['authentication']['sicilyNegotiate'] = negotiate.getData() 

330 response = self.sendReceive(bindRequest)[0]['protocolOp'] 

331 

332 # NTLM Challenge 

333 type2 = response['bindResponse']['matchedDN'] 

334 

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) 

341 

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 ) 

347 

348 return True 

349 

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 = [] 

360 

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) 

370 

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) 

393 

394 return answers 

395 

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 

415 

416 def close(self): 

417 if self._socket is not None: 

418 self._socket.close() 

419 

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) 

426 

427 data = encoder.encode(message) 

428 

429 return self._socket.sendall(data) 

430 

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 

440 

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 

462 

463 return response 

464 

465 def sendReceive(self, request, controls=None): 

466 self.send(request, controls) 

467 return self.recv() 

468 

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 

479 

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) 

488 

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) 

496 

497 filters = [] 

498 while True: 

499 try: 

500 filters.append(self._consumeCompositeFilter(filterList)) 

501 except LDAPFilterSyntaxError: 

502 break 

503 

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) 

511 

512 return self._compileCompositeFilter(operator, filters) 

513 

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) 

522 

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) 

536 

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) 

543 

544 return self._compileSimpleFilter(attribute, operator, value) 

545 

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) 

561 

562 return searchFilter 

563 

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)) 

607 

608 return searchFilter 

609 

610 

611class LDAPFilterSyntaxError(SyntaxError): 

612 pass 

613 

614 

615class LDAPFilterInvalidException(Exception): 

616 pass 

617 

618 

619class LDAPSessionError(Exception): 

620 """ 

621 This is the exception every client should catch 

622 """ 

623 

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 

629 

630 def getErrorCode(self): 

631 return self.error 

632 

633 def getErrorPacket(self): 

634 return self.packet 

635 

636 def getErrorString(self): 

637 return self.errorString 

638 

639 def __str__(self): 

640 return self.errorString 

641 

642 

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 

649 

650 def getAnswers(self): 

651 return self.answers