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

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

## 428 statements

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#

23 import os

24 import re

25 import socket

26 from binascii import unhexlify

27 import random

28

29 from pyasn1.codec.ber import encoder, decoder

30 from pyasn1.error import SubstrateUnderrunError

31 from pyasn1.type.univ import noValue

32

33 from impacket import LOG

34 from 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

37 from impacket.ntlm import getNTLMSSPType1, getNTLMSSPType3

38 from impacket.spnego import SPNEGO_NegTokenInit, TypesMech

39

40 try:

41 import OpenSSL

42 from OpenSSL import SSL, crypto

43 except:

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

54 DESCRIPTION = r'(?:[a-z][a-z0-9\-]*)'

55 NUMERIC_OID = r'(?:(?:\d|[1-9]\d+)(?:\.(?:\d|[1-9]\d+))*)'

56 OID = r'(?:%s|%s)' % (DESCRIPTION, NUMERIC_OID)

57 OPTIONS = r'(?:(?:;[a-z0-9\-]+)*)'

58 ATTRIBUTE = r'(%s%s)' % (OID, OPTIONS)

59 DN = r'(:dn)'

60 MATCHING_RULE = r'(?::(%s))' % OID

61

62 RE_OPERATOR = re.compile(r'([:<>~]?=)')

63 RE_ATTRIBUTE = re.compile(r'^%s$' % ATTRIBUTE, re.I)

64 RE_EX_ATTRIBUTE_1 = re.compile(r'^%s%s?%s?$' % (ATTRIBUTE, DN, MATCHING_RULE), re.I)

65 RE_EX_ATTRIBUTE_2 = re.compile(r'^(){0}%s?%s$' % (DN, MATCHING_RULE), re.I)

66

67

68 class 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

611 class LDAPFilterSyntaxError(SyntaxError):

612 pass

613

614

615 class LDAPFilterInvalidException(Exception):

616 pass

617

618

619 class 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

643 class 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