Skip to content

Commit fa35757

Browse files
pyldap contributorsencukou
authored andcommitted
py3: Make the bytes/text distinction
- DNs, attribute names, URLs are text (encoded to UTF-8 on the wire) - Attribute values are always bytes A "bytes_mode" switch controls behavior under Python 2.
1 parent 750fe8c commit fa35757

23 files changed

+636
-172
lines changed

Lib/ldap/dn.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
See https://www.python-ldap.org/ for details.
55
"""
66

7+
import sys
78
from ldap.pkginfo import __version__
89

910
import _ldap
@@ -46,6 +47,8 @@ def str2dn(dn,flags=0):
4647
"""
4748
if not dn:
4849
return []
50+
if sys.version_info[0] < 3 and isinstance(dn, unicode):
51+
dn = dn.encode('utf-8')
4952
return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags)
5053

5154

Lib/ldap/functions.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _ldap_function_call(lock,func,*args,**kwargs):
6262
return result
6363

6464

65-
def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None):
65+
def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None, bytes_mode=None):
6666
"""
6767
Return LDAPObject instance by opening LDAP connection to
6868
LDAP host specified by LDAP URL
@@ -76,11 +76,13 @@ def initialize(uri,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None):
7676
trace_file
7777
File object where to write the trace output to.
7878
Default is to use stdout.
79+
bytes_mode
80+
Whether to enable "bytes_mode" for backwards compatibility under Py2.
7981
"""
80-
return LDAPObject(uri,trace_level,trace_file,trace_stack_limit)
82+
return LDAPObject(uri,trace_level,trace_file,trace_stack_limit,bytes_mode)
8183

8284

83-
def open(host,port=389,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None):
85+
def open(host,port=389,trace_level=0,trace_file=sys.stdout,trace_stack_limit=None,bytes_mode=None):
8486
"""
8587
Return LDAPObject instance by opening LDAP connection to
8688
specified LDAP host
@@ -95,10 +97,12 @@ def open(host,port=389,trace_level=0,trace_file=sys.stdout,trace_stack_limit=Non
9597
trace_file
9698
File object where to write the trace output to.
9799
Default is to use stdout.
100+
bytes_mode
101+
Whether to enable "bytes_mode" for backwards compatibility under Py2.
98102
"""
99103
import warnings
100104
warnings.warn('ldap.open() is deprecated! Use ldap.initialize() instead.', DeprecationWarning,2)
101-
return initialize('ldap://%s:%d' % (host,port),trace_level,trace_file,trace_stack_limit)
105+
return initialize('ldap://%s:%d' % (host,port),trace_level,trace_file,trace_stack_limit,bytes_mode)
102106

103107
init = open
104108

Lib/ldap/ldapobject.py

Lines changed: 210 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
See https://www.python-ldap.org/ for details.
55
"""
66

7+
from __future__ import unicode_literals
8+
79
from os import strerror
810

911
from ldap.pkginfo import __version__, __author__, __license__
@@ -20,6 +22,7 @@
2022
import traceback
2123

2224
import sys,time,pprint,_ldap,ldap,ldap.sasl,ldap.functions
25+
import warnings
2326

2427
from ldap.schema import SCHEMA_ATTRS
2528
from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples
@@ -28,6 +31,11 @@
2831

2932
from ldap import LDAPError
3033

34+
PY2 = bool(sys.version_info[0] <= 2)
35+
if PY2:
36+
text_type = unicode
37+
else:
38+
text_type = str
3139

3240
class NO_UNIQUE_ENTRY(ldap.NO_SUCH_OBJECT):
3341
"""
@@ -55,7 +63,7 @@ class SimpleLDAPObject:
5563

5664
def __init__(
5765
self,uri,
58-
trace_level=0,trace_file=None,trace_stack_limit=5
66+
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None
5967
):
6068
self._trace_level = trace_level
6169
self._trace_file = trace_file or sys.stdout
@@ -66,6 +74,186 @@ def __init__(
6674
self.timeout = -1
6775
self.protocol_version = ldap.VERSION3
6876

77+
# Bytes mode
78+
# ----------
79+
80+
# By default, raise a TypeError when receiving invalid args
81+
self.bytes_mode_hardfail = True
82+
if bytes_mode is None and PY2:
83+
warnings.warn(
84+
"Under Python 2, python-ldap uses bytes by default. "
85+
"This will be removed in Python 3 (no bytes for DN/RDN/field names). "
86+
"Please call initialize(..., bytes_mode=False) explicitly.",
87+
BytesWarning,
88+
stacklevel=2,
89+
)
90+
bytes_mode = True
91+
# Disable hard failure when running in backwards compatibility mode.
92+
self.bytes_mode_hardfail = False
93+
elif bytes_mode and not PY2:
94+
raise ValueError("bytes_mode is *not* supported under Python 3.")
95+
# On by default on Py2, off on Py3.
96+
self.bytes_mode = bytes_mode
97+
98+
def _bytesify_input(self, value):
99+
"""Adapt a value following bytes_mode in Python 2.
100+
101+
In Python 3, returns the original value unmodified.
102+
103+
With bytes_mode ON, takes bytes or None and returns bytes or None.
104+
With bytes_mode OFF, takes unicode or None and returns bytes or None.
105+
106+
This function should be applied on all text inputs (distinguished names
107+
and attribute names in modlists) to convert them to the bytes expected
108+
by the C bindings.
109+
"""
110+
if not PY2:
111+
return value
112+
113+
if value is None:
114+
return value
115+
elif self.bytes_mode:
116+
if isinstance(value, bytes):
117+
return value
118+
else:
119+
if self.bytes_mode_hardfail:
120+
raise TypeError("All provided fields *must* be bytes when bytes mode is on; got %r" % (value,))
121+
else:
122+
warnings.warn(
123+
"Received non-bytes value %r with default (disabled) bytes mode; please choose an explicit "
124+
"option for bytes_mode on your LDAP connection" % (value,),
125+
BytesWarning,
126+
stacklevel=6,
127+
)
128+
return value.encode('utf-8')
129+
else:
130+
if not isinstance(value, text_type):
131+
raise TypeError("All provided fields *must* be text when bytes mode is off; got %r" % (value,))
132+
assert not isinstance(value, bytes)
133+
return value.encode('utf-8')
134+
135+
def _bytesify_inputs(self, *values):
136+
"""Adapt values following bytes_mode.
137+
138+
Applies _bytesify_input on each arg.
139+
140+
Usage:
141+
>>> a, b, c = self._bytesify_inputs(a, b, c)
142+
"""
143+
if not PY2:
144+
return values
145+
return (
146+
self._bytesify_input(value)
147+
for value in values
148+
)
149+
150+
def _bytesify_modlist(self, modlist, with_opcode):
151+
"""Adapt a modlist according to bytes_mode.
152+
153+
A modlist is a tuple of (op, attr, value), where:
154+
- With bytes_mode ON, attr is checked to be bytes
155+
- With bytes_mode OFF, attr is converted from unicode to bytes
156+
- value is *always* bytes
157+
"""
158+
if not PY2:
159+
return modlist
160+
if with_opcode:
161+
return tuple(
162+
(op, self._bytesify_input(attr), val)
163+
for op, attr, val in modlist
164+
)
165+
else:
166+
return tuple(
167+
(self._bytesify_input(attr), val)
168+
for attr, val in modlist
169+
)
170+
171+
def _unbytesify_text_value(self, value):
172+
"""Adapt a 'known text, UTF-8 encoded' returned value following bytes_mode.
173+
174+
With bytes_mode ON, takes bytes or None and returns bytes or None.
175+
With bytes_mode OFF, takes bytes or None and returns unicode or None.
176+
177+
This function should only be applied on field *values*; distinguished names
178+
or field *names* are already natively handled in result4.
179+
"""
180+
if value is None:
181+
return value
182+
183+
# Preserve logic of assertions only under Python 2
184+
if PY2:
185+
assert isinstance(value, bytes), "Expected bytes value, got text instead (%r)" % (value,)
186+
187+
if self.bytes_mode:
188+
return value
189+
else:
190+
return value.decode('utf-8')
191+
192+
def _maybe_rebytesify_text(self, value):
193+
"""Re-encodes text to bytes if needed by bytes_mode.
194+
195+
Takes unicode (and checks for it), and returns:
196+
- bytes under bytes_mode
197+
- unicode otherwise.
198+
"""
199+
if not PY2:
200+
return value
201+
202+
if value is None:
203+
return value
204+
205+
assert isinstance(value, text_type), "Should return text, got bytes instead (%r)" % (value,)
206+
if not self.bytes_mode:
207+
return value
208+
else:
209+
return value.encode('utf-8')
210+
211+
def _bytesify_result_value(self, result_value):
212+
"""Applies bytes_mode to a result value.
213+
214+
Such a value can either be:
215+
- a dict mapping an attribute name to its list of values
216+
(where attribute names are unicode and values bytes)
217+
- a list of referals (which are unicode)
218+
"""
219+
if not PY2:
220+
return result_value
221+
if hasattr(result_value, 'items'):
222+
# It's a attribute_name: [values] dict
223+
return dict(
224+
(self._maybe_rebytesify_text(key), value)
225+
for (key, value) in result_value.items()
226+
)
227+
elif isinstance(result_value, bytes):
228+
return result_value
229+
else:
230+
# It's a list of referals
231+
# Example value:
232+
# [u'ldap://DomainDnsZones.xxxx.root.local/DC=DomainDnsZones,DC=xxxx,DC=root,DC=local']
233+
return [self._maybe_rebytesify_text(referal) for referal in result_value]
234+
235+
def _bytesify_results(self, results, with_ctrls=False):
236+
"""Converts a "results" object according to bytes_mode.
237+
238+
Takes:
239+
- a list of (dn, {field: [values]}) if with_ctrls is False
240+
- a list of (dn, {field: [values]}, ctrls) if with_ctrls is True
241+
242+
And, if bytes_mode is on, converts dn and fields to bytes.
243+
"""
244+
if not PY2:
245+
return results
246+
if with_ctrls:
247+
return [
248+
(self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields), ctrls)
249+
for (dn, fields, ctrls) in results
250+
]
251+
else:
252+
return [
253+
(self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields))
254+
for (dn, fields) in results
255+
]
256+
69257
def _ldap_lock(self,desc=''):
70258
if ldap.LIBLDAP_R:
71259
return ldap.LDAPLock(desc='%s within %s' %(desc,repr(self)))
@@ -185,6 +373,8 @@ def add_ext(self,dn,modlist,serverctrls=None,clientctrls=None):
185373
The parameter modlist is similar to the one passed to modify(),
186374
except that no operation integer need be included in the tuples.
187375
"""
376+
dn = self._bytesify_input(dn)
377+
modlist = self._bytesify_modlist(modlist, with_opcode=False)
188378
return self._ldap_call(self._l.add_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
189379

190380
def add_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None):
@@ -209,6 +399,7 @@ def simple_bind(self,who='',cred='',serverctrls=None,clientctrls=None):
209399
"""
210400
simple_bind([who='' [,cred='']]) -> int
211401
"""
402+
who, cred = self._bytesify_inputs(who, cred)
212403
return self._ldap_call(self._l.simple_bind,who,cred,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
213404

214405
def simple_bind_s(self,who='',cred='',serverctrls=None,clientctrls=None):
@@ -285,6 +476,7 @@ def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None):
285476
A design bug in the library prevents value from containing
286477
nul characters.
287478
"""
479+
dn, attr = self._bytesify_inputs(dn, attr)
288480
return self._ldap_call(self._l.compare_ext,dn,attr,value,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
289481

290482
def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None):
@@ -315,6 +507,7 @@ def delete_ext(self,dn,serverctrls=None,clientctrls=None):
315507
form returns the message id of the initiated request, and the
316508
result can be obtained from a subsequent call to result().
317509
"""
510+
dn = self._bytesify_input(dn)
318511
return self._ldap_call(self._l.delete_ext,dn,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
319512

320513
def delete_ext_s(self,dn,serverctrls=None,clientctrls=None):
@@ -363,6 +556,8 @@ def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None):
363556
"""
364557
modify_ext(dn, modlist[,serverctrls=None[,clientctrls=None]]) -> int
365558
"""
559+
dn = self._bytesify_input(dn)
560+
modlist = self._bytesify_modlist(modlist, with_opcode=True)
366561
return self._ldap_call(self._l.modify_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
367562

368563
def modify_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None):
@@ -416,6 +611,7 @@ def modrdn_s(self,dn,newrdn,delold=1):
416611
return self.rename_s(dn,newrdn,None,delold)
417612

418613
def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None):
614+
user, oldpw, newpw = self._bytesify_inputs(user, oldpw, newpw)
419615
return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
420616

421617
def passwd_s(self,user,oldpw,newpw,serverctrls=None,clientctrls=None):
@@ -437,6 +633,7 @@ def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls
437633
This actually corresponds to the rename* routines in the
438634
LDAP-EXT C API library.
439635
"""
636+
dn, newrdn, newsuperior = self._bytesify_inputs(dn, newrdn, newsuperior)
440637
return self._ldap_call(self._l.rename,dn,newrdn,newsuperior,delold,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
441638

442639
def rename_s(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None):
@@ -525,6 +722,8 @@ def result4(self,msgid=ldap.RES_ANY,all=1,timeout=None,add_ctrls=0,add_intermedi
525722
if add_ctrls:
526723
resp_data = [ (t,r,DecodeControlTuples(c,resp_ctrl_classes)) for t,r,c in resp_data ]
527724
decoded_resp_ctrls = DecodeControlTuples(resp_ctrls,resp_ctrl_classes)
725+
if resp_data is not None:
726+
resp_data = self._bytesify_results(resp_data, with_ctrls=add_ctrls)
528727
return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value
529728

530729
def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0):
@@ -572,6 +771,9 @@ def search_ext(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrson
572771
The amount of search results retrieved can be limited with the
573772
sizelimit parameter if non-zero.
574773
"""
774+
base, filterstr = self._bytesify_inputs(base, filterstr)
775+
if attrlist is not None:
776+
attrlist = tuple(self._bytesify_inputs(*attrlist))
575777
return self._ldap_call(
576778
self._l.search_ext,
577779
base,scope,filterstr,
@@ -665,6 +867,8 @@ def search_subschemasubentry_s(self,dn=''):
665867
666868
None as result indicates that the DN of the sub schema sub entry could
667869
not be determined.
870+
871+
Returns: None or text/bytes depending on bytes_mode.
668872
"""
669873
try:
670874
r = self.search_s(
@@ -686,7 +890,9 @@ def search_subschemasubentry_s(self,dn=''):
686890
# If dn was already root DSE we can return here
687891
return None
688892
else:
689-
return search_subschemasubentry_dn
893+
# With legacy bytes mode, return bytes; otherwise, since this is a DN,
894+
# RFCs impose that the field value *can* be decoded to UTF-8.
895+
return self._unbytesify_text_value(search_subschemasubentry_dn)
690896
except IndexError:
691897
return None
692898

@@ -788,7 +994,7 @@ class ReconnectLDAPObject(SimpleLDAPObject):
788994

789995
def __init__(
790996
self,uri,
791-
trace_level=0,trace_file=None,trace_stack_limit=5,
997+
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
792998
retry_max=1,retry_delay=60.0
793999
):
7941000
"""
@@ -803,7 +1009,7 @@ def __init__(
8031009
self._uri = uri
8041010
self._options = []
8051011
self._last_bind = None
806-
SimpleLDAPObject.__init__(self,uri,trace_level,trace_file,trace_stack_limit)
1012+
SimpleLDAPObject.__init__(self,uri,trace_level,trace_file,trace_stack_limit,bytes_mode)
8071013
self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self)))
8081014
self._retry_max = retry_max
8091015
self._retry_delay = retry_delay

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy