2025-12-25 upload

This commit is contained in:
“shengyudong”
2025-12-25 11:16:59 +08:00
commit 322ac74336
2241 changed files with 639966 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
"""
"""
# Created on 2015.08.19
#
# Author: Giovanni Cannata
#
# Copyright 2015 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from pyasn1 import __version__ as pyasn1_version
from pyasn1.codec.ber import decoder # for usage in other modules
from pyasn1.codec.ber.encoder import Encoder # for monkeypatching of boolean value
from ..core.results import RESULT_CODES
from ..utils.conv import to_unicode
from ..protocol.convert import referrals_to_list
CLASSES = {(False, False): 0, # Universal
(False, True): 1, # Application
(True, False): 2, # Context
(True, True): 3} # Private
# Monkeypatching of pyasn1 for encoding Boolean with the value 0xFF for TRUE
# THIS IS NOT PART OF THE FAST BER DECODER
if pyasn1_version == 'xxx0.2.3':
from pyasn1.codec.ber.encoder import tagMap, BooleanEncoder, encode
from pyasn1.type.univ import Boolean
from pyasn1.compat.octets import ints2octs
class BooleanCEREncoder(BooleanEncoder):
_true = ints2octs((255,))
tagMap[Boolean.tagSet] = BooleanCEREncoder()
else:
from pyasn1.codec.ber.encoder import tagMap, typeMap, AbstractItemEncoder
from pyasn1.type.univ import Boolean
from copy import deepcopy
class LDAPBooleanEncoder(AbstractItemEncoder):
supportIndefLenMode = False
if pyasn1_version <= '0.2.3':
from pyasn1.compat.octets import ints2octs
_true = ints2octs((255,))
_false = ints2octs((0,))
def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
return value and self._true or self._false, 0
elif pyasn1_version <= '0.3.1':
def encodeValue(self, encodeFun, value, defMode, maxChunkSize):
return value and (255,) or (0,), False, False
elif pyasn1_version <= '0.3.4':
def encodeValue(self, encodeFun, value, defMode, maxChunkSize, ifNotEmpty=False):
return value and (255,) or (0,), False, False
elif pyasn1_version <= '0.3.7':
def encodeValue(self, value, encodeFun, **options):
return value and (255,) or (0,), False, False
else:
def encodeValue(self, value, asn1Spec, encodeFun, **options):
return value and (255,) or (0,), False, False
customTagMap = deepcopy(tagMap)
customTypeMap = deepcopy(typeMap)
customTagMap[Boolean.tagSet] = LDAPBooleanEncoder()
customTypeMap[Boolean.typeId] = LDAPBooleanEncoder()
encode = Encoder(customTagMap, customTypeMap)
# end of monkey patching
# a fast BER decoder for LDAP responses only
def compute_ber_size(data):
"""
Compute size according to BER definite length rules
Returns size of value and value offset
"""
if data[1] <= 127: # BER definite length - short form. Highest bit of byte 1 is 0, message length is in the last 7 bits - Value can be up to 127 bytes long
return data[1], 2
else: # BER definite length - long form. Highest bit of byte 1 is 1, last 7 bits counts the number of following octets containing the value length
bytes_length = data[1] - 128
value_length = 0
cont = bytes_length
for byte in data[2: 2 + bytes_length]:
cont -= 1
value_length += byte * (256 ** cont)
return value_length, bytes_length + 2
def decode_message_fast(message):
ber_len, ber_value_offset = compute_ber_size(get_bytes(message[:10])) # get start of sequence, at maximum 3 bytes for length
decoded = decode_sequence(message, ber_value_offset, ber_len + ber_value_offset, LDAP_MESSAGE_CONTEXT)
return {
'messageID': decoded[0][3],
'protocolOp': decoded[1][2],
'payload': decoded[1][3],
'controls': decoded[2][3] if len(decoded) == 3 else None
}
def decode_sequence(message, start, stop, context_decoders=None):
decoded = []
while start < stop:
octet = get_byte(message[start])
ber_class = CLASSES[(bool(octet & 0b10000000), bool(octet & 0b01000000))]
ber_constructed = bool(octet & 0b00100000)
ber_type = octet & 0b00011111
ber_decoder = DECODERS[(ber_class, octet & 0b00011111)] if ber_class < 2 else None
ber_len, ber_value_offset = compute_ber_size(get_bytes(message[start: start + 10]))
start += ber_value_offset
if ber_decoder:
value = ber_decoder(message, start, start + ber_len, context_decoders) # call value decode function
else:
# try:
value = context_decoders[ber_type](message, start, start + ber_len) # call value decode function for context class
# except KeyError:
# if ber_type == 3: # Referral in result
# value = decode_sequence(message, start, start + ber_len)
# else:
# raise # re-raise, should never happen
decoded.append((ber_class, ber_constructed, ber_type, value))
start += ber_len
return decoded
def decode_integer(message, start, stop, context_decoders=None):
first = message[start]
value = -1 if get_byte(first) & 0x80 else 0
for octet in message[start: stop]:
value = value << 8 | get_byte(octet)
return value
def decode_octet_string(message, start, stop, context_decoders=None):
return message[start: stop]
def decode_boolean(message, start, stop, context_decoders=None):
return False if message[start: stop] == 0 else True
def decode_bind_response(message, start, stop, context_decoders=None):
return decode_sequence(message, start, stop, BIND_RESPONSE_CONTEXT)
def decode_extended_response(message, start, stop, context_decoders=None):
return decode_sequence(message, start, stop, EXTENDED_RESPONSE_CONTEXT)
def decode_intermediate_response(message, start, stop, context_decoders=None):
return decode_sequence(message, start, stop, INTERMEDIATE_RESPONSE_CONTEXT)
def decode_controls(message, start, stop, context_decoders=None):
return decode_sequence(message, start, stop, CONTROLS_CONTEXT)
def ldap_result_to_dict_fast(response):
response_dict = dict()
response_dict['result'] = int(response[0][3]) # resultCode
response_dict['description'] = RESULT_CODES[response_dict['result']]
response_dict['dn'] = to_unicode(response[1][3], from_server=True) # matchedDN
response_dict['message'] = to_unicode(response[2][3], from_server=True) # diagnosticMessage
if len(response) == 4:
response_dict['referrals'] = referrals_to_list([to_unicode(referral[3], from_server=True) for referral in response[3][3]]) # referrals
else:
response_dict['referrals'] = None
return response_dict
######
if str is not bytes: # Python 3
def get_byte(x):
return x
def get_bytes(x):
return x
else: # Python 2
def get_byte(x):
return ord(x)
def get_bytes(x):
return bytearray(x)
DECODERS = {
# Universal
(0, 1): decode_boolean, # Boolean
(0, 2): decode_integer, # Integer
(0, 4): decode_octet_string, # Octet String
(0, 10): decode_integer, # Enumerated
(0, 16): decode_sequence, # Sequence
(0, 17): decode_sequence, # Set
# Application
(1, 1): decode_bind_response, # Bind response
(1, 4): decode_sequence, # Search result entry
(1, 5): decode_sequence, # Search result done
(1, 7): decode_sequence, # Modify response
(1, 9): decode_sequence, # Add response
(1, 11): decode_sequence, # Delete response
(1, 13): decode_sequence, # ModifyDN response
(1, 15): decode_sequence, # Compare response
(1, 19): decode_sequence, # Search result reference
(1, 24): decode_extended_response, # Extended response
(1, 25): decode_intermediate_response, # intermediate response
(2, 3): decode_octet_string #
}
BIND_RESPONSE_CONTEXT = {
7: decode_octet_string # SaslCredentials
}
EXTENDED_RESPONSE_CONTEXT = {
10: decode_octet_string, # ResponseName
11: decode_octet_string # Response Value
}
INTERMEDIATE_RESPONSE_CONTEXT = {
0: decode_octet_string, # IntermediateResponseName
1: decode_octet_string # IntermediateResponseValue
}
LDAP_MESSAGE_CONTEXT = {
0: decode_controls, # Controls
3: decode_sequence # Referral
}
CONTROLS_CONTEXT = {
0: decode_sequence # Control
}

View File

@@ -0,0 +1,199 @@
"""
"""
# Created on 2014.08.23
#
# Author: Giovanni Cannata
#
# Copyright 2014 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
try:
from collections.abc import MutableMapping, Mapping
except ImportError:
from collections import MutableMapping, Mapping
from .. import SEQUENCE_TYPES
class CaseInsensitiveDict(MutableMapping):
def __init__(self, other=None, **kwargs):
self._store = dict() # store use the original key
self._case_insensitive_keymap = dict() # is a mapping ci_key -> key
if other or kwargs:
if other is None:
other = dict()
self.update(other, **kwargs)
def __contains__(self, item):
try:
self.__getitem__(item)
return True
except KeyError:
return False
@staticmethod
def _ci_key(key):
return key.strip().lower() if hasattr(key, 'lower') else key
def __delitem__(self, key):
ci_key = self._ci_key(key)
del self._store[self._case_insensitive_keymap[ci_key]]
del self._case_insensitive_keymap[ci_key]
def __setitem__(self, key, item):
ci_key = self._ci_key(key)
if ci_key in self._case_insensitive_keymap: # updates existing value
self._store[self._case_insensitive_keymap[ci_key]] = item
else: # new key
self._store[key] = item
self._case_insensitive_keymap[ci_key] = key
def __getitem__(self, key):
return self._store[self._case_insensitive_keymap[self._ci_key(key)]]
def __iter__(self):
return self._store.__iter__()
def __len__(self): # if len is 0 then the cidict appears as False in IF statement
return len(self._store)
def __repr__(self):
return repr(self._store)
def __str__(self):
return str(self._store)
def keys(self):
return self._store.keys()
def values(self):
return self._store.values()
def items(self):
return self._store.items()
def __eq__(self, other):
if not isinstance(other, (Mapping, dict)):
return NotImplemented
if isinstance(other, CaseInsensitiveDict):
if len(self.items()) != len(other.items()):
return False
else:
for key, value in self.items():
if not (key in other and other[key] == value):
return False
return True
return self == CaseInsensitiveDict(other)
def copy(self):
return CaseInsensitiveDict(self._store)
class CaseInsensitiveWithAliasDict(CaseInsensitiveDict):
def __init__(self, other=None, **kwargs):
self._aliases = dict()
self._alias_keymap = dict() # is a mapping key -> [alias1, alias2, ...]
CaseInsensitiveDict.__init__(self, other, **kwargs)
def aliases(self):
return self._aliases.keys()
def __setitem__(self, key, value):
if isinstance(key, SEQUENCE_TYPES):
ci_key = self._ci_key(key[0])
if ci_key not in self._aliases:
CaseInsensitiveDict.__setitem__(self, key[0], value)
self.set_alias(ci_key, key[1:])
else:
raise KeyError('\'' + str(key[0] + ' already used as alias'))
else:
ci_key = self._ci_key(key)
if ci_key not in self._aliases:
CaseInsensitiveDict.__setitem__(self, key, value)
else:
self[self._aliases[ci_key]] = value
def __delitem__(self, key):
ci_key = self._ci_key(key)
try:
CaseInsensitiveDict.__delitem__(self, ci_key)
if ci_key in self._alias_keymap:
for alias in self._alias_keymap[ci_key][:]: # removes aliases, uses a copy of _alias_keymap because iterator gets confused when aliases are removed from _alias_keymap
self.remove_alias(alias)
return
except KeyError: # try to remove alias
if ci_key in self._aliases:
self.remove_alias(ci_key)
def set_alias(self, key, alias, ignore_duplicates=False):
if not isinstance(alias, SEQUENCE_TYPES):
alias = [alias]
for alias_to_add in alias:
ci_key = self._ci_key(key)
if ci_key in self._case_insensitive_keymap:
ci_alias = self._ci_key(alias_to_add)
if ci_alias not in self._case_insensitive_keymap: # checks if alias is used a key
if ci_alias not in self._aliases: # checks if alias is used as another alias
self._aliases[ci_alias] = ci_key
if ci_key in self._alias_keymap: # extends alias keymap
self._alias_keymap[ci_key].append(self._ci_key(ci_alias))
else:
self._alias_keymap[ci_key] = list()
self._alias_keymap[ci_key].append(self._ci_key(ci_alias))
else:
if ci_key in self._alias_keymap and ci_alias in self._alias_keymap[ci_key]: # passes if alias is already defined to the same key
pass
elif not ignore_duplicates:
raise KeyError('\'' + str(alias_to_add) + '\' already used as alias')
else:
if ci_key == self._ci_key(self._case_insensitive_keymap[ci_alias]): # passes if alias is already defined to the same key
pass
elif not ignore_duplicates:
raise KeyError('\'' + str(alias_to_add) + '\' already used as key')
else:
for keymap in self._alias_keymap:
if ci_key in self._alias_keymap[keymap]: # kye is already aliased
self.set_alias(keymap, alias + [ci_key], ignore_duplicates=ignore_duplicates)
break
else:
raise KeyError('\'' + str(ci_key) + '\' is not an existing alias or key')
def remove_alias(self, alias):
if not isinstance(alias, SEQUENCE_TYPES):
alias = [alias]
for alias_to_remove in alias:
ci_alias = self._ci_key(alias_to_remove)
self._alias_keymap[self._aliases[ci_alias]].remove(ci_alias)
if not self._alias_keymap[self._aliases[ci_alias]]: # remove keymap if empty
del self._alias_keymap[self._aliases[ci_alias]]
del self._aliases[ci_alias]
def __getitem__(self, key):
try:
return CaseInsensitiveDict.__getitem__(self, key)
except KeyError:
return CaseInsensitiveDict.__getitem__(self, self._aliases[self._ci_key(key)])
def copy(self):
new = CaseInsensitiveWithAliasDict(self._store)
new._aliases = self._aliases.copy()
new._alias_keymap = self._alias_keymap
return new

View File

@@ -0,0 +1,299 @@
"""
"""
# Created on 2016.08.31
#
# Author: Giovanni Cannata
#
# Copyright 2013 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from sys import stdin, getdefaultencoding
from .. import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, NO_ATTRIBUTES, SEQUENCE_TYPES
from ..core.exceptions import LDAPConfigurationParameterError
# checks
_CLASSES_EXCLUDED_FROM_CHECK = ['subschema']
_ATTRIBUTES_EXCLUDED_FROM_CHECK = [ALL_ATTRIBUTES,
ALL_OPERATIONAL_ATTRIBUTES,
NO_ATTRIBUTES,
'ldapSyntaxes',
'matchingRules',
'matchingRuleUse',
'dITContentRules',
'dITStructureRules',
'nameForms',
'altServer',
'namingContexts',
'supportedControl',
'supportedExtension',
'supportedFeatures',
'supportedCapabilities',
'supportedLdapVersion',
'supportedSASLMechanisms',
'vendorName',
'vendorVersion',
'subschemaSubentry',
'ACL']
_UTF8_ENCODED_SYNTAXES = ['1.2.840.113556.1.4.904', # DN String [MICROSOFT]
'1.2.840.113556.1.4.1362', # String (Case) [MICROSOFT]
'1.3.6.1.4.1.1466.115.121.1.12', # DN String [RFC4517]
'1.3.6.1.4.1.1466.115.121.1.15', # Directory String [RFC4517]
'1.3.6.1.4.1.1466.115.121.1.41', # Postal Address) [RFC4517]
'1.3.6.1.4.1.1466.115.121.1.58', # Substring Assertion [RFC4517]
'2.16.840.1.113719.1.1.5.1.6', # Case Ignore List [NOVELL]
'2.16.840.1.113719.1.1.5.1.14', # Tagged String [NOVELL]
'2.16.840.1.113719.1.1.5.1.15', # Tagged Name and String [NOVELL]
'2.16.840.1.113719.1.1.5.1.23', # Tagged Name [NOVELL]
'2.16.840.1.113719.1.1.5.1.25'] # Typed Name [NOVELL]
_UTF8_ENCODED_TYPES = []
_ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF = ['msds-memberOfTransitive', 'msds-memberTransitive', 'entryDN']
_IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF = ['instanceType', 'nTSecurityDescriptor', 'objectCategory']
_CASE_INSENSITIVE_ATTRIBUTE_NAMES = True
_CASE_INSENSITIVE_SCHEMA_NAMES = True
# abstraction layer
_ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX = 'OA_'
# communication
_POOLING_LOOP_TIMEOUT = 10 # number of seconds to wait before restarting a cycle to find an active server in the pool
_RESPONSE_SLEEPTIME = 0.05 # seconds to wait while waiting for a response in asynchronous strategies
_RESPONSE_WAITING_TIMEOUT = 20 # waiting timeout for receiving a response in asynchronous strategies
_SOCKET_SIZE = 4096 # socket byte size
_CHECK_AVAILABILITY_TIMEOUT = 2.5 # default timeout for socket connect when checking availability
_RESET_AVAILABILITY_TIMEOUT = 5 # default timeout for resetting the availability status when checking candidate addresses
_RESTARTABLE_SLEEPTIME = 2 # time to wait in a restartable strategy before retrying the request
_RESTARTABLE_TRIES = 30 # number of times to retry in a restartable strategy before giving up. Set to True for unlimited retries
_REUSABLE_THREADED_POOL_SIZE = 5
_REUSABLE_THREADED_LIFETIME = 3600 # 1 hour
_DEFAULT_THREADED_POOL_NAME = 'REUSABLE_DEFAULT_POOL'
_ADDRESS_INFO_REFRESH_TIME = 300 # seconds to wait before refreshing address info from dns
_ADDITIONAL_SERVER_ENCODINGS = ['latin-1', 'koi8-r'] # some broken LDAP implementation may have different encoding than those expected by RFCs
_ADDITIONAL_CLIENT_ENCODINGS = ['utf-8']
_IGNORE_MALFORMED_SCHEMA = False # some flaky LDAP servers returns malformed schema. If True no expection is raised and schema is thrown away
_DEFAULT_SERVER_ENCODING = 'utf-8' # should always be utf-8
_LDIF_LINE_LENGTH = 78 # as stated in RFC 2849
if stdin and hasattr(stdin, 'encoding') and stdin.encoding:
_DEFAULT_CLIENT_ENCODING = stdin.encoding
elif getdefaultencoding():
_DEFAULT_CLIENT_ENCODING = getdefaultencoding()
else:
_DEFAULT_CLIENT_ENCODING = 'utf-8'
PARAMETERS = ['CASE_INSENSITIVE_ATTRIBUTE_NAMES',
'CASE_INSENSITIVE_SCHEMA_NAMES',
'ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX',
'POOLING_LOOP_TIMEOUT',
'RESPONSE_SLEEPTIME',
'RESPONSE_WAITING_TIMEOUT',
'SOCKET_SIZE',
'CHECK_AVAILABILITY_TIMEOUT',
'RESTARTABLE_SLEEPTIME',
'RESTARTABLE_TRIES',
'REUSABLE_THREADED_POOL_SIZE',
'REUSABLE_THREADED_LIFETIME',
'DEFAULT_THREADED_POOL_NAME',
'ADDRESS_INFO_REFRESH_TIME',
'RESET_AVAILABILITY_TIMEOUT',
'DEFAULT_CLIENT_ENCODING',
'DEFAULT_SERVER_ENCODING',
'CLASSES_EXCLUDED_FROM_CHECK',
'ATTRIBUTES_EXCLUDED_FROM_CHECK',
'UTF8_ENCODED_SYNTAXES',
'UTF8_ENCODED_TYPES',
'ADDITIONAL_SERVER_ENCODINGS',
'ADDITIONAL_CLIENT_ENCODINGS',
'IGNORE_MALFORMED_SCHEMA',
'ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF',
'IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF',
'LDIF_LINE_LENGTH'
]
def get_config_parameter(parameter):
if parameter == 'CASE_INSENSITIVE_ATTRIBUTE_NAMES': # Boolean
return _CASE_INSENSITIVE_ATTRIBUTE_NAMES
elif parameter == 'CASE_INSENSITIVE_SCHEMA_NAMES': # Boolean
return _CASE_INSENSITIVE_SCHEMA_NAMES
elif parameter == 'ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX': # String
return _ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX
elif parameter == 'POOLING_LOOP_TIMEOUT': # Integer
return _POOLING_LOOP_TIMEOUT
elif parameter == 'RESPONSE_SLEEPTIME': # Integer
return _RESPONSE_SLEEPTIME
elif parameter == 'RESPONSE_WAITING_TIMEOUT': # Integer
return _RESPONSE_WAITING_TIMEOUT
elif parameter == 'SOCKET_SIZE': # Integer
return _SOCKET_SIZE
elif parameter == 'CHECK_AVAILABILITY_TIMEOUT': # Integer
return _CHECK_AVAILABILITY_TIMEOUT
elif parameter == 'RESTARTABLE_SLEEPTIME': # Integer
return _RESTARTABLE_SLEEPTIME
elif parameter == 'RESTARTABLE_TRIES': # Integer
return _RESTARTABLE_TRIES
elif parameter == 'REUSABLE_THREADED_POOL_SIZE': # Integer
return _REUSABLE_THREADED_POOL_SIZE
elif parameter == 'REUSABLE_THREADED_LIFETIME': # Integer
return _REUSABLE_THREADED_LIFETIME
elif parameter == 'DEFAULT_THREADED_POOL_NAME': # String
return _DEFAULT_THREADED_POOL_NAME
elif parameter == 'ADDRESS_INFO_REFRESH_TIME': # Integer
return _ADDRESS_INFO_REFRESH_TIME
elif parameter == 'RESET_AVAILABILITY_TIMEOUT': # Integer
return _RESET_AVAILABILITY_TIMEOUT
elif parameter in ['DEFAULT_CLIENT_ENCODING', 'DEFAULT_ENCODING']: # String - DEFAULT_ENCODING for backward compatibility
return _DEFAULT_CLIENT_ENCODING
elif parameter == 'DEFAULT_SERVER_ENCODING': # String
return _DEFAULT_SERVER_ENCODING
elif parameter == 'CLASSES_EXCLUDED_FROM_CHECK': # Sequence
if isinstance(_CLASSES_EXCLUDED_FROM_CHECK, SEQUENCE_TYPES):
return _CLASSES_EXCLUDED_FROM_CHECK
else:
return [_CLASSES_EXCLUDED_FROM_CHECK]
elif parameter == 'ATTRIBUTES_EXCLUDED_FROM_CHECK': # Sequence
if isinstance(_ATTRIBUTES_EXCLUDED_FROM_CHECK, SEQUENCE_TYPES):
return _ATTRIBUTES_EXCLUDED_FROM_CHECK
else:
return [_ATTRIBUTES_EXCLUDED_FROM_CHECK]
elif parameter == 'UTF8_ENCODED_SYNTAXES': # Sequence
if isinstance(_UTF8_ENCODED_SYNTAXES, SEQUENCE_TYPES):
return _UTF8_ENCODED_SYNTAXES
else:
return [_UTF8_ENCODED_SYNTAXES]
elif parameter == 'UTF8_ENCODED_TYPES': # Sequence
if isinstance(_UTF8_ENCODED_TYPES, SEQUENCE_TYPES):
return _UTF8_ENCODED_TYPES
else:
return [_UTF8_ENCODED_TYPES]
elif parameter in ['ADDITIONAL_SERVER_ENCODINGS', 'ADDITIONAL_ENCODINGS']: # Sequence - ADDITIONAL_ENCODINGS for backward compatibility
if isinstance(_ADDITIONAL_SERVER_ENCODINGS, SEQUENCE_TYPES):
return _ADDITIONAL_SERVER_ENCODINGS
else:
return [_ADDITIONAL_SERVER_ENCODINGS]
elif parameter in ['ADDITIONAL_CLIENT_ENCODINGS']: # Sequence
if isinstance(_ADDITIONAL_CLIENT_ENCODINGS, SEQUENCE_TYPES):
return _ADDITIONAL_CLIENT_ENCODINGS
else:
return [_ADDITIONAL_CLIENT_ENCODINGS]
elif parameter == 'IGNORE_MALFORMED_SCHEMA': # Boolean
return _IGNORE_MALFORMED_SCHEMA
elif parameter == 'ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF': # Sequence
if isinstance(_ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF, SEQUENCE_TYPES):
return _ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF
else:
return [_ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF]
elif parameter == 'IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF': # Sequence
if isinstance(_IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF, SEQUENCE_TYPES):
return _IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF
else:
return [_IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF]
elif parameter == 'LDIF_LINE_LENGTH': # Integer
return _LDIF_LINE_LENGTH
raise LDAPConfigurationParameterError('configuration parameter %s not valid' % parameter)
def set_config_parameter(parameter, value):
if parameter == 'CASE_INSENSITIVE_ATTRIBUTE_NAMES':
global _CASE_INSENSITIVE_ATTRIBUTE_NAMES
_CASE_INSENSITIVE_ATTRIBUTE_NAMES = value
elif parameter == 'CASE_INSENSITIVE_SCHEMA_NAMES':
global _CASE_INSENSITIVE_SCHEMA_NAMES
_CASE_INSENSITIVE_SCHEMA_NAMES = value
elif parameter == 'ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX':
global _ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX
_ABSTRACTION_OPERATIONAL_ATTRIBUTE_PREFIX = value
elif parameter == 'POOLING_LOOP_TIMEOUT':
global _POOLING_LOOP_TIMEOUT
_POOLING_LOOP_TIMEOUT = value
elif parameter == 'RESPONSE_SLEEPTIME':
global _RESPONSE_SLEEPTIME
_RESPONSE_SLEEPTIME = value
elif parameter == 'RESPONSE_WAITING_TIMEOUT':
global _RESPONSE_WAITING_TIMEOUT
_RESPONSE_WAITING_TIMEOUT = value
elif parameter == 'SOCKET_SIZE':
global _SOCKET_SIZE
_SOCKET_SIZE = value
elif parameter == 'CHECK_AVAILABILITY_TIMEOUT':
global _CHECK_AVAILABILITY_TIMEOUT
_CHECK_AVAILABILITY_TIMEOUT = value
elif parameter == 'RESTARTABLE_SLEEPTIME':
global _RESTARTABLE_SLEEPTIME
_RESTARTABLE_SLEEPTIME = value
elif parameter == 'RESTARTABLE_TRIES':
global _RESTARTABLE_TRIES
_RESTARTABLE_TRIES = value
elif parameter == 'REUSABLE_THREADED_POOL_SIZE':
global _REUSABLE_THREADED_POOL_SIZE
_REUSABLE_THREADED_POOL_SIZE = value
elif parameter == 'REUSABLE_THREADED_LIFETIME':
global _REUSABLE_THREADED_LIFETIME
_REUSABLE_THREADED_LIFETIME = value
elif parameter == 'DEFAULT_THREADED_POOL_NAME':
global _DEFAULT_THREADED_POOL_NAME
_DEFAULT_THREADED_POOL_NAME = value
elif parameter == 'ADDRESS_INFO_REFRESH_TIME':
global _ADDRESS_INFO_REFRESH_TIME
_ADDRESS_INFO_REFRESH_TIME = value
elif parameter == 'RESET_AVAILABILITY_TIMEOUT':
global _RESET_AVAILABILITY_TIMEOUT
_RESET_AVAILABILITY_TIMEOUT = value
elif parameter in ['DEFAULT_CLIENT_ENCODING', 'DEFAULT_ENCODING']:
global _DEFAULT_CLIENT_ENCODING
_DEFAULT_CLIENT_ENCODING = value
elif parameter == 'DEFAULT_SERVER_ENCODING':
global _DEFAULT_SERVER_ENCODING
_DEFAULT_SERVER_ENCODING = value
elif parameter == 'CLASSES_EXCLUDED_FROM_CHECK':
global _CLASSES_EXCLUDED_FROM_CHECK
_CLASSES_EXCLUDED_FROM_CHECK = value
elif parameter == 'ATTRIBUTES_EXCLUDED_FROM_CHECK':
global _ATTRIBUTES_EXCLUDED_FROM_CHECK
_ATTRIBUTES_EXCLUDED_FROM_CHECK = value
elif parameter == 'UTF8_ENCODED_SYNTAXES':
global _UTF8_ENCODED_SYNTAXES
_UTF8_ENCODED_SYNTAXES = value
elif parameter == 'UTF8_ENCODED_TYPES':
global _UTF8_ENCODED_TYPES
_UTF8_ENCODED_TYPES = value
elif parameter in ['ADDITIONAL_SERVER_ENCODINGS', 'ADDITIONAL_ENCODINGS']:
global _ADDITIONAL_SERVER_ENCODINGS
_ADDITIONAL_SERVER_ENCODINGS = value if isinstance(value, SEQUENCE_TYPES) else [value]
elif parameter in ['ADDITIONAL_CLIENT_ENCODINGS']:
global _ADDITIONAL_CLIENT_ENCODINGS
_ADDITIONAL_CLIENT_ENCODINGS = value if isinstance(value, SEQUENCE_TYPES) else [value]
elif parameter == 'IGNORE_MALFORMED_SCHEMA':
global _IGNORE_MALFORMED_SCHEMA
_IGNORE_MALFORMED_SCHEMA = value
elif parameter == 'ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF':
global _ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF
_ATTRIBUTES_EXCLUDED_FROM_OBJECT_DEF = value
elif parameter == 'IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF':
global _IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF
_IGNORED_MANDATORY_ATTRIBUTES_IN_OBJECT_DEF = value
elif parameter == 'LDIF_LINE_LENGTH':
global _LDIF_LINE_LENGTH
_LDIF_LINE_LENGTH = value
else:
raise LDAPConfigurationParameterError('unable to set configuration parameter %s' % parameter)

View File

@@ -0,0 +1,272 @@
"""
"""
# Created on 2014.04.26
#
# Author: Giovanni Cannata
#
# Copyright 2014 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from base64 import b64encode, b64decode
import datetime
import re
from .. import SEQUENCE_TYPES, STRING_TYPES, NUMERIC_TYPES, get_config_parameter
from ..utils.ciDict import CaseInsensitiveDict
from ..core.exceptions import LDAPDefinitionError
def to_unicode(obj, encoding=None, from_server=False):
"""Try to convert bytes (and str in python2) to unicode.
Return object unmodified if python3 string, else raise an exception
"""
conf_default_client_encoding = get_config_parameter('DEFAULT_CLIENT_ENCODING')
conf_default_server_encoding = get_config_parameter('DEFAULT_SERVER_ENCODING')
conf_additional_server_encodings = get_config_parameter('ADDITIONAL_SERVER_ENCODINGS')
conf_additional_client_encodings = get_config_parameter('ADDITIONAL_CLIENT_ENCODINGS')
if isinstance(obj, NUMERIC_TYPES):
obj = str(obj)
if isinstance(obj, (bytes, bytearray)):
if from_server: # data from server
if encoding is None:
encoding = conf_default_server_encoding
try:
return obj.decode(encoding)
except UnicodeDecodeError:
for encoding in conf_additional_server_encodings: # AD could have DN not encoded in utf-8 (even if this is not allowed by RFC4510)
try:
return obj.decode(encoding)
except UnicodeDecodeError:
pass
raise UnicodeError("Unable to convert server data to unicode: %r" % obj)
else: # data from client
if encoding is None:
encoding = conf_default_client_encoding
try:
return obj.decode(encoding)
except UnicodeDecodeError:
for encoding in conf_additional_client_encodings: # tries additional encodings
try:
return obj.decode(encoding)
except UnicodeDecodeError:
pass
raise UnicodeError("Unable to convert client data to unicode: %r" % obj)
if isinstance(obj, STRING_TYPES): # python3 strings, python 2 unicode
return obj
raise UnicodeError("Unable to convert type %s to unicode: %r" % (obj.__class__.__name__, obj))
def to_raw(obj, encoding='utf-8'):
"""Tries to convert to raw bytes from unicode"""
if isinstance(obj, NUMERIC_TYPES):
obj = str(obj)
if not (isinstance(obj, bytes)):
if isinstance(obj, SEQUENCE_TYPES):
return [to_raw(element) for element in obj]
elif isinstance(obj, STRING_TYPES):
return obj.encode(encoding)
return obj
def escape_filter_chars(text, encoding=None):
""" Escape chars mentioned in RFC4515. """
if encoding is None:
encoding = get_config_parameter('DEFAULT_ENCODING')
try:
text = to_unicode(text, encoding)
escaped = text.replace('\\', '\\5c')
escaped = escaped.replace('*', '\\2a')
escaped = escaped.replace('(', '\\28')
escaped = escaped.replace(')', '\\29')
escaped = escaped.replace('\x00', '\\00')
except Exception: # probably raw bytes values, return escaped bytes value
escaped = to_unicode(escape_bytes(text))
# escape all octets greater than 0x7F that are not part of a valid UTF-8
# escaped = ''.join(c if c <= ord(b'\x7f') else escape_bytes(to_raw(to_unicode(c, encoding))) for c in escaped)
return escaped
def unescape_filter_chars(text, encoding=None):
""" unescape chars mentioned in RFC4515. """
if encoding is None:
encoding = get_config_parameter('DEFAULT_ENCODING')
unescaped = to_raw(text, encoding)
unescaped = unescaped.replace(b'\\5c', b'\\')
unescaped = unescaped.replace(b'\\5C', b'\\')
unescaped = unescaped.replace(b'\\2a', b'*')
unescaped = unescaped.replace(b'\\2A', b'*')
unescaped = unescaped.replace(b'\\28', b'(')
unescaped = unescaped.replace(b'\\29', b')')
unescaped = unescaped.replace(b'\\00', b'\x00')
return unescaped
def escape_bytes(bytes_value):
""" Convert a byte sequence to a properly escaped for LDAP (format BACKSLASH HEX HEX) string"""
if bytes_value:
if str is not bytes: # Python 3
if isinstance(bytes_value, str):
bytes_value = bytearray(bytes_value, encoding='utf-8')
escaped = '\\'.join([('%02x' % int(b)) for b in bytes_value])
else: # Python 2
if isinstance(bytes_value, unicode):
bytes_value = bytes_value.encode('utf-8')
escaped = '\\'.join([('%02x' % ord(b)) for b in bytes_value])
else:
escaped = ''
return ('\\' + escaped) if escaped else ''
def prepare_for_stream(value):
if str is not bytes: # Python 3
return value
else: # Python 2
return value.decode()
def json_encode_b64(obj):
try:
return dict(encoding='base64', encoded=b64encode(obj))
except Exception as e:
raise LDAPDefinitionError('unable to encode ' + str(obj) + ' - ' + str(e))
# noinspection PyProtectedMember
def check_json_dict(json_dict):
# needed for python 2
for k, v in json_dict.items():
if isinstance(v, dict):
check_json_dict(v)
elif isinstance(v, CaseInsensitiveDict):
check_json_dict(v._store)
elif isinstance(v, SEQUENCE_TYPES):
for i, e in enumerate(v):
if isinstance(e, dict):
check_json_dict(e)
elif isinstance(e, CaseInsensitiveDict):
check_json_dict(e._store)
else:
v[i] = format_json(e)
else:
json_dict[k] = format_json(v)
def json_hook(obj):
if hasattr(obj, 'keys') and len(list(obj.keys())) == 2 and 'encoding' in obj.keys() and 'encoded' in obj.keys():
return b64decode(obj['encoded'])
return obj
# noinspection PyProtectedMember
def format_json(obj, iso_format=False):
if isinstance(obj, CaseInsensitiveDict):
return obj._store
if isinstance(obj, datetime.datetime):
return str(obj)
if isinstance(obj, int):
return obj
if isinstance(obj, datetime.timedelta):
if iso_format:
return obj.isoformat()
return str(obj)
if str is bytes: # Python 2
if isinstance(obj, long): # long exists only in python2
return obj
try:
if str is not bytes: # Python 3
if isinstance(obj, bytes):
# return check_escape(str(obj, 'utf-8', errors='strict'))
return str(obj, 'utf-8', errors='strict')
raise LDAPDefinitionError('unable to serialize ' + str(obj))
else: # Python 2
if isinstance(obj, unicode):
return obj
else:
# return unicode(check_escape(obj))
return unicode(obj)
except (TypeError, UnicodeDecodeError):
pass
try:
return json_encode_b64(bytes(obj))
except Exception:
pass
raise LDAPDefinitionError('unable to serialize ' + str(obj))
def is_filter_escaped(text):
if not type(text) == ((str is not bytes) and str or unicode): # requires str for Python 3 and unicode for Python 2
raise ValueError('unicode input expected')
return all(c not in text for c in '()*\0') and not re.search('\\\\([^0-9a-fA-F]|(.[^0-9a-fA-F]))', text)
def ldap_escape_to_bytes(text):
bytesequence = bytearray()
i = 0
try:
if isinstance(text, STRING_TYPES):
while i < len(text):
if text[i] == '\\':
if len(text) > i + 2:
try:
bytesequence.append(int(text[i+1:i+3], 16))
i += 3
continue
except ValueError:
pass
bytesequence.append(92) # "\" ASCII code
else:
raw = to_raw(text[i])
for c in raw:
bytesequence.append(c)
i += 1
elif isinstance(text, (bytes, bytearray)):
while i < len(text):
if text[i] == 92: # "\" ASCII code
if len(text) > i + 2:
try:
bytesequence.append(int(text[i + 1:i + 3], 16))
i += 3
continue
except ValueError:
pass
bytesequence.append(92) # "\" ASCII code
else:
bytesequence.append(text[i])
i += 1
except Exception:
raise LDAPDefinitionError('badly formatted LDAP byte escaped sequence')
return bytes(bytesequence)

View File

@@ -0,0 +1,405 @@
"""
"""
# Created on 2014.09.08
#
# Author: Giovanni Cannata
#
# Copyright 2014 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from string import hexdigits, ascii_letters, digits
from .. import SEQUENCE_TYPES
from ..core.exceptions import LDAPInvalidDnError
STATE_ANY = 0
STATE_ESCAPE = 1
STATE_ESCAPE_HEX = 2
def _add_ava(ava, decompose, remove_space, space_around_equal):
if not ava:
return ''
space = ' ' if space_around_equal else ''
attr_name, _, value = ava.partition('=')
if decompose:
if remove_space:
component = (attr_name.strip(), value.strip())
else:
component = (attr_name, value)
else:
if remove_space:
component = attr_name.strip() + space + '=' + space + value.strip()
else:
component = attr_name + space + '=' + space + value
return component
def to_dn(iterator, decompose=False, remove_space=False, space_around_equal=False, separate_rdn=False):
"""
Convert an iterator to a list of dn parts
if decompose=True return a list of tuple (one for each dn component) else return a list of strings
if remove_space=True removes unneeded spaces
if space_around_equal=True add spaces around equal in returned strings
if separate_rdn=True consider multiple RDNs as different component of DN
"""
dn = []
component = ''
escape_sequence = False
for c in iterator:
if c == '\\': # escape sequence
escape_sequence = True
elif escape_sequence and c != ' ':
escape_sequence = False
elif c == '+' and separate_rdn:
dn.append(_add_ava(component, decompose, remove_space, space_around_equal))
component = ''
continue
elif c == ',':
if '=' in component:
dn.append(_add_ava(component, decompose, remove_space, space_around_equal))
component = ''
continue
component += c
dn.append(_add_ava(component, decompose, remove_space, space_around_equal))
return dn
def _find_first_unescaped(dn, char, pos):
while True:
pos = dn.find(char, pos)
if pos == -1:
break # no char found
if pos > 0 and dn[pos - 1] != '\\': # unescaped char
break
elif pos > 1 and dn[pos - 1] == '\\': # may be unescaped
escaped = True
for c in dn[pos - 2:0:-1]:
if c == '\\':
escaped = not escaped
else:
break
if not escaped:
break
pos += 1
return pos
def _find_last_unescaped(dn, char, start, stop=0):
while True:
stop = dn.rfind(char, start, stop)
if stop == -1:
break
if stop >= 0 and dn[stop - 1] != '\\':
break
elif stop > 1 and dn[stop - 1] == '\\': # may be unescaped
escaped = True
for c in dn[stop - 2:0:-1]:
if c == '\\':
escaped = not escaped
else:
break
if not escaped:
break
if stop < start:
stop = -1
break
return stop
def _get_next_ava(dn):
comma = _find_first_unescaped(dn, ',', 0)
plus = _find_first_unescaped(dn, '+', 0)
if plus > 0 and (plus < comma or comma == -1):
equal = _find_first_unescaped(dn, '=', plus + 1)
if equal > plus + 1:
plus = _find_last_unescaped(dn, '+', plus, equal)
return dn[:plus], '+'
if comma > 0:
equal = _find_first_unescaped(dn, '=', comma + 1)
if equal > comma + 1:
comma = _find_last_unescaped(dn, ',', comma, equal)
return dn[:comma], ','
return dn, ''
def _split_ava(ava, escape=False, strip=True):
equal = ava.find('=')
while equal > 0: # not first character
if ava[equal - 1] != '\\': # not an escaped equal so it must be an ava separator
# attribute_type1 = ava[0:equal].strip() if strip else ava[0:equal]
if strip:
attribute_type = ava[0:equal].strip()
attribute_value = _escape_attribute_value(ava[equal + 1:].strip()) if escape else ava[equal + 1:].strip()
else:
attribute_type = ava[0:equal]
attribute_value = _escape_attribute_value(ava[equal + 1:]) if escape else ava[equal + 1:]
return attribute_type, attribute_value
equal = ava.find('=', equal + 1)
return '', (ava.strip if strip else ava) # if no equal found return only value
def _validate_attribute_type(attribute_type):
if not attribute_type:
raise LDAPInvalidDnError('attribute type not present')
if attribute_type == '<GUID': # patch for AD DirSync
return True
for c in attribute_type:
if not (c in ascii_letters or c in digits or c == '-'): # allowed uppercase and lowercase letters, digits and hyphen as per RFC 4512
raise LDAPInvalidDnError('character \'' + c + '\' not allowed in attribute type')
if attribute_type[0] in digits or attribute_type[0] == '-': # digits and hyphen not allowed as first character
raise LDAPInvalidDnError('character \'' + attribute_type[0] + '\' not allowed as first character of attribute type')
return True
def _validate_attribute_value(attribute_value):
if not attribute_value:
return False
if attribute_value[0] == '#': # only hex characters are valid
for c in attribute_value:
if c not in hexdigits: # allowed only hex digits as per RFC 4514
raise LDAPInvalidDnError('character ' + c + ' not allowed in hex representation of attribute value')
if len(attribute_value) % 2 == 0: # string must be # + HEX HEX (an odd number of chars)
raise LDAPInvalidDnError('hex representation must be in the form of <HEX><HEX> pairs')
if attribute_value[0] == ' ': # unescaped space cannot be used as leading or last character
raise LDAPInvalidDnError('SPACE must be escaped as leading character of attribute value')
if attribute_value.endswith(' ') and not attribute_value.endswith('\\ '):
raise LDAPInvalidDnError('SPACE must be escaped as trailing character of attribute value')
state = STATE_ANY
for c in attribute_value:
if state == STATE_ANY:
if c == '\\':
state = STATE_ESCAPE
elif c in '"#+,;<=>\00':
raise LDAPInvalidDnError('special character ' + c + ' must be escaped')
elif state == STATE_ESCAPE:
if c in hexdigits:
state = STATE_ESCAPE_HEX
elif c in ' "#+,;<=>\\\00':
state = STATE_ANY
else:
raise LDAPInvalidDnError('invalid escaped character ' + c)
elif state == STATE_ESCAPE_HEX:
if c in hexdigits:
state = STATE_ANY
else:
raise LDAPInvalidDnError('invalid escaped character ' + c)
# final state
if state != STATE_ANY:
raise LDAPInvalidDnError('invalid final character')
return True
def _escape_attribute_value(attribute_value):
if not attribute_value:
return ''
if attribute_value[0] == '#': # with leading SHARP only pairs of hex characters are valid
valid_hex = True
if len(attribute_value) % 2 == 0: # string must be # + HEX HEX (an odd number of chars)
valid_hex = False
if valid_hex:
for c in attribute_value:
if c not in hexdigits: # allowed only hex digits as per RFC 4514
valid_hex = False
break
if valid_hex:
return attribute_value
state = STATE_ANY
escaped = ''
tmp_buffer = ''
for c in attribute_value:
if state == STATE_ANY:
if c == '\\':
state = STATE_ESCAPE
elif c in '"#+,;<=>\00':
escaped += '\\' + c
else:
escaped += c
elif state == STATE_ESCAPE:
if c in hexdigits:
tmp_buffer = c
state = STATE_ESCAPE_HEX
elif c in ' "#+,;<=>\\\00':
escaped += '\\' + c
state = STATE_ANY
else:
escaped += '\\\\' + c
elif state == STATE_ESCAPE_HEX:
if c in hexdigits:
escaped += '\\' + tmp_buffer + c
else:
escaped += '\\\\' + tmp_buffer + c
tmp_buffer = ''
state = STATE_ANY
# final state
if state == STATE_ESCAPE:
escaped += '\\\\'
elif state == STATE_ESCAPE_HEX:
escaped += '\\\\' + tmp_buffer
if escaped[0] == ' ': # leading SPACE must be escaped
escaped = '\\' + escaped
if escaped[-1] == ' ' and len(escaped) > 1 and escaped[-2] != '\\': # trailing SPACE must be escaped
escaped = escaped[:-1] + '\\ '
return escaped
def parse_dn(dn, escape=False, strip=False):
"""
Parses a DN into syntactic components
:param dn:
:param escape:
:param strip:
:return:
a list of tripels representing `attributeTypeAndValue` elements
containing `attributeType`, `attributeValue` and the following separator (`COMMA` or `PLUS`) if given, else an empty `str`.
in their original representation, still containing escapes or encoded as hex.
"""
rdns = []
avas = []
while dn:
ava, separator = _get_next_ava(dn) # if returned ava doesn't containg any unescaped equal it'a appended to last ava in avas
dn = dn[len(ava) + 1:]
if _find_first_unescaped(ava, '=', 0) > 0 or len(avas) == 0:
avas.append((ava, separator))
else:
avas[len(avas) - 1] = (avas[len(avas) - 1][0] + avas[len(avas) - 1][1] + ava, separator)
for ava, separator in avas:
attribute_type, attribute_value = _split_ava(ava, escape, strip)
if not _validate_attribute_type(attribute_type):
raise LDAPInvalidDnError('unable to validate attribute type in ' + ava)
if not _validate_attribute_value(attribute_value):
raise LDAPInvalidDnError('unable to validate attribute value in ' + ava)
rdns.append((attribute_type, attribute_value, separator))
dn = dn[len(ava) + 1:]
if not rdns:
raise LDAPInvalidDnError('empty dn')
return rdns
def safe_dn(dn, decompose=False, reverse=False):
"""
normalize and escape a dn, if dn is a sequence it is joined.
the reverse parameter changes the join direction of the sequence
"""
if isinstance(dn, SEQUENCE_TYPES):
components = [rdn for rdn in dn]
if reverse:
dn = ','.join(reversed(components))
else:
dn = ','.join(components)
if decompose:
escaped_dn = []
else:
escaped_dn = ''
if dn.startswith('<GUID=') and dn.endswith('>'): # Active Directory allows looking up objects by putting its GUID in a specially-formatted DN (e.g. '<GUID=7b95f0d5-a3ed-486c-919c-077b8c9731f2>')
escaped_dn = dn
elif dn.startswith('<WKGUID=') and dn.endswith('>'): # Active Directory allows Binding to Well-Known Objects Using WKGUID in a specially-formatted DN (e.g. <WKGUID=a9d1ca15768811d1aded00c04fd8d5cd,dc=Fabrikam,dc=com>)
escaped_dn = dn
elif dn.startswith('<SID=') and dn.endswith('>'): # Active Directory allows looking up objects by putting its security identifier (SID) in a specially-formatted DN (e.g. '<SID=S-#-#-##-##########-##########-##########-######>')
escaped_dn = dn
elif '@' not in dn: # active directory UPN (User Principal Name) consist of an account, the at sign (@) and a domain, or the domain level logn name domain\username
for component in parse_dn(dn, escape=True):
if decompose:
escaped_dn.append((component[0], component[1], component[2]))
else:
escaped_dn += component[0] + '=' + component[1] + component[2]
elif '@' in dn and '=' not in dn and len(dn.split('@')) != 2:
raise LDAPInvalidDnError('Active Directory User Principal Name must consist of name@domain')
elif '\\' in dn and '=' not in dn and len(dn.split('\\')) != 2:
raise LDAPInvalidDnError('Active Directory Domain Level Logon Name must consist of name\\domain')
else:
escaped_dn = dn
return escaped_dn
def safe_rdn(dn, decompose=False):
"""Returns a list of rdn for the dn, usually there is only one rdn, but it can be more than one when the + sign is used"""
escaped_rdn = []
one_more = True
for component in parse_dn(dn, escape=True):
if component[2] == '+' or one_more:
if decompose:
escaped_rdn.append((component[0], component[1]))
else:
escaped_rdn.append(component[0] + '=' + component[1])
if component[2] == '+':
one_more = True
else:
one_more = False
break
if one_more:
raise LDAPInvalidDnError('bad dn ' + str(dn))
return escaped_rdn
def escape_rdn(rdn):
"""
Escape rdn characters to prevent injection according to RFC 4514.
"""
# '/' must be handled first or the escape slashes will be escaped!
for char in ['\\', ',', '+', '"', '<', '>', ';', '=', '\x00']:
rdn = rdn.replace(char, '\\' + char)
if rdn[0] == '#' or rdn[0] == ' ':
rdn = ''.join(('\\', rdn))
if rdn[-1] == ' ':
rdn = ''.join((rdn[:-1], '\\ '))
return rdn

View File

@@ -0,0 +1,94 @@
"""
"""
# Created on 2015.07.16
#
# Author: Giovanni Cannata
#
# Copyright 2015 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from .. import HASHED_NONE, HASHED_MD5, HASHED_SALTED_MD5, HASHED_SALTED_SHA, HASHED_SALTED_SHA256, \
HASHED_SALTED_SHA384, HASHED_SALTED_SHA512, HASHED_SHA, HASHED_SHA256, HASHED_SHA384, HASHED_SHA512
import hashlib
from os import urandom
from base64 import b64encode
from ..core.exceptions import LDAPInvalidHashAlgorithmError
# each tuple: (the string to include between braces in the digest, the name of the algorithm to invoke with the new() function)
algorithms_table = {
HASHED_MD5: ('md5', 'MD5'),
HASHED_SHA: ('sha', 'SHA1'),
HASHED_SHA256: ('sha256', 'SHA256'),
HASHED_SHA384: ('sha384', 'SHA384'),
HASHED_SHA512: ('sha512', 'SHA512')
}
salted_table = {
HASHED_SALTED_MD5: ('smd5', HASHED_MD5),
HASHED_SALTED_SHA: ('ssha', HASHED_SHA),
HASHED_SALTED_SHA256: ('ssha256', HASHED_SHA256),
HASHED_SALTED_SHA384: ('ssha384', HASHED_SHA384),
HASHED_SALTED_SHA512: ('ssha512', HASHED_SHA512)
}
def hashed(algorithm, value, salt=None, raw=False, encoding='utf-8'):
if str is not bytes and not isinstance(value, bytes): # Python 3
value = value.encode(encoding)
if algorithm is None or algorithm == HASHED_NONE:
return value
# algorithm name can be already coded in the ldap3 constants or can be any value passed in the 'algorithm' parameter
if algorithm in algorithms_table:
try:
digest = hashlib.new(algorithms_table[algorithm][1], value).digest()
except ValueError:
raise LDAPInvalidHashAlgorithmError('Hash algorithm ' + str(algorithm) + ' not available')
if raw:
return digest
return ('{%s}' % algorithms_table[algorithm][0]) + b64encode(digest).decode('ascii')
elif algorithm in salted_table:
if not salt:
salt = urandom(8)
digest = hashed(salted_table[algorithm][1], value + salt, raw=True) + salt
if raw:
return digest
return ('{%s}' % salted_table[algorithm][0]) + b64encode(digest).decode('ascii')
else:
# if an unknown (to the library) algorithm is requested passes the name as the string in braces and as the algorithm name
# if salt is present uses it to salt the digest
try:
if not salt:
digest = hashlib.new(algorithm, value).digest()
else:
digest = hashlib.new(algorithm, value + salt).digest() + salt
except ValueError:
raise LDAPInvalidHashAlgorithmError('Hash algorithm ' + str(algorithm) + ' not available')
if raw:
return digest
return ('{%s}' % algorithm) + b64encode(digest).decode('ascii')

View File

@@ -0,0 +1,216 @@
"""
"""
# Created on 2015.05.01
#
# Author: Giovanni Cannata
#
# Copyright 2015 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from logging import getLogger, DEBUG
from copy import deepcopy
from pprint import pformat
from ..protocol.rfc4511 import LDAPMessage
# logging levels
OFF = 0
ERROR = 10
BASIC = 20
PROTOCOL = 30
NETWORK = 40
EXTENDED = 50
_sensitive_lines = ('simple', 'credentials', 'serversaslcreds') # must be a tuple, not a list, lowercase
_sensitive_args = ('simple', 'password', 'sasl_credentials', 'saslcreds', 'server_creds')
_sensitive_attrs = ('userpassword', 'unicodepwd')
_hide_sensitive_data = None
DETAIL_LEVELS = [OFF, ERROR, BASIC, PROTOCOL, NETWORK, EXTENDED]
_max_line_length = 4096
_logging_level = 0
_detail_level = 0
_logging_encoding = 'ascii'
try:
from logging import NullHandler
except ImportError: # NullHandler not present in Python < 2.7
from logging import Handler
class NullHandler(Handler):
def handle(self, record):
pass
def emit(self, record):
pass
def createLock(self):
self.lock = None
def _strip_sensitive_data_from_dict(d):
if not isinstance(d, dict):
return d
try:
d = deepcopy(d)
except Exception: # if deepcopy goes wrong gives up and returns the dict unchanged
return d
for k in d.keys():
if isinstance(d[k], dict):
d[k] = _strip_sensitive_data_from_dict(d[k])
elif k.lower() in _sensitive_args and d[k]:
d[k] = '<stripped %d characters of sensitive data>' % len(d[k])
return d
def get_detail_level_name(level_name):
if level_name == OFF:
return 'OFF'
elif level_name == ERROR:
return 'ERROR'
elif level_name == BASIC:
return 'BASIC'
elif level_name == PROTOCOL:
return 'PROTOCOL'
elif level_name == NETWORK:
return 'NETWORK'
elif level_name == EXTENDED:
return 'EXTENDED'
raise ValueError('unknown detail level')
def log(detail, message, *args):
if detail <= _detail_level:
if _hide_sensitive_data:
args = tuple([_strip_sensitive_data_from_dict(arg) if isinstance(arg, dict) else arg for arg in args])
if str is not bytes: # Python 3
encoded_message = (get_detail_level_name(detail) + ':' + message % args).encode(_logging_encoding, 'backslashreplace')
encoded_message = encoded_message.decode()
else:
try:
encoded_message = (get_detail_level_name(detail) + ':' + message % args).encode(_logging_encoding, 'replace')
except Exception:
encoded_message = (get_detail_level_name(detail) + ':' + message % args).decode(_logging_encoding, 'replace')
if len(encoded_message) > _max_line_length:
logger.log(_logging_level, encoded_message[:_max_line_length] + ' <removed %d remaining bytes in this log line>' % (len(encoded_message) - _max_line_length, ))
else:
logger.log(_logging_level, encoded_message)
def log_enabled(detail):
if detail <= _detail_level:
if logger.isEnabledFor(_logging_level):
return True
return False
def set_library_log_hide_sensitive_data(hide=True):
global _hide_sensitive_data
if hide:
_hide_sensitive_data = True
else:
_hide_sensitive_data = False
if log_enabled(ERROR):
log(ERROR, 'hide sensitive data set to ' + str(_hide_sensitive_data))
def get_library_log_hide_sensitive_data():
return True if _hide_sensitive_data else False
def set_library_log_activation_level(logging_level):
if isinstance(logging_level, int):
global _logging_level
_logging_level = logging_level
else:
if log_enabled(ERROR):
log(ERROR, 'invalid library log activation level <%s> ', logging_level)
raise ValueError('invalid library log activation level')
def get_library_log_activation_lavel():
return _logging_level
def set_library_log_max_line_length(length):
if isinstance(length, int):
global _max_line_length
_max_line_length = length
else:
if log_enabled(ERROR):
log(ERROR, 'invalid log max line length <%s> ', length)
raise ValueError('invalid library log max line length')
def get_library_log_max_line_length():
return _max_line_length
def set_library_log_detail_level(detail):
if detail in DETAIL_LEVELS:
global _detail_level
_detail_level = detail
if log_enabled(ERROR):
log(ERROR, 'detail level set to ' + get_detail_level_name(_detail_level))
else:
if log_enabled(ERROR):
log(ERROR, 'unable to set log detail level to <%s>', detail)
raise ValueError('invalid library log detail level')
def get_library_log_detail_level():
return _detail_level
def format_ldap_message(message, prefix):
if isinstance(message, LDAPMessage):
try: # pyasn1 prettyprint raises exception in version 0.4.3
formatted = message.prettyPrint().split('\n') # pyasn1 pretty print
except Exception as e:
formatted = ['pyasn1 exception', str(e)]
else:
formatted = pformat(message).split('\n')
prefixed = ''
for line in formatted:
if line:
if _hide_sensitive_data and line.strip().lower().startswith(_sensitive_lines): # _sensitive_lines is a tuple. startswith() method checks each tuple element
tag, _, data = line.partition('=')
if data.startswith("b'") and data.endswith("'") or data.startswith('b"') and data.endswith('"'):
prefixed += '\n' + prefix + tag + '=<stripped %d characters of sensitive data>' % (len(data) - 3, )
else:
prefixed += '\n' + prefix + tag + '=<stripped %d characters of sensitive data>' % len(data)
else:
prefixed += '\n' + prefix + line
return prefixed
# sets a logger for the library with NullHandler. It can be used by the application with its own logging configuration
logger = getLogger('ldap3')
logger.addHandler(NullHandler())
# sets defaults for the library logging
set_library_log_activation_level(DEBUG)
set_library_log_detail_level(OFF)
set_library_log_hide_sensitive_data(True)

View File

@@ -0,0 +1,505 @@
"""
"""
# Created on 2015.04.02
#
# Author: Giovanni Cannata
#
# Copyright 2015 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
# NTLMv2 authentication as per [MS-NLMP] (https://msdn.microsoft.com/en-us/library/cc236621.aspx)
from struct import pack, unpack
from platform import system, version
from socket import gethostname
from time import time
import hmac
import hashlib
import binascii
from os import urandom
try:
from locale import getpreferredencoding
oem_encoding = getpreferredencoding()
except Exception:
oem_encoding = 'utf-8'
from ..protocol.formatters.formatters import format_ad_timestamp
NTLM_SIGNATURE = b'NTLMSSP\x00'
NTLM_MESSAGE_TYPE_NTLM_NEGOTIATE = 1
NTLM_MESSAGE_TYPE_NTLM_CHALLENGE = 2
NTLM_MESSAGE_TYPE_NTLM_AUTHENTICATE = 3
FLAG_NEGOTIATE_56 = 31 # W
FLAG_NEGOTIATE_KEY_EXCH = 30 # V
FLAG_NEGOTIATE_128 = 29 # U
FLAG_NEGOTIATE_VERSION = 25 # T
FLAG_NEGOTIATE_TARGET_INFO = 23 # S
FLAG_REQUEST_NOT_NT_SESSION_KEY = 22 # R
FLAG_NEGOTIATE_IDENTIFY = 20 # Q
FLAG_NEGOTIATE_EXTENDED_SESSIONSECURITY = 19 # P
FLAG_TARGET_TYPE_SERVER = 17 # O
FLAG_TARGET_TYPE_DOMAIN = 16 # N
FLAG_NEGOTIATE_ALWAYS_SIGN = 15 # M
FLAG_NEGOTIATE_OEM_WORKSTATION_SUPPLIED = 13 # L
FLAG_NEGOTIATE_OEM_DOMAIN_SUPPLIED = 12 # K
FLAG_NEGOTIATE_ANONYMOUS = 11 # J
FLAG_NEGOTIATE_NTLM = 9 # H
FLAG_NEGOTIATE_LM_KEY = 7 # G
FLAG_NEGOTIATE_DATAGRAM = 6 # F
FLAG_NEGOTIATE_SEAL = 5 # E
FLAG_NEGOTIATE_SIGN = 4 # D
FLAG_REQUEST_TARGET = 2 # C
FLAG_NEGOTIATE_OEM = 1 # B
FLAG_NEGOTIATE_UNICODE = 0 # A
FLAG_TYPES = [FLAG_NEGOTIATE_56,
FLAG_NEGOTIATE_KEY_EXCH,
FLAG_NEGOTIATE_128,
FLAG_NEGOTIATE_VERSION,
FLAG_NEGOTIATE_TARGET_INFO,
FLAG_REQUEST_NOT_NT_SESSION_KEY,
FLAG_NEGOTIATE_IDENTIFY,
FLAG_NEGOTIATE_EXTENDED_SESSIONSECURITY,
FLAG_TARGET_TYPE_SERVER,
FLAG_TARGET_TYPE_DOMAIN,
FLAG_NEGOTIATE_ALWAYS_SIGN,
FLAG_NEGOTIATE_OEM_WORKSTATION_SUPPLIED,
FLAG_NEGOTIATE_OEM_DOMAIN_SUPPLIED,
FLAG_NEGOTIATE_ANONYMOUS,
FLAG_NEGOTIATE_NTLM,
FLAG_NEGOTIATE_LM_KEY,
FLAG_NEGOTIATE_DATAGRAM,
FLAG_NEGOTIATE_SEAL,
FLAG_NEGOTIATE_SIGN,
FLAG_REQUEST_TARGET,
FLAG_NEGOTIATE_OEM,
FLAG_NEGOTIATE_UNICODE]
AV_END_OF_LIST = 0
AV_NETBIOS_COMPUTER_NAME = 1
AV_NETBIOS_DOMAIN_NAME = 2
AV_DNS_COMPUTER_NAME = 3
AV_DNS_DOMAIN_NAME = 4
AV_DNS_TREE_NAME = 5
AV_FLAGS = 6
AV_TIMESTAMP = 7
AV_SINGLE_HOST_DATA = 8
AV_TARGET_NAME = 9
AV_CHANNEL_BINDINGS = 10
AV_TYPES = [AV_END_OF_LIST,
AV_NETBIOS_COMPUTER_NAME,
AV_NETBIOS_DOMAIN_NAME,
AV_DNS_COMPUTER_NAME,
AV_DNS_DOMAIN_NAME,
AV_DNS_TREE_NAME,
AV_FLAGS,
AV_TIMESTAMP,
AV_SINGLE_HOST_DATA,
AV_TARGET_NAME,
AV_CHANNEL_BINDINGS]
AV_FLAG_CONSTRAINED = 0
AV_FLAG_INTEGRITY = 1
AV_FLAG_TARGET_SPN_UNTRUSTED = 2
AV_FLAG_TYPES = [AV_FLAG_CONSTRAINED,
AV_FLAG_INTEGRITY,
AV_FLAG_TARGET_SPN_UNTRUSTED]
def pack_windows_version(debug=False):
if debug:
if system().lower() == 'windows':
try:
major_release, minor_release, build = version().split('.')
major_release = int(major_release)
minor_release = int(minor_release)
build = int(build)
except Exception:
major_release = 5
minor_release = 1
build = 2600
else:
major_release = 5
minor_release = 1
build = 2600
else:
major_release = 0
minor_release = 0
build = 0
return pack('<B', major_release) + \
pack('<B', minor_release) + \
pack('<H', build) + \
pack('<B', 0) + \
pack('<B', 0) + \
pack('<B', 0) + \
pack('<B', 15)
def unpack_windows_version(version_message):
if len(version_message) != 8:
raise ValueError('version field must be 8 bytes long')
if str is bytes: # Python 2
return (unpack('<B', version_message[0])[0],
unpack('<B', version_message[1])[0],
unpack('<H', version_message[2:4])[0],
unpack('<B', version_message[7])[0])
else: # Python 3
return (int(version_message[0]),
int(version_message[1]),
int(unpack('<H', version_message[2:4])[0]),
int(version_message[7]))
class NtlmClient(object):
def __init__(self, domain, user_name, password):
self.client_config_flags = 0
self.exported_session_key = None
self.negotiated_flags = None
self.user_name = user_name
self.user_domain = domain
self.no_lm_response_ntlm_v1 = None
self.client_blocked = False
self.client_block_exceptions = []
self.client_require_128_bit_encryption = None
self.max_life_time = None
self.client_signing_key = None
self.client_sealing_key = None
self.sequence_number = None
self.server_sealing_key = None
self.server_signing_key = None
self.integrity = False
self.replay_detect = False
self.sequence_detect = False
self.confidentiality = False
self.datagram = False
self.identity = False
self.client_supplied_target_name = None
self.client_channel_binding_unhashed = None
self.unverified_target_name = None
self._password = password
self.server_challenge = None
self.server_target_name = None
self.server_target_info = None
self.server_version = None
self.server_av_netbios_computer_name = None
self.server_av_netbios_domain_name = None
self.server_av_dns_computer_name = None
self.server_av_dns_domain_name = None
self.server_av_dns_forest_name = None
self.server_av_target_name = None
self.server_av_flags = None
self.server_av_timestamp = None
self.server_av_single_host_data = None
self.server_av_channel_bindings = None
self.server_av_flag_constrained = None
self.server_av_flag_integrity = None
self.server_av_flag_target_spn_untrusted = None
self.current_encoding = None
self.client_challenge = None
self.server_target_info_raw = None
def get_client_flag(self, flag):
if not self.client_config_flags:
return False
if flag in FLAG_TYPES:
return True if self.client_config_flags & (1 << flag) else False
raise ValueError('invalid flag')
def get_negotiated_flag(self, flag):
if not self.negotiated_flags:
return False
if flag not in FLAG_TYPES:
raise ValueError('invalid flag')
return True if self.negotiated_flags & (1 << flag) else False
def get_server_av_flag(self, flag):
if not self.server_av_flags:
return False
if flag not in AV_FLAG_TYPES:
raise ValueError('invalid AV flag')
return True if self.server_av_flags & (1 << flag) else False
def set_client_flag(self, flags):
if type(flags) == int:
flags = [flags]
for flag in flags:
if flag in FLAG_TYPES:
self.client_config_flags |= (1 << flag)
else:
raise ValueError('invalid flag')
def reset_client_flags(self):
self.client_config_flags = 0
def unset_client_flag(self, flags):
if type(flags) == int:
flags = [flags]
for flag in flags:
if flag in FLAG_TYPES:
self.client_config_flags &= ~(1 << flag)
else:
raise ValueError('invalid flag')
def create_negotiate_message(self):
"""
Microsoft MS-NLMP 2.2.1.1
"""
self.reset_client_flags()
self.set_client_flag([FLAG_REQUEST_TARGET,
FLAG_NEGOTIATE_56,
FLAG_NEGOTIATE_128,
FLAG_NEGOTIATE_NTLM,
FLAG_NEGOTIATE_ALWAYS_SIGN,
FLAG_NEGOTIATE_OEM,
FLAG_NEGOTIATE_UNICODE,
FLAG_NEGOTIATE_EXTENDED_SESSIONSECURITY])
message = NTLM_SIGNATURE # 8 bytes
message += pack('<I', NTLM_MESSAGE_TYPE_NTLM_NEGOTIATE) # 4 bytes
message += pack('<I', self.client_config_flags) # 4 bytes
message += self.pack_field('', 40) # domain name field # 8 bytes
if self.get_client_flag(FLAG_NEGOTIATE_VERSION): # version 8 bytes - used for debug in ntlm
message += pack_windows_version(True)
else:
message += pack_windows_version(False)
return message
def parse_challenge_message(self, message):
"""
Microsoft MS-NLMP 2.2.1.2
"""
if len(message) < 56: # minimum size of challenge message
return False
if message[0:8] != NTLM_SIGNATURE: # NTLM signature - 8 bytes
return False
if int(unpack('<I', message[8:12])[0]) != NTLM_MESSAGE_TYPE_NTLM_CHALLENGE: # type of message - 4 bytes
return False
target_name_len, _, target_name_offset = self.unpack_field(message[12:20]) # targetNameFields - 8 bytes
self.negotiated_flags = unpack('<I', message[20:24])[0] # negotiated flags - 4 bytes
self.current_encoding = 'utf-16-le' if self.get_negotiated_flag(
FLAG_NEGOTIATE_UNICODE) else oem_encoding # set encoding
self.server_challenge = message[24:32] # server challenge - 8 bytes
target_info_len, _, target_info_offset = self.unpack_field(message[40:48]) # targetInfoFields - 8 bytes
self.server_version = unpack_windows_version(message[48:56])
if self.get_negotiated_flag(FLAG_REQUEST_TARGET) and target_name_len:
self.server_target_name = message[target_name_offset: target_name_offset + target_name_len].decode(
self.current_encoding)
if self.get_negotiated_flag(FLAG_NEGOTIATE_TARGET_INFO) and target_info_len:
self.server_target_info_raw = message[target_info_offset: target_info_offset + target_info_len]
self.server_target_info = self.unpack_av_info(self.server_target_info_raw)
for attribute, value in self.server_target_info:
if attribute == AV_NETBIOS_COMPUTER_NAME:
self.server_av_netbios_computer_name = value.decode('utf-16-le') # always unicode
elif attribute == AV_NETBIOS_DOMAIN_NAME:
self.server_av_netbios_domain_name = value.decode('utf-16-le') # always unicode
elif attribute == AV_DNS_COMPUTER_NAME:
self.server_av_dns_computer_name = value.decode('utf-16-le') # always unicode
elif attribute == AV_DNS_DOMAIN_NAME:
self.server_av_dns_domain_name = value.decode('utf-16-le') # always unicode
elif attribute == AV_DNS_TREE_NAME:
self.server_av_dns_forest_name = value.decode('utf-16-le') # always unicode
elif attribute == AV_FLAGS:
if self.get_server_av_flag(AV_FLAG_CONSTRAINED):
self.server_av_flag_constrained = True
if self.get_server_av_flag(AV_FLAG_INTEGRITY):
self.server_av_flag_integrity = True
if self.get_server_av_flag(AV_FLAG_TARGET_SPN_UNTRUSTED):
self.server_av_flag_target_spn_untrusted = True
elif attribute == AV_TIMESTAMP:
self.server_av_timestamp = format_ad_timestamp(unpack('<Q', value)[0])
elif attribute == AV_SINGLE_HOST_DATA:
self.server_av_single_host_data = value
elif attribute == AV_TARGET_NAME:
self.server_av_target_name = value.decode('utf-16-le') # always unicode
elif attribute == AV_CHANNEL_BINDINGS:
self.server_av_channel_bindings = value
else:
raise ValueError('unknown AV type')
def create_authenticate_message(self):
"""
Microsoft MS-NLMP 2.2.1.3
"""
# 3.1.5.2
if not self.client_config_flags and not self.negotiated_flags:
return False
# 3.1.5.2
if self.get_client_flag(FLAG_NEGOTIATE_128) and not self.get_negotiated_flag(FLAG_NEGOTIATE_128):
return False
# 3.1.5.2
if (not self.server_av_netbios_computer_name or not self.server_av_netbios_domain_name) and self.server_av_flag_integrity:
return False
message = NTLM_SIGNATURE # 8 bytes
message += pack('<I', NTLM_MESSAGE_TYPE_NTLM_AUTHENTICATE) # 4 bytes
pos = 88 # payload starts at 88
# 3.1.5.2
if self.server_target_info:
lm_challenge_response = b''
else:
# computed LmChallengeResponse - todo
lm_challenge_response = b''
message += self.pack_field(lm_challenge_response, pos) # LmChallengeResponseField field # 8 bytes
pos += len(lm_challenge_response)
nt_challenge_response = self.compute_nt_response()
message += self.pack_field(nt_challenge_response, pos) # NtChallengeResponseField field # 8 bytes
pos += len(nt_challenge_response)
domain_name = self.user_domain.encode(self.current_encoding)
message += self.pack_field(domain_name, pos) # DomainNameField field # 8 bytes
pos += len(domain_name)
user_name = self.user_name.encode(self.current_encoding)
message += self.pack_field(user_name, pos) # UserNameField field # 8 bytes
pos += len(user_name)
if self.get_negotiated_flag(FLAG_NEGOTIATE_OEM_WORKSTATION_SUPPLIED) or self.get_negotiated_flag(
FLAG_NEGOTIATE_VERSION):
workstation = gethostname().encode(self.current_encoding)
else:
workstation = b''
message += self.pack_field(workstation, pos) # empty WorkstationField field # 8 bytes
pos += len(workstation)
encrypted_random_session_key = b''
message += self.pack_field(encrypted_random_session_key, pos) # EncryptedRandomSessionKeyField field # 8 bytes
pos += len(encrypted_random_session_key)
message += pack('<I', self.negotiated_flags) # negotiated flags - 4 bytes
if self.get_negotiated_flag(FLAG_NEGOTIATE_VERSION):
message += pack_windows_version(True) # windows version - 8 bytes
else:
message += pack_windows_version() # empty windows version - 8 bytes
message += pack('<Q', 0) # mic
message += pack('<Q', 0) # mic - total of 16 bytes
# payload starts at 88
message += lm_challenge_response
message += nt_challenge_response
message += domain_name
message += user_name
message += workstation
message += encrypted_random_session_key
return message
@staticmethod
def pack_field(value, offset):
return pack('<HHI', len(value), len(value), offset)
@staticmethod
def unpack_field(field_message):
if len(field_message) != 8:
raise ValueError('ntlm field must be 8 bytes long')
return unpack('<H', field_message[0:2])[0], \
unpack('<H', field_message[2:4])[0], \
unpack('<I', field_message[4:8])[0]
@staticmethod
def unpack_av_info(info):
if info:
avs = list()
done = False
pos = 0
while not done:
av_type = unpack('<H', info[pos: pos + 2])[0]
if av_type not in AV_TYPES:
raise ValueError('unknown AV type')
av_len = unpack('<H', info[pos + 2: pos + 4])[0]
av_value = info[pos + 4: pos + 4 + av_len]
pos += av_len + 4
if av_type == AV_END_OF_LIST:
done = True
else:
avs.append((av_type, av_value))
else:
return list()
return avs
@staticmethod
def pack_av_info(avs):
# avs is a list of tuples, each tuple is made of av_type and av_value
info = b''
for av_type, av_value in avs:
if av_type(0) == AV_END_OF_LIST:
continue
info += pack('<H', av_type)
info += pack('<H', len(av_value))
info += av_value
# add AV_END_OF_LIST
info += pack('<H', AV_END_OF_LIST)
info += pack('<H', 0)
return info
@staticmethod
def pack_windows_timestamp():
return pack('<Q', (int(time()) + 11644473600) * 10000000)
def compute_nt_response(self):
if not self.user_name and not self._password: # anonymous authentication
return b''
self.client_challenge = urandom(8)
temp = b''
temp += pack('<B', 1) # ResponseVersion - 1 byte
temp += pack('<B', 1) # HiResponseVersion - 1 byte
temp += pack('<H', 0) # Z(2)
temp += pack('<I', 0) # Z(4) - total Z(6)
temp += self.pack_windows_timestamp() # time - 8 bytes
temp += self.client_challenge # random client challenge - 8 bytes
temp += pack('<I', 0) # Z(4)
temp += self.server_target_info_raw
temp += pack('<I', 0) # Z(4)
response_key_nt = self.ntowf_v2()
nt_proof_str = hmac.new(response_key_nt, self.server_challenge + temp, digestmod=hashlib.md5).digest()
nt_challenge_response = nt_proof_str + temp
return nt_challenge_response
def ntowf_v2(self):
passparts = self._password.split(':')
if len(passparts) == 2 and len(passparts[0]) == 32 and len(passparts[1]) == 32:
# The specified password is an LM:NTLM hash
password_digest = binascii.unhexlify(passparts[1])
else:
try:
password_digest = hashlib.new('MD4', self._password.encode('utf-16-le')).digest()
except ValueError as e:
try:
from Crypto.Hash import MD4 # try with the Crypto library if present
password_digest = MD4.new(self._password.encode('utf-16-le')).digest()
except ImportError:
raise e # raise original exception
return hmac.new(password_digest, (self.user_name.upper() + self.user_domain).encode('utf-16-le'), digestmod=hashlib.md5).digest()

View File

@@ -0,0 +1,130 @@
# Copyright (c) 2009 Raymond Hettinger
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
# used only in Python 2.6
from UserDict import DictMixin
class OrderedDict(dict, DictMixin):
def __init__(self, *args, **kwds):
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__end
except AttributeError:
self.clear()
self.update(*args, **kwds)
def clear(self):
self.__end = end = []
end += [None, end, end] # sentinel node for doubly linked list
self.__map = {} # key --> [key, prev, next]
dict.clear(self)
def __setitem__(self, key, value):
if key not in self:
end = self.__end
curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end]
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
key, prev, next = self.__map.pop(key)
prev[2] = next
next[1] = prev
def __iter__(self):
end = self.__end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.__end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def popitem(self, last=True):
if not self:
raise KeyError('dictionary is empty')
if last:
key = reversed(self).next()
else:
key = iter(self).next()
value = self.pop(key)
return key, value
def __reduce__(self):
items = [[k, self[k]] for k in self]
tmp = self.__map, self.__end
del self.__map, self.__end
inst_dict = vars(self).copy()
self.__map, self.__end = tmp
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def keys(self):
return list(self)
setdefault = DictMixin.setdefault
update = DictMixin.update
pop = DictMixin.pop
values = DictMixin.values
items = DictMixin.items
iterkeys = DictMixin.iterkeys
itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
def copy(self):
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
if len(self) != len(other):
return False
for p, q in zip(self.items(), other.items()):
if p != q:
return False
return True
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other

View File

@@ -0,0 +1,37 @@
""" Some helper functions for validation of ports and lists of ports. """
def check_port(port):
""" Check if a port is valid. Return an error message indicating what is invalid if something isn't valid. """
if isinstance(port, int):
if port not in range(0, 65535):
return 'Source port must in range from 0 to 65535'
else:
return 'Source port must be an integer'
return None
def check_port_and_port_list(port, port_list):
""" Take in a port and a port list and check that at most one is non-null. Additionally check that if either
is non-null, it is valid.
Return an error message indicating what is invalid if something isn't valid.
"""
if port is not None and port_list is not None:
return 'Cannot specify both a source port and a source port list'
elif port is not None:
if isinstance(port, int):
if port not in range(0, 65535):
return 'Source port must in range from 0 to 65535'
else:
return 'Source port must be an integer'
elif port_list is not None:
try:
_ = iter(port_list)
except TypeError:
return 'Source port list must be an iterable {}'.format(port_list)
for candidate_port in port_list:
err = check_port(candidate_port)
if err:
return err
return None

View File

@@ -0,0 +1,51 @@
"""
"""
# Created on 2015.07.09
#
# Author: Giovanni Cannata
#
# Copyright 2015 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
from binascii import hexlify
from .. import STRING_TYPES
try:
from sys import stdout
repr_encoding = stdout.encoding # get the encoding of the stdout for printing (repr)
if not repr_encoding:
repr_encoding = 'ascii' # default
except Exception:
repr_encoding = 'ascii' # default
def to_stdout_encoding(value):
if not isinstance(value, STRING_TYPES):
value = str(value)
if str is bytes: # Python 2
try:
return value.encode(repr_encoding, 'backslashreplace')
except UnicodeDecodeError: # Python 2.6
return hexlify(value)
else: # Python 3
try:
return value.encode(repr_encoding, errors='backslashreplace').decode(repr_encoding, errors='backslashreplace')
except UnicodeDecodeError:
return hexlify(value)

View File

@@ -0,0 +1,133 @@
"""
"""
# Created on 2014.10.05
#
# Author: Giovanni Cannata
#
# Copyright 2014 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
import re
from ..utils.log import log, log_enabled, NETWORK
try:
from backports.ssl_match_hostname import match_hostname, CertificateError
except ImportError:
class CertificateError(ValueError): # fix for Python 2, code from Python 3.5 standard library
pass
def _dnsname_match(dn, hostname, max_wildcards=1):
"""Backported from Python 3.4.3 standard library
Matching according to RFC 6125, section 6.4.3
http://tools.ietf.org/html/rfc6125#section-6.4.3
"""
if log_enabled(NETWORK):
log(NETWORK, "matching dn %s with hostname %s", dn, hostname)
pats = []
if not dn:
return False
pieces = dn.split(r'.')
leftmost = pieces[0]
remainder = pieces[1:]
wildcards = leftmost.count('*')
if wildcards > max_wildcards:
# Issue #17980: avoid denials of service by refusing more
# than one wildcard per fragment. A survey of established
# policy among SSL implementations showed it to be a
# reasonable choice.
raise CertificateError(
"too many wildcards in certificate DNS name: " + repr(dn))
# speed up common case w/o wildcards
if not wildcards:
return dn.lower() == hostname.lower()
# RFC 6125, section 6.4.3, subitem 1.
# The client SHOULD NOT attempt to match a presented identifier in which
# the wildcard character comprises a label other than the left-most label.
if leftmost == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
# RFC 6125, section 6.4.3, subitem 3.
# The client SHOULD NOT attempt to match a presented identifier
# where the wildcard character is embedded within an A-label or
# U-label of an internationalized domain name.
pats.append(re.escape(leftmost))
else:
# Otherwise, '*' matches any dotless string, e.g. www*
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
# add the remaining fragments, ignore any wildcards
for frag in remainder:
pats.append(re.escape(frag))
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
return pat.match(hostname)
def match_hostname(cert, hostname):
"""Backported from Python 3.4.3 standard library.
Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
rules are followed, but IP addresses are not accepted for *hostname*.
CertificateError is raised on failure. On success, the function
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate, match_hostname needs a "
"SSL socket or SSL context with either "
"CERT_OPTIONAL or CERT_REQUIRED")
dnsnames = []
san = cert.get('subjectAltName', ())
for key, value in san:
if key == 'DNS':
if _dnsname_match(value, hostname):
return
dnsnames.append(value)
if not dnsnames:
# The subject is only checked when there is no dNSName entry
# in subjectAltName
for sub in cert.get('subject', ()):
for key, value in sub:
# XXX according to RFC 2818, the most specific Common Name
# must be used.
if key == 'commonName':
if _dnsname_match(value, hostname):
return
dnsnames.append(value)
if len(dnsnames) > 1:
raise CertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise CertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
else:
raise CertificateError("no appropriate commonName or "
"subjectAltName fields were found")

View File

@@ -0,0 +1,118 @@
"""
"""
# Created on 2014.09.08
#
# Author: Giovanni Cannata
#
# Copyright 2014 - 2020 Giovanni Cannata
#
# This file is part of ldap3.
#
# ldap3 is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ldap3 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ldap3 in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.
try:
from urllib.parse import unquote # Python3
except ImportError:
from urllib import unquote # Python 2
from .. import SUBTREE, BASE, LEVEL
def parse_uri(uri):
"""
Decode LDAP URI as specified in RFC 4516 relaxing specifications
permitting 'ldaps' as scheme for ssl-ldap
"""
# ldapurl = scheme COLON SLASH SLASH [host [COLON port]]
# [SLASH dn [QUESTION [attributes]
# [QUESTION [scope] [QUESTION [filter]
# [QUESTION extensions]]]]]
# ; <host> and <port> are defined
# ; in Sections 3.2.2 and 3.2.3
# ; of [RFC3986].
# ; <filter> is from Section 3 of
# ; [RFC4515], subject to the
# ; provisions of the
# ; "Percent-Encoding" section
# ; below.
#
# scheme = "ldap" / "ldaps" <== not RFC4516 compliant (original is 'scheme = "ldap"')
# dn = distinguishedName ; From Section 3 of [RFC4514],
# ; subject to the provisions of
# ; the "Percent-Encoding"
# ; section below.
#
# attributes = attrdesc *(COMMA attrdesc)
# attrdesc = selector *(COMMA selector)
# selector = attributeSelector ; From Section 4.5.1 of
# ; [RFC4511], subject to the
# ; provisions of the
# ; "Percent-Encoding" section
# ; below.
#
# scope = "base" / "one" / "sub"
# extensions = extension *(COMMA extension)
# extension = [EXCLAMATION] extype [EQUALS exvalue]
# extype = oid ; From section 1.4 of [RFC4512].
#
# exvalue = LDAPString ; From section 4.1.2 of
# ; [RFC4511], subject to the
# ; provisions of the
# ; "Percent-Encoding" section
# ; below.
#
# EXCLAMATION = %x21 ; exclamation mark ("!")
# SLASH = %x2F ; forward slash ("/")
# COLON = %x3A ; colon (":")
# QUESTION = %x3F ; question mark ("?")
uri_components = dict()
parts = unquote(uri).split('?') # encoding defaults to utf-8 in Python 3
scheme, sep, remain = parts[0].partition('://')
if sep != '://' or scheme not in ['ldap', 'ldaps']:
return None
address, _, uri_components['base'] = remain.partition('/')
uri_components['ssl'] = True if scheme == 'ldaps' else False
uri_components['host'], sep, uri_components['port'] = address.partition(':')
if sep != ':':
if uri_components['ssl']:
uri_components['port'] = 636
else:
uri_components['port'] = None
else:
if not uri_components['port'].isdigit() or not (0 < int(uri_components['port']) < 65536):
return None
else:
uri_components['port'] = int(uri_components['port'])
uri_components['attributes'] = parts[1].split(',') if len(parts) > 1 and parts[1] else None
uri_components['scope'] = parts[2] if len(parts) > 2 else None
if uri_components['scope'] == 'base':
uri_components['scope'] = BASE
elif uri_components['scope'] == 'sub':
uri_components['scope'] = SUBTREE
elif uri_components['scope'] == 'one':
uri_components['scope'] = LEVEL
elif uri_components['scope']:
return None
uri_components['filter'] = parts[3] if len(parts) > 3 else None
uri_components['extensions'] = parts[4].split(',') if len(parts) > 4 else None
return uri_components