Skip to content

Commit e6a0a38

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

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

@@ -1086,7 +1093,7 @@ class ReconnectLDAPObject(SimpleLDAPObject):
10861093
def __init__(
10871094
self,uri,
10881095
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
1089-
bytes_strictness=None, retry_max=1, retry_delay=60.0
1096+
bytes_strictness=None, retry_max=1, retry_delay=60.0, fileno=None
10901097
):
10911098
"""
10921099
Parameters like SimpleLDAPObject.__init__() with these
@@ -1102,7 +1109,8 @@ def __init__(
11021109
self._last_bind = None
11031110
SimpleLDAPObject.__init__(self, uri, trace_level, trace_file,
11041111
trace_stack_limit, bytes_mode,
1105-
bytes_strictness=bytes_strictness)
1112+
bytes_strictness=bytes_strictness,
1113+
fileno=fileno)
11061114
self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self)))
11071115
self._retry_max = retry_max
11081116
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
@@ -754,5 +758,28 @@ def test105_reconnect_restore(self):
754758
self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn)
755759

756760

761+
class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject):
762+
def _get_bytes_ldapobject(self, explicit=True, **kwargs):
763+
raise unittest.SkipTest("Test opens two sockets")
764+
765+
def _search_wrong_type(self, bytes_mode, strictness):
766+
raise unittest.SkipTest("Test opens two sockets")
767+
768+
def _open_ldap_conn(self, who=None, cred=None, **kwargs):
769+
if hasattr(self, '_sock'):
770+
raise RuntimeError("socket already connected")
771+
self._sock = socket.create_connection(
772+
(self.server.hostname, self.server.port)
773+
)
774+
return super(Test03_SimpleLDAPObjectWithFileno, self)._open_ldap_conn(
775+
who=who, cred=cred, fileno=self._sock.fileno(), **kwargs
776+
)
777+
778+
def tearDown(self):
779+
self._sock.close()
780+
del self._sock
781+
super(Test03_SimpleLDAPObjectWithFileno, self).tearDown()
782+
783+
757784
if __name__ == '__main__':
758785
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)