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# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.

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

21 import os

22 import re

23 import socket

24 from binascii import unhexlify

25 import random

26

27 from pyasn1.codec.ber import encoder, decoder

28 from pyasn1.error import SubstrateUnderrunError

29 from pyasn1.type.univ import noValue

30

31 from impacket import LOG

32 from impacket.ldap.ldapasn1 import Filter, Control, SimplePagedResultsControl, ResultCode, Scope, DerefAliases, Operation, \

33 KNOWN_CONTROLS, CONTROL_PAGEDRESULTS, NOTIFICATION_DISCONNECT, KNOWN_NOTIFICATIONS, BindRequest, SearchRequest, \

34 SearchResultDone, LDAPMessage

35 from impacket.ntlm import getNTLMSSPType1, getNTLMSSPType3

36 from impacket.spnego import SPNEGO_NegTokenInit, TypesMech

37

38 try:

39 import OpenSSL

40 from OpenSSL import SSL, crypto

41 except:

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

43 raise

44

45 __all__ = [

46 'LDAPConnection', 'LDAPFilterSyntaxError', 'LDAPFilterInvalidException', 'LDAPSessionError', 'LDAPSearchError',

47 'Control', 'SimplePagedResultsControl', 'ResultCode', 'Scope', 'DerefAliases', 'Operation',

48 'CONTROL_PAGEDRESULTS', 'KNOWN_CONTROLS', 'NOTIFICATION_DISCONNECT', 'KNOWN_NOTIFICATIONS',

49]

50

51# https://tools.ietf.org/search/rfc4515#section-3

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

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

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

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

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

57 DN = r'(:dn)'

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

59

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

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

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

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

64

65

66 class LDAPConnection:

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

68 """

69 LDAPConnection class

70

71 :param string url:

72 :param string baseDN:

73 :param string dstIp:

74

75 :return: a LDAP instance, if not raises a LDAPSessionError exception

76 """

77 self._SSL = False

78 self._dstPort = 0

79 self._dstHost = 0

80 self._socket = None

81 self._baseDN = baseDN

82 self._dstIp = dstIp

83

84 if url.startswith('ldap://'): 84 ↛ 88line 84 didn't jump to line 88, because the condition on line 84 was never false

85 self._dstPort = 389

86 self._SSL = False

87 self._dstHost = url[7:]

88 elif url.startswith('ldaps://'):

89 self._dstPort = 636

90 self._SSL = True

91 self._dstHost = url[8:]

92 elif url.startswith('gc://'):

93 self._dstPort = 3268

94 self._SSL = False

95 self._dstHost = url[5:]

96 else:

97 raise LDAPSessionError(errorString="Unknown URL prefix: '%s'" % url)

98

99 # Try to connect

100 if self._dstIp is not None: 100 ↛ 101line 100 didn't jump to line 101, because the condition on line 100 was never true

101 targetHost = self._dstIp

102 else:

103 targetHost = self._dstHost

104

105 LOG.debug('Connecting to %s, port %d, SSL %s' % (targetHost, self._dstPort, self._SSL))

106 try:

107 af, socktype, proto, _, sa = socket.getaddrinfo(targetHost, self._dstPort, 0, socket.SOCK_STREAM)[0]

108 self._socket = socket.socket(af, socktype, proto)

109 except socket.error as e:

110 raise socket.error('Connection error (%s:%d)' % (targetHost, 88), e)

111

112 if self._SSL is False: 112 ↛ 116line 112 didn't jump to line 116, because the condition on line 112 was never false

113 self._socket.connect(sa)

114 else:

115 # Switching to TLS now

116 ctx = SSL.Context(SSL.TLSv1_METHOD)

117 # ctx.set_cipher_list('RC4')

118 self._socket = SSL.Connection(ctx, self._socket)

119 self._socket.connect(sa)

120 self._socket.do_handshake()

121

122 def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None,

123 TGS=None, useCache=True):

124 """

125 logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.

126

127 :param string user: username

128 :param string password: password for the user

129 :param string domain: domain where the account is valid for (required)

130 :param string lmhash: LMHASH used to authenticate using hashes (password is not used)

131 :param string nthash: NTHASH used to authenticate using hashes (password is not used)

132 :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication

133 :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)

134 :param struct TGT: If there's a TGT available, send the structure here and it will be used

135 :param struct TGS: same for TGS. See smb3.py for the format

136 :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False

137

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

139 """

140

141 if lmhash != '' or nthash != '':

142 if len(lmhash) % 2: 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true

143 lmhash = '0' + lmhash

144 if len(nthash) % 2: 144 ↛ 145line 144 didn't jump to line 145, because the condition on line 144 was never true

145 nthash = '0' + nthash

146 try: # just in case they were converted already

147 lmhash = unhexlify(lmhash)

148 nthash = unhexlify(nthash)

149 except TypeError:

150 pass

151

152 # Importing down here so pyasn1 is not required if kerberos is not used.

153 from impacket.krb5.ccache import CCache

154 from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set

155 from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS

156 from impacket.krb5 import constants

157 from impacket.krb5.types import Principal, KerberosTime, Ticket

158 import datetime

159

160 if TGT is not None or TGS is not None: 160 ↛ 161line 160 didn't jump to line 161, because the condition on line 160 was never true

161 useCache = False

162

163 if useCache: 163 ↛ 200line 163 didn't jump to line 200, because the condition on line 163 was never false

164 try:

165 ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))

166 except:

167 # No cache present

168 pass

169 else:

170 # retrieve domain information from CCache file if needed

171 if domain == '':

172 domain = ccache.principal.realm['data'].decode('utf-8')

173 LOG.debug('Domain retrieved from CCache: %s' % domain)

174

175 LOG.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))

176 principal = 'ldap/%s@%s' % (self._dstHost.upper(), domain.upper())

177 creds = ccache.getCredential(principal)

178 if creds is None:

179 # Let's try for the TGT and go from there

180 principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())

181 creds = ccache.getCredential(principal)

182 if creds is not None:

183 TGT = creds.toTGT()

184 LOG.debug('Using TGT from cache')

185 else:

186 LOG.debug('No valid credentials found in cache')

187 else:

188 TGS = creds.toTGS(principal)

189 LOG.debug('Using TGS from cache')

190

191 # retrieve user information from CCache file if needed

192 if user == '' and creds is not None:

193 user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')

194 LOG.debug('Username retrieved from CCache: %s' % user)

195 elif user == '' and len(ccache.principal.components) > 0:

196 user = ccache.principal.components[0]['data'].decode('utf-8')

197 LOG.debug('Username retrieved from CCache: %s' % user)

198

199 # First of all, we need to get a TGT for the user

200 userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)

201 if TGT is None: 201 ↛ 206line 201 didn't jump to line 206, because the condition on line 201 was never false

202 if TGS is None: 202 ↛ 210line 202 didn't jump to line 210, because the condition on line 202 was never false

203 tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash,

204 aesKey, kdcHost)

205 else:

206 tgt = TGT['KDC_REP']

207 cipher = TGT['cipher']

208 sessionKey = TGT['sessionKey']

209

210 if TGS is None: 210 ↛ 215line 210 didn't jump to line 215, because the condition on line 210 was never false

211 serverName = Principal('ldap/%s' % self._dstHost, type=constants.PrincipalNameType.NT_SRV_INST.value)

212 tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher,

213 sessionKey)

214 else:

215 tgs = TGS['KDC_REP']

216 cipher = TGS['cipher']

217 sessionKey = TGS['sessionKey']

218

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

220

221 blob = SPNEGO_NegTokenInit()

222

223 # Kerberos

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

225

226 # Let's extract the ticket from the TGS

227 tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]

228 ticket = Ticket()

229 ticket.from_asn1(tgs['ticket'])

230

231 # Now let's build the AP_REQ

232 apReq = AP_REQ()

233 apReq['pvno'] = 5

234 apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)

235

236 opts = []

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

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

239

240 authenticator = Authenticator()

241 authenticator['authenticator-vno'] = 5

242 authenticator['crealm'] = domain

243 seq_set(authenticator, 'cname', userName.components_to_asn1)

244 now = datetime.datetime.utcnow()

245

246 authenticator['cusec'] = now.microsecond

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

248

249 encodedAuthenticator = encoder.encode(authenticator)

250

251 # Key Usage 11

252 # AP-REQ Authenticator (includes application authenticator

253 # subkey), encrypted with the application session key

254 # (Section 5.5.1)

255 encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)

256

257 apReq['authenticator'] = noValue

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

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

260

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

262

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

264

265 bindRequest = BindRequest()

266 bindRequest['version'] = 3

267 bindRequest['name'] = user

268 bindRequest['authentication']['sasl']['mechanism'] = 'GSS-SPNEGO'

269 bindRequest['authentication']['sasl']['credentials'] = blob.getData()

270

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

272

273 if response['bindResponse']['resultCode'] != ResultCode('success'): 273 ↛ 274line 273 didn't jump to line 274, because the condition on line 273 was never true

274 raise LDAPSessionError(

275 errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(),

276 response['bindResponse']['diagnosticMessage'])

277 )

278

279 return True

280

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

282 """

283 logins into the target system

284

285 :param string user: username

286 :param string password: password for the user

287 :param string domain: domain where the account is valid for

288 :param string lmhash: LMHASH used to authenticate using hashes (password is not used)

289 :param string nthash: NTHASH used to authenticate using hashes (password is not used)

290 :param string authenticationChoice: type of authentication protocol to use (default NTLM)

291

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

293 """

294 bindRequest = BindRequest()

295 bindRequest['version'] = 3

296

297 if authenticationChoice == 'simple': 297 ↛ 298line 297 didn't jump to line 298, because the condition on line 297 was never true

298 if '.' in domain:

299 bindRequest['name'] = user + '@' + domain

300 elif domain:

301 bindRequest['name'] = domain + '\\' + user

302 else:

303 bindRequest['name'] = user

304 bindRequest['authentication']['simple'] = password

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

306 elif authenticationChoice == 'sicilyPackageDiscovery':

307 bindRequest['name'] = user

308 bindRequest['authentication']['sicilyPackageDiscovery'] = ''

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

310 elif authenticationChoice == 'sicilyNegotiate': 310 ↛ 338line 310 didn't jump to line 338, because the condition on line 310 was never false

311 # Deal with NTLM Authentication

312 if lmhash != '' or nthash != '':

313 if len(lmhash) % 2: 313 ↛ 314line 313 didn't jump to line 314, because the condition on line 313 was never true

314 lmhash = '0' + lmhash

315 if len(nthash) % 2: 315 ↛ 316line 315 didn't jump to line 316, because the condition on line 315 was never true

316 nthash = '0' + nthash

317 try: # just in case they were converted already

318 lmhash = unhexlify(lmhash)

319 nthash = unhexlify(nthash)

320 except TypeError:

321 pass

322

323 bindRequest['name'] = user

324

325 # NTLM Negotiate

326 negotiate = getNTLMSSPType1('', domain)

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

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

329

330 # NTLM Challenge

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

332

333 # NTLM Auth

334 type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash)

335 bindRequest['authentication']['sicilyResponse'] = type3.getData()

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

337 else:

338 raise LDAPSessionError(errorString="Unknown authenticationChoice: '%s'" % authenticationChoice)

339

340 if response['bindResponse']['resultCode'] != ResultCode('success'): 340 ↛ 341line 340 didn't jump to line 341, because the condition on line 340 was never true

341 raise LDAPSessionError(

342 errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(),

343 response['bindResponse']['diagnosticMessage'])

344 )

345

346 return True

347

348 def search(self, searchBase=None, scope=None, derefAliases=None, sizeLimit=0, timeLimit=0, typesOnly=False,

349 searchFilter='(objectClass=*)', attributes=None, searchControls=None, perRecordCallback=None):

350 if searchBase is None: 350 ↛ 352line 350 didn't jump to line 352, because the condition on line 350 was never false

351 searchBase = self._baseDN

352 if scope is None: 352 ↛ 354line 352 didn't jump to line 354, because the condition on line 352 was never false

353 scope = Scope('wholeSubtree')

354 if derefAliases is None: 354 ↛ 356line 354 didn't jump to line 356, because the condition on line 354 was never false

355 derefAliases = DerefAliases('neverDerefAliases')

356 if attributes is None: 356 ↛ 357line 356 didn't jump to line 357, because the condition on line 356 was never true

357 attributes = []

358

359 searchRequest = SearchRequest()

360 searchRequest['baseObject'] = searchBase

361 searchRequest['scope'] = scope

362 searchRequest['derefAliases'] = derefAliases

363 searchRequest['sizeLimit'] = sizeLimit

364 searchRequest['timeLimit'] = timeLimit

365 searchRequest['typesOnly'] = typesOnly

366 searchRequest['filter'] = self._parseFilter(searchFilter)

367 searchRequest['attributes'].setComponents(*attributes)

368

369 done = False

370 answers = []

371 # We keep asking records until we get a SearchResultDone packet and all controls are handled

372 while not done:

373 response = self.sendReceive(searchRequest, searchControls)

374 for message in response:

375 searchResult = message['protocolOp'].getComponent()

376 if searchResult.isSameTypeWith(SearchResultDone()):

377 if searchResult['resultCode'] == ResultCode('success'): 377 ↛ 380line 377 didn't jump to line 380, because the condition on line 377 was never false

378 done = self._handleControls(searchControls, message['controls'])

379 else:

380 raise LDAPSearchError(

381 error=int(searchResult['resultCode']),

382 errorString='Error in searchRequest -> %s: %s' % (searchResult['resultCode'].prettyPrint(),

383 searchResult['diagnosticMessage']),

384 answers=answers

385 )

386 else:

387 if perRecordCallback is None: 387 ↛ 390line 387 didn't jump to line 390, because the condition on line 387 was never false

388 answers.append(searchResult)

389 else:

390 perRecordCallback(searchResult)

391

392 return answers

393

394 def _handleControls(self, requestControls, responseControls):

395 done = True

396 if requestControls is not None: 396 ↛ 397line 396 didn't jump to line 397, because the condition on line 396 was never true

397 for requestControl in requestControls:

398 if responseControls is not None:

399 for responseControl in responseControls:

400 if str(requestControl['controlType']) == CONTROL_PAGEDRESULTS:

401 if str(responseControl['controlType']) == CONTROL_PAGEDRESULTS:

402 if hasattr(responseControl, 'getCookie') is not True:

403 responseControl = decoder.decode(encoder.encode(responseControl),

404 asn1Spec=KNOWN_CONTROLS[CONTROL_PAGEDRESULTS]())[0]

405 if responseControl.getCookie():

406 done = False

407 requestControl.setCookie(responseControl.getCookie())

408 break

409 else:

410 # handle different controls here

411 pass

412 return done

413

414 def close(self):

415 if self._socket is not None:

416 self._socket.close()

417

418 def send(self, request, controls=None):

419 message = LDAPMessage()

420 message['messageID'] = random.randrange(1, 2147483647)

421 message['protocolOp'].setComponentByType(request.getTagSet(), request)

422 if controls is not None: 422 ↛ 423line 422 didn't jump to line 423, because the condition on line 422 was never true

423 message['controls'].setComponents(*controls)

424

425 data = encoder.encode(message)

426

427 return self._socket.sendall(data)

428

429 def recv(self):

430 REQUEST_SIZE = 8192

431 data = b''

432 done = False

433 while not done:

434 recvData = self._socket.recv(REQUEST_SIZE)

435 if len(recvData) < REQUEST_SIZE: 435 ↛ 437line 435 didn't jump to line 437, because the condition on line 435 was never false

436 done = True

437 data += recvData

438

439 response = []

440 while len(data) > 0:

441 try:

442 message, remaining = decoder.decode(data, asn1Spec=LDAPMessage())

443 except SubstrateUnderrunError:

444 # We need more data

445 remaining = data + self._socket.recv(REQUEST_SIZE)

446 else:

447 if message['messageID'] == 0: # unsolicited notification 447 ↛ 448line 447 didn't jump to line 448, because the condition on line 447 was never true

448 name = message['protocolOp']['extendedResp']['responseName'] or message['responseName']

449 notification = KNOWN_NOTIFICATIONS.get(name, "Unsolicited Notification '%s'" % name)

450 if name == NOTIFICATION_DISCONNECT: # Server has disconnected

451 self.close()

452 raise LDAPSessionError(

453 error=int(message['protocolOp']['extendedResp']['resultCode']),

454 errorString='%s -> %s: %s' % (notification,

455 message['protocolOp']['extendedResp']['resultCode'].prettyPrint(),

456 message['protocolOp']['extendedResp']['diagnosticMessage'])

457 )

458 response.append(message)

459 data = remaining

460

461 return response

462

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

464 self.send(request, controls)

465 return self.recv()

466

467 def _parseFilter(self, filterStr):

468 try:

469 filterStr = filterStr.decode()

470 except AttributeError:

471 pass

472 filterList = list(reversed(filterStr))

473 searchFilter = self._consumeCompositeFilter(filterList)

474 if filterList: # we have not consumed the whole filter string 474 ↛ 475line 474 didn't jump to line 475, because the condition on line 474 was never true

475 raise LDAPFilterSyntaxError("unexpected token: '%s'" % filterList[-1])

476 return searchFilter

477

478 def _consumeCompositeFilter(self, filterList):

479 try:

480 c = filterList.pop()

481 except IndexError:

482 raise LDAPFilterSyntaxError('EOL while parsing search filter')

483 if c != '(': # filter must start with a '(' 483 ↛ 484line 483 didn't jump to line 484, because the condition on line 483 was never true

484 filterList.append(c)

485 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)

486

487 try:

488 operator = filterList.pop()

489 except IndexError:

490 raise LDAPFilterSyntaxError('EOL while parsing search filter')

491 if operator not in ['!', '&', '|']: # must be simple filter in this case 491 ↛ 495line 491 didn't jump to line 495, because the condition on line 491 was never false

492 filterList.extend([operator, c])

493 return self._consumeSimpleFilter(filterList)

494

495 filters = []

496 while True:

497 try:

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

499 except LDAPFilterSyntaxError:

500 break

501

502 try:

503 c = filterList.pop()

504 except IndexError:

505 raise LDAPFilterSyntaxError('EOL while parsing search filter')

506 if c != ')': # filter must end with a ')'

507 filterList.append(c)

508 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)

509

510 return self._compileCompositeFilter(operator, filters)

511

512 def _consumeSimpleFilter(self, filterList):

513 try:

514 c = filterList.pop()

515 except IndexError:

516 raise LDAPFilterSyntaxError('EOL while parsing search filter')

517 if c != '(': # filter must start with a '(' 517 ↛ 518line 517 didn't jump to line 518, because the condition on line 517 was never true

518 filterList.append(c)

519 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c)

520

521 filter = []

522 while True:

523 try:

524 c = filterList.pop()

525 except IndexError:

526 raise LDAPFilterSyntaxError('EOL while parsing search filter')

527 if c == ')': # we pop till we find a ')'

528 break

529 elif c == '(': # should be no unencoded parenthesis 529 ↛ 530line 529 didn't jump to line 530, because the condition on line 529 was never true

530 filterList.append(c)

531 raise LDAPFilterSyntaxError("unexpected token: '('")

532 else:

533 filter.append(c)

534

535 filterStr = ''.join(filter)

536 try:

537 # https://tools.ietf.org/search/rfc4515#section-3

538 attribute, operator, value = RE_OPERATOR.split(filterStr, 1)

539 except ValueError:

540 raise LDAPFilterInvalidException("invalid filter: '(%s)'" % filterStr)

541

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

543

544 @staticmethod

545 def _compileCompositeFilter(operator, filters):

546 searchFilter = Filter()

547 if operator == '!':

548 if len(filters) != 1:

549 raise LDAPFilterInvalidException("'not' filter must have exactly one element")

550 searchFilter['not'].setComponents(*filters)

551 elif operator == '&':

552 if len(filters) == 0:

553 raise LDAPFilterInvalidException("'and' filter must have at least one element")

554 searchFilter['and'].setComponents(*filters)

555 elif operator == '|':

556 if len(filters) == 0:

557 raise LDAPFilterInvalidException("'or' filter must have at least one element")

558 searchFilter['or'].setComponents(*filters)

559

560 return searchFilter

561

562 @staticmethod

563 def _compileSimpleFilter(attribute, operator, value):

564 searchFilter = Filter()

565 if operator == ':=': # extensibleMatch 565 ↛ 566line 565 didn't jump to line 566, because the condition on line 565 was never true

566 match = RE_EX_ATTRIBUTE_1.match(attribute) or RE_EX_ATTRIBUTE_2.match(attribute)

567 if not match:

568 raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute)

569 attribute, dn, matchingRule = match.groups()

570 if attribute:

571 searchFilter['extensibleMatch']['type'] = attribute

572 if dn:

573 searchFilter['extensibleMatch']['dnAttributes'] = bool(dn)

574 if matchingRule:

575 searchFilter['extensibleMatch']['matchingRule'] = matchingRule

576 searchFilter['extensibleMatch']['matchValue'] = value

577 else:

578 if not RE_ATTRIBUTE.match(attribute): 578 ↛ 579line 578 didn't jump to line 579, because the condition on line 578 was never true

579 raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute)

580 if value == '*' and operator == '=': # present

581 searchFilter['present'] = attribute

582 elif '*' in value and operator == '=': # substring 582 ↛ 583line 582 didn't jump to line 583, because the condition on line 582 was never true

583 assertions = value.split('*')

584 choice = searchFilter['substrings']['substrings'].getComponentType()

585 substrings = []

586 if assertions[0]:

587 substrings.append(choice.clone().setComponentByName('initial', assertions[0]))

588 for assertion in assertions[1:-1]:

589 substrings.append(choice.clone().setComponentByName('any', assertion))

590 if assertions[-1]:

591 substrings.append(choice.clone().setComponentByName('final', assertions[-1]))

592 searchFilter['substrings']['type'] = attribute

593 searchFilter['substrings']['substrings'].setComponents(*substrings)

594 elif '*' not in value: # simple 594 ↛ 604line 594 didn't jump to line 604, because the condition on line 594 was never false

595 if operator == '=': 595 ↛ 597line 595 didn't jump to line 597, because the condition on line 595 was never false

596 searchFilter['equalityMatch'].setComponents(attribute, value)

597 elif operator == '~=':

598 searchFilter['approxMatch'].setComponents(attribute, value)

599 elif operator == '>=':

600 searchFilter['greaterOrEqual'].setComponents(attribute, value)

601 elif operator == '<=':

602 searchFilter['lessOrEqual'].setComponents(attribute, value)

603 else:

604 raise LDAPFilterInvalidException("invalid filter '(%s%s%s)'" % (attribute, operator, value))

605

606 return searchFilter

607

608

609 class LDAPFilterSyntaxError(SyntaxError):

610 pass

611

612

613 class LDAPFilterInvalidException(Exception):

614 pass

615

616

617 class LDAPSessionError(Exception):

618 """

619 This is the exception every client should catch

620 """

621

622 def __init__(self, error=0, packet=0, errorString=''):

623 Exception.__init__(self)

624 self.error = error

625 self.packet = packet

626 self.errorString = errorString

627

628 def getErrorCode(self):

629 return self.error

630

631 def getErrorPacket(self):

632 return self.packet

633

634 def getErrorString(self):

635 return self.errorString

636

637 def __str__(self):

638 return self.errorString

639

640

641 class LDAPSearchError(LDAPSessionError):

642 def __init__(self, error=0, packet=0, errorString='', answers=None):

643 LDAPSessionError.__init__(self, error, packet, errorString)

644 if answers is None:

645 answers = []

646 self.answers = answers

647

648 def getAnswers(self):

649 return self.answers