Skip to content

Commit 1685038

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 7e084ae commit 1685038

File tree

10 files changed

+185
-16
lines changed

10 files changed

+185
-16
lines changed

Doc/reference/ldap.rst

Lines changed: 9 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,14 @@ 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 the socket file descriptor will be used
44+
to connect to an LDAP server. The socket fd must already be connected. The
45+
:class:`~ldap.ldapobject.LDAPObject` does not take ownership of the file
46+
descriptor. It must be kept open and closed after the
47+
:class:`~ldap.ldapobject.LDAPObject` is unbound. The connection type is
48+
determined from the URI, ``TCP`` for ``ldap://`` / ``ldaps://``, ``IPC``
49+
for ``ldapi://``.
50+
4351
Note that internally the OpenLDAP function
4452
`ldap_initialize(3) <https://www.openldap.org/software/man.cgi?query=ldap_init&sektion=3>`_
4553
is called which just initializes the LDAP connection struct in the C API

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/LDAPObject.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Tuple_to_LDAPMod(PyObject *tup, int no_op)
126126
}
127127

128128
lm = PyMem_NEW(LDAPMod, 1);
129+
129130
if (lm == NULL)
130131
goto nomem;
131132

@@ -236,6 +237,7 @@ List_to_LDAPMods(PyObject *list, int no_op)
236237
}
237238

238239
lms = PyMem_NEW(LDAPMod *, len + 1);
240+
239241
if (lms == NULL)
240242
goto nomem;
241243

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+
#define LDAP_PROTO_EXT 4
46+
#endif
47+
48+
extern int
49+
ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url, LDAP **ldp);
50+
51+
static PyObject *
52+
l_ldap_initialize_fd(PyObject *unused, PyObject *args)
53+
{
54+
char *url;
55+
LDAP *ld = NULL;
56+
int ret;
57+
int fd;
58+
int proto = -1;
59+
LDAPURLDesc *lud = NULL;
60+
61+
PyThreadState *save;
62+
63+
if (!PyArg_ParseTuple(args, "is:initialize_fd", &fd, &url))
64+
return NULL;
65+
66+
/* Get LDAP protocol from scheme */
67+
ret = ldap_url_parse(url, &lud);
68+
if (ret != LDAP_SUCCESS)
69+
return LDAPerr(ret);
70+
71+
if (strcmp(lud->lud_scheme, "ldap") == 0) {
72+
proto = LDAP_PROTO_TCP;
73+
}
74+
else if (strcmp(lud->lud_scheme, "ldaps") == 0) {
75+
proto = LDAP_PROTO_TCP;
76+
}
77+
else if (strcmp(lud->lud_scheme, "ldapi") == 0) {
78+
proto = LDAP_PROTO_IPC;
79+
}
80+
#ifdef LDAP_CONNECTIONLESS
81+
else if (strcmp(lud->lud_scheme, "cldap") == 0) {
82+
proto = LDAP_PROTO_UDP;
83+
}
84+
#endif
85+
else {
86+
ldap_free_urldesc(lud);
87+
PyErr_SetString(PyExc_ValueError, "unsupported URL scheme");
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, "ldap_initialize");
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},

Modules/ldapcontrol.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Tuple_to_LDAPControl(PyObject *tup)
8282
return NULL;
8383

8484
lc = PyMem_NEW(LDAPControl, 1);
85+
8586
if (lc == NULL) {
8687
PyErr_NoMemory();
8788
return NULL;
@@ -91,6 +92,7 @@ Tuple_to_LDAPControl(PyObject *tup)
9192

9293
len = strlen(oid);
9394
lc->ldctl_oid = PyMem_NEW(char, len + 1);
95+
9496
if (lc->ldctl_oid == NULL) {
9597
PyErr_NoMemory();
9698
LDAPControl_DEL(lc);
@@ -137,6 +139,7 @@ LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret)
137139

138140
len = PySequence_Length(list);
139141
ldcs = PyMem_NEW(LDAPControl *, len + 1);
142+
140143
if (ldcs == NULL) {
141144
PyErr_NoMemory();
142145
return 0;

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: 16 additions & 1 deletion
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
@@ -674,7 +675,7 @@ def test_compare_s_invalidattr(self):
674675
result = l.compare_s('cn=Foo1,%s' % base, 'invalidattr', b'invalid')
675676

676677

677-
class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
678+
class XXXTest01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
678679
"""
679680
test ReconnectLDAPObject by restarting slapd
680681
"""
@@ -754,5 +755,19 @@ def test105_reconnect_restore(self):
754755
self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn)
755756

756757

758+
class Test02_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject):
759+
def _open_ldap_conn(self, who=None, cred=None, **kwargs):
760+
self._sock = socket.create_connection(
761+
(self.server.hostname, self.server.port)
762+
)
763+
return super(Test02_SimpleLDAPObjectWithFileno, self)._open_ldap_conn(
764+
who=who, cred=cred, fileno=self._sock.fileno(), **kwargs
765+
)
766+
767+
def tearDown(self):
768+
self._sock.close()
769+
super(Test02_SimpleLDAPObjectWithFileno, self).tearDown()
770+
771+
757772
if __name__ == '__main__':
758773
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