Skip to content

Commit cb4eb78

Browse files
tiranencukou
authored andcommitted
Allow LDAP connection from file descriptor
``ldap.initialize()`` now takes an optional fileno argument to create an LDAP connection from a connected socket. See: python-ldap#178 Signed-off-by: Christian Heimes <cheimes@redhat.com>
1 parent 0870889 commit cb4eb78

File tree

8 files changed

+197
-15
lines changed

8 files changed

+197
-15
lines changed

Doc/reference/ldap.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Functions
2929

3030
This module defines the following functions:
3131

32-
.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None, [bytes_strictness=None]]]]]) -> LDAPObject object
32+
.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None, [bytes_strictness=None, [fileno=None]]]]]]) -> LDAPObject object
3333
3434
Initializes a new connection object for accessing the given LDAP server,
3535
and return an :class:`~ldap.ldapobject.LDAPObject` used to perform operations
@@ -40,6 +40,16 @@ This module defines the following functions:
4040
when using multiple URIs you cannot determine to which URI your client
4141
gets connected.
4242

43+
If *fileno* parameter is given then the file descriptor will be used to
44+
connect to an LDAP server. The *fileno* must either be a socket file
45+
descriptor as :class:`int` or a file-like object with a *fileno()* method
46+
that returns a socket file descriptor. The socket file descriptor must
47+
already be connected. :class:`~ldap.ldapobject.LDAPObject` does not take
48+
ownership of the file descriptor. It must be kept open during operations
49+
and explicitly closed after the :class:`~ldap.ldapobject.LDAPObject` is
50+
unbound. The internal connection type is determined from the URI, ``TCP``
51+
for ``ldap://`` / ``ldaps://``, ``IPC`` (``AF_UNIX``) for ``ldapi://``.
52+
4353
Note that internally the OpenLDAP function
4454
`ldap_initialize(3) <https://www.openldap.org/software/man.cgi?query=ldap_init&sektion=3>`_
4555
is called which just initializes the LDAP connection struct in the C API
@@ -72,6 +82,10 @@ This module defines the following functions:
7282

7383
:rfc:`4516` - Lightweight Directory Access Protocol (LDAP): Uniform Resource Locator
7484

85+
.. versionadded:: 3.3
86+
87+
The *fileno* argument was added.
88+
7589

7690
.. py:function:: get_option(option) -> int|string
7791

Lib/ldap/functions.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _ldap_function_call(lock,func,*args,**kwargs):
6767

6868
def initialize(
6969
uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None,
70-
bytes_mode=None, **kwargs
70+
bytes_mode=None, fileno=None, **kwargs
7171
):
7272
"""
7373
Return LDAPObject instance by opening LDAP connection to
@@ -84,12 +84,17 @@ def initialize(
8484
Default is to use stdout.
8585
bytes_mode
8686
Whether to enable :ref:`bytes_mode` for backwards compatibility under Py2.
87+
fileno
88+
If not None the socket file descriptor is used to connect to an
89+
LDAP server.
8790
8891
Additional keyword arguments (such as ``bytes_strictness``) are
8992
passed to ``LDAPObject``.
9093
"""
9194
return LDAPObject(
92-
uri, trace_level, trace_file, trace_stack_limit, bytes_mode, **kwargs)
95+
uri, trace_level, trace_file, trace_stack_limit, bytes_mode,
96+
fileno=fileno, **kwargs
97+
)
9398

9499

95100
def get_option(option):

Lib/ldap/ldapobject.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,21 @@ class SimpleLDAPObject:
9696
def __init__(
9797
self,uri,
9898
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
99-
bytes_strictness=None,
99+
bytes_strictness=None, fileno=None
100100
):
101101
self._trace_level = trace_level or ldap._trace_level
102102
self._trace_file = trace_file or ldap._trace_file
103103
self._trace_stack_limit = trace_stack_limit
104104
self._uri = uri
105105
self._ldap_object_lock = self._ldap_lock('opcall')
106-
self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri)
106+
if fileno is not None:
107+
if hasattr(fileno, "fileno"):
108+
fileno = fileno.fileno()
109+
self._l = ldap.functions._ldap_function_call(
110+
ldap._ldap_module_lock, _ldap.initialize_fd, fileno, uri
111+
)
112+
else:
113+
self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri)
107114
self.timeout = -1
108115
self.protocol_version = ldap.VERSION3
109116

@@ -1093,7 +1100,7 @@ class ReconnectLDAPObject(SimpleLDAPObject):
10931100
def __init__(
10941101
self,uri,
10951102
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
1096-
bytes_strictness=None, retry_max=1, retry_delay=60.0
1103+
bytes_strictness=None, retry_max=1, retry_delay=60.0, fileno=None
10971104
):
10981105
"""
10991106
Parameters like SimpleLDAPObject.__init__() with these
@@ -1109,7 +1116,8 @@ def __init__(
11091116
self._last_bind = None
11101117
SimpleLDAPObject.__init__(self, uri, trace_level, trace_file,
11111118
trace_stack_limit, bytes_mode,
1112-
bytes_strictness=bytes_strictness)
1119+
bytes_strictness=bytes_strictness,
1120+
fileno=fileno)
11131121
self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self)))
11141122
self._retry_max = retry_max
11151123
self._retry_delay = retry_delay

Lib/slapdtest/_slapdtest.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class SlapdObject(object):
179179
root_cn = 'Manager'
180180
root_pw = 'password'
181181
slapd_loglevel = 'stats stats2'
182-
local_host = '127.0.0.1'
182+
local_host = LOCALHOST
183183
testrunsubdirs = (
184184
'schema',
185185
)
@@ -214,7 +214,7 @@ def __init__(self):
214214
self._schema_prefix = os.path.join(self.testrundir, 'schema')
215215
self._slapd_conf = os.path.join(self.testrundir, 'slapd.conf')
216216
self._db_directory = os.path.join(self.testrundir, "openldap-data")
217-
self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port)
217+
self.ldap_uri = "ldap://%s:%d/" % (self.local_host, self._port)
218218
if HAVE_LDAPI:
219219
ldapi_path = os.path.join(self.testrundir, 'ldapi')
220220
self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path)
@@ -243,6 +243,14 @@ def __init__(self):
243243
def root_dn(self):
244244
return 'cn={self.root_cn},{self.suffix}'.format(self=self)
245245

246+
@property
247+
def hostname(self):
248+
return self.local_host
249+
250+
@property
251+
def port(self):
252+
return self._port
253+
246254
def _find_commands(self):
247255
self.PATH_LDAPADD = self._find_command('ldapadd')
248256
self.PATH_LDAPDELETE = self._find_command('ldapdelete')

Modules/functions.c

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,75 @@ l_ldap_initialize(PyObject *unused, PyObject *args)
3030
return (PyObject *)newLDAPObject(ld);
3131
}
3232

33+
#ifdef HAVE_LDAP_INIT_FD
34+
35+
/* initialize_fd(fileno, url)
36+
*
37+
* ldap_init_fd() is not a private API but it's not in a public header either
38+
* SSSD has been using the function for a while, so it's probably OK.
39+
*/
40+
41+
#ifndef LDAP_PROTO_TCP
42+
#define LDAP_PROTO_TCP 1
43+
#define LDAP_PROTO_UDP 2
44+
#define LDAP_PROTO_IPC 3
45+
#endif
46+
47+
extern int
48+
ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url, LDAP **ldp);
49+
50+
static PyObject *
51+
l_ldap_initialize_fd(PyObject *unused, PyObject *args)
52+
{
53+
char *url;
54+
LDAP *ld = NULL;
55+
int ret;
56+
int fd;
57+
int proto = -1;
58+
LDAPURLDesc *lud = NULL;
59+
60+
PyThreadState *save;
61+
62+
if (!PyArg_ParseTuple(args, "is:initialize_fd", &fd, &url))
63+
return NULL;
64+
65+
/* Get LDAP protocol from scheme */
66+
ret = ldap_url_parse(url, &lud);
67+
if (ret != LDAP_SUCCESS)
68+
return LDAPerr(ret);
69+
70+
if (strcmp(lud->lud_scheme, "ldap") == 0) {
71+
proto = LDAP_PROTO_TCP;
72+
}
73+
else if (strcmp(lud->lud_scheme, "ldaps") == 0) {
74+
proto = LDAP_PROTO_TCP;
75+
}
76+
else if (strcmp(lud->lud_scheme, "ldapi") == 0) {
77+
proto = LDAP_PROTO_IPC;
78+
}
79+
#ifdef LDAP_CONNECTIONLESS
80+
else if (strcmp(lud->lud_scheme, "cldap") == 0) {
81+
proto = LDAP_PROTO_UDP;
82+
}
83+
#endif
84+
else {
85+
ldap_free_urldesc(lud);
86+
PyErr_SetString(PyExc_ValueError, "unsupported URL scheme");
87+
return NULL;
88+
}
89+
ldap_free_urldesc(lud);
90+
91+
save = PyEval_SaveThread();
92+
ret = ldap_init_fd((ber_socket_t) fd, proto, url, &ld);
93+
PyEval_RestoreThread(save);
94+
95+
if (ret != LDAP_SUCCESS)
96+
return LDAPerror(ld);
97+
98+
return (PyObject *)newLDAPObject(ld);
99+
}
100+
#endif /* HAVE_LDAP_INIT_FD */
101+
33102
/* ldap_str2dn */
34103

35104
static PyObject *
@@ -137,6 +206,9 @@ l_ldap_get_option(PyObject *self, PyObject *args)
137206

138207
static PyMethodDef methods[] = {
139208
{"initialize", (PyCFunction)l_ldap_initialize, METH_VARARGS},
209+
#ifdef HAVE_LDAP_INIT_FD
210+
{"initialize_fd", (PyCFunction)l_ldap_initialize_fd, METH_VARARGS},
211+
#endif
140212
{"str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS},
141213
{"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS},
142214
{"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS},

Tests/t_cext.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
from __future__ import unicode_literals
99

10+
import contextlib
1011
import errno
1112
import os
13+
import socket
1214
import unittest
1315

1416
# Switch off processing .ldaprc or ldap.conf before importing _ldap
@@ -92,14 +94,35 @@ def _open_conn(self, bind=True):
9294
"""
9395
l = _ldap.initialize(self.server.ldap_uri)
9496
if bind:
95-
# Perform a simple bind
96-
l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3)
97-
m = l.simple_bind(self.server.root_dn, self.server.root_pw)
98-
result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout)
99-
self.assertEqual(result, _ldap.RES_BIND)
100-
self.assertEqual(type(msgid), type(0))
97+
self._bind_conn(l)
10198
return l
10299

100+
@contextlib.contextmanager
101+
def _open_conn_fd(self, bind=True):
102+
sock = socket.create_connection(
103+
(self.server.hostname, self.server.port)
104+
)
105+
try:
106+
l = _ldap.initialize_fd(sock.fileno(), self.server.ldap_uri)
107+
if bind:
108+
self._bind_conn(l)
109+
yield sock, l
110+
finally:
111+
try:
112+
sock.close()
113+
except OSError:
114+
# already closed
115+
pass
116+
117+
def _bind_conn(self, l):
118+
# Perform a simple bind
119+
l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3)
120+
m = l.simple_bind(self.server.root_dn, self.server.root_pw)
121+
result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout)
122+
self.assertEqual(result, _ldap.RES_BIND)
123+
self.assertEqual(type(msgid), type(0))
124+
125+
103126
# Test for the existence of a whole bunch of constants
104127
# that the C module is supposed to export
105128
def test_constants(self):
@@ -224,6 +247,30 @@ def test_test_flags(self):
224247
def test_simple_bind(self):
225248
l = self._open_conn()
226249

250+
def test_simple_bind_fileno(self):
251+
with self._open_conn_fd() as (sock, l):
252+
self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn)
253+
254+
def test_simple_bind_fileno_invalid(self):
255+
with open(os.devnull) as f:
256+
l = _ldap.initialize_fd(f.fileno(), self.server.ldap_uri)
257+
with self.assertRaises(_ldap.SERVER_DOWN):
258+
self._bind_conn(l)
259+
260+
def test_simple_bind_fileno_closed(self):
261+
with self._open_conn_fd() as (sock, l):
262+
self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn)
263+
sock.close()
264+
with self.assertRaises(_ldap.SERVER_DOWN):
265+
l.whoami_s()
266+
267+
def test_simple_bind_fileno_rebind(self):
268+
with self._open_conn_fd() as (sock, l):
269+
self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn)
270+
l.unbind_ext()
271+
with self.assertRaises(_ldap.LDAPError):
272+
self._bind_conn(l)
273+
227274
def test_simple_anonymous_bind(self):
228275
l = self._open_conn(bind=False)
229276
m = l.simple_bind("", "")

Tests/t_ldapobject.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import contextlib
2121
import linecache
2222
import os
23+
import socket
2324
import unittest
2425
import warnings
2526
import pickle
@@ -103,6 +104,9 @@ def setUp(self):
103104
# open local LDAP connection
104105
self._ldap_conn = self._open_ldap_conn(bytes_mode=False)
105106

107+
def tearDown(self):
108+
del self._ldap_conn
109+
106110
def test_reject_bytes_base(self):
107111
base = self.server.suffix
108112
l = self._ldap_conn
@@ -807,5 +811,28 @@ def test105_reconnect_restore(self):
807811
self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn)
808812

809813

814+
class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject):
815+
def _get_bytes_ldapobject(self, explicit=True, **kwargs):
816+
raise unittest.SkipTest("Test opens two sockets")
817+
818+
def _search_wrong_type(self, bytes_mode, strictness):
819+
raise unittest.SkipTest("Test opens two sockets")
820+
821+
def _open_ldap_conn(self, who=None, cred=None, **kwargs):
822+
if hasattr(self, '_sock'):
823+
raise RuntimeError("socket already connected")
824+
self._sock = socket.create_connection(
825+
(self.server.hostname, self.server.port)
826+
)
827+
return super(Test03_SimpleLDAPObjectWithFileno, self)._open_ldap_conn(
828+
who=who, cred=cred, fileno=self._sock.fileno(), **kwargs
829+
)
830+
831+
def tearDown(self):
832+
self._sock.close()
833+
del self._sock
834+
super(Test03_SimpleLDAPObjectWithFileno, self).tearDown()
835+
836+
810837
if __name__ == '__main__':
811838
unittest.main()

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class OpenLDAP2:
145145
('LDAPMODULE_VERSION', pkginfo.__version__),
146146
('LDAPMODULE_AUTHOR', pkginfo.__author__),
147147
('LDAPMODULE_LICENSE', pkginfo.__license__),
148+
('HAVE_LDAP_INIT_FD', None),
148149
]
149150
),
150151
],

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