diff --git a/.coveragerc b/.coveragerc
index fc432b9..738d86f 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,9 +1,10 @@
 [run]
 branch = True
 source =
-    Lib/
-omit =
-    Lib/slapdtest.py
+   ldap
+   ldif
+   ldapurl
+   slapdtest
 
 [paths]
 source =
diff --git a/CHANGES b/CHANGES
index 6e37016..e129fff 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,63 @@
+Released 3.3.1 2020-06-29
+
+Changes:
+* On MacOS, remove option to make LDAP connections from a file descriptor
+  when built wit the system libldap (which lacks the underlying function,
+  ``ldap_init_fd``)
+
+
+----------------------------------------------------------------
+Released 3.3.0 2020-06-18
+
+Highlights:
+* ``LDAPError`` now contains additional fields, such as ctrls, result, msgid
+* ``passwd_s`` can now extract the newly generated password
+* LDAP connections can now be made from a file descriptor
+
+This release is tested on Python 3.8, and the beta of Python 3.9.
+
+The following undocumented functions are deprecated and scheduled for removal:
+- ``ldap.cidict.strlist_intersection``
+- ``ldap.cidict.strlist_minus``
+- ``ldap.cidict.strlist_union``
+
+Modules/
+* Ensure ReconnectLDAPObject is not left in an inconsistent state after
+  a reconnection timeout
+* Syncrepl now correctly parses SyncInfoMessage when the message is a syncIdSet
+* Release GIL around global get/set option call
+* Do not leak serverctrls in result functions
+* Don't overallocate memory in attrs_from_List()
+* Fix thread support check for Python 3
+* With OpenLDAP 2.4.48, use the new header openldap.h
+
+Lib/
+* Fix some edge cases regarding quoting in the schema tokenizer
+* Fix escaping a single space in ldap.escape_dn_chars
+* Fix string formatting in ldap.compare_ext_s
+* Prefer iterating dict instead of calling dict.keys()
+
+Doc/
+* Clarify the relationship between initialize() and LDAPObject()
+* Improve documentation of TLS options
+* Update FAQ to include Samba AD-DC error message
+  "Operation unavailable without authentication"
+* Fix several incorrect examples and demos
+  (but note that these are not yet tested)
+* Update Debian installation instructions for Debian Buster
+* Typo fixes in docs and docstrings
+
+Test/
+* Test and document error cases in ldap.compare_s
+* Test if reconnection is done after connection loss
+* Make test certificates valid for the far future
+* Use slapd -Tt instead of slaptest
+
+Infrastructure:
+* Mark the LICENCE file as a license for setuptools
+* Use "unittest discover" rather than "setup.py test" to run tests
+
+
 ----------------------------------------------------------------
 Released 3.2.0 2019-03-13
 
diff --git a/Demo/pyasn1/readentrycontrol.py b/Demo/pyasn1/readentrycontrol.py
index 10faa2b..a857be2 100644
--- a/Demo/pyasn1/readentrycontrol.py
+++ b/Demo/pyasn1/readentrycontrol.py
@@ -39,8 +39,8 @@ msg_id = l.add_ext(
   serverctrls = [pr]
 )
 _,_,_,resp_ctrls = l.result3(msg_id)
-print("resp_ctrls[0].dn:",resp_ctrls[0].dn)
-print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry))
+print("resp_ctrls[0].dn:", resp_ctrls[0].dn)
+print("resp_ctrls[0].entry:", pprint.pformat(resp_ctrls[0].entry))
 
 print("""#---------------------------------------------------------------------------
 # Modify entry
@@ -56,7 +56,7 @@ msg_id = l.modify_ext(
 )
 _,_,_,resp_ctrls = l.result3(msg_id)
 print("resp_ctrls[0].dn:",resp_ctrls[0].dn)
-print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry))
+print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry))
 
 pr = PostReadControl(criticality=True,attrList=['uidNumber','gidNumber','entryCSN'])
 
@@ -67,7 +67,7 @@ msg_id = l.modify_ext(
 )
 _,_,_,resp_ctrls = l.result3(msg_id)
 print("resp_ctrls[0].dn:",resp_ctrls[0].dn)
-print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry))
+print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry))
 
 print("""#---------------------------------------------------------------------------
 # Rename entry
@@ -83,7 +83,7 @@ msg_id = l.rename(
 )
 _,_,_,resp_ctrls = l.result3(msg_id)
 print("resp_ctrls[0].dn:",resp_ctrls[0].dn)
-print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry))
+print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry))
 
 pr = PreReadControl(criticality=True,attrList=['uid'])
 msg_id = l.rename(
@@ -94,7 +94,7 @@ msg_id = l.rename(
 )
 _,_,_,resp_ctrls = l.result3(msg_id)
 print("resp_ctrls[0].dn:",resp_ctrls[0].dn)
-print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry))
+print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry))
 
 print("""#---------------------------------------------------------------------------
 # Delete entry
@@ -108,4 +108,4 @@ msg_id = l.delete_ext(
 )
 _,_,_,resp_ctrls = l.result3(msg_id)
 print("resp_ctrls[0].dn:",resp_ctrls[0].dn)
-print("resp_ctrls[0].entry:";pprint.pprint(resp_ctrls[0].entry))
+print("resp_ctrls[0].entry:",pprint.pformat(resp_ctrls[0].entry))
diff --git a/Demo/pyasn1/sss_highest_number.py b/Demo/pyasn1/sss_highest_number.py
index 5f5bdc5..020dcdb 100644
--- a/Demo/pyasn1/sss_highest_number.py
+++ b/Demo/pyasn1/sss_highest_number.py
@@ -38,9 +38,9 @@ for id_attr in ('uidNumber','gidNumber'):
   except ldap.SIZELIMIT_EXCEEDED:
     pass
   # print result
-  print 'Highest value of %s' % (id_attr)
+  print('Highest value of %s' % (id_attr))
   if ldap_result:
     dn,entry = ldap_result[0]
-    print '->',entry[id_attr]
+    print('->',entry[id_attr])
   else:
-    print 'not found'
+    print('not found')
diff --git a/Demo/schema_tree.py b/Demo/schema_tree.py
index 648bb86..bda5f64 100644
--- a/Demo/schema_tree.py
+++ b/Demo/schema_tree.py
@@ -15,9 +15,9 @@ def PrintSchemaTree(schema,se_class,se_tree,se_oid,level):
   """ASCII text output for console"""
   se_obj = schema.get_obj(se_class,se_oid)
   if se_obj!=None:
-    print('|    '*(level-1)+'+---'*(level>0), \)
-          ', '.join(se_obj.names), \
-          '(%s)' % se_obj.oid
+    print('|    '*(level-1)+'+---'*(level>0),
+          ', '.join(se_obj.names),
+          '(%s)' % se_obj.oid)
   for sub_se_oid in se_tree[se_oid]:
     print('|    '*(level+1))
     PrintSchemaTree(schema,se_class,se_tree,sub_se_oid,level+1)
diff --git a/Doc/bytes_mode.rst b/Doc/bytes_mode.rst
index dcd3dcb..0d20745 100644
--- a/Doc/bytes_mode.rst
+++ b/Doc/bytes_mode.rst
@@ -55,7 +55,7 @@ argument to :func:`ldap.initialize`:
     Text values are represented as ``unicode``.
 
 If not given explicitly, python-ldap will default to ``bytes_mode=True``,
-but if an ``unicode`` value supplied to it, if will warn and use that value.
+but if a ``unicode`` value is supplied to it, it will warn and use that value.
 
 Backwards-compatible behavior is not scheduled for removal until Python 2
 itself reaches end of life.
diff --git a/Doc/faq.rst b/Doc/faq.rst
index c2e7e15..38a645f 100644
--- a/Doc/faq.rst
+++ b/Doc/faq.rst
@@ -45,10 +45,10 @@ That used to work, but after an upgrade it does not work anymore. Why?
     providing the full functionality.
 
 **Q**: My script bound to MS Active Directory but a a search operation results
-in the exception :exc:`ldap.OPERATIONS_ERROR` with the diagnostic messages text
-“In order to perform this operation a successful bind must be
-completed on the connection.”
-What's happening here?
+in the exception :exc:`ldap.OPERATIONS_ERROR` with the diagnostic message text
+*“In order to perform this operation a successful bind must be completed on the
+connection.”* Alternatively, a Samba 4 AD returns the diagnostic message
+*"Operation unavailable without authentication"*. What's happening here?
 
     **A**: When searching from the domain level, MS AD returns referrals (search continuations)
     for some objects to indicate to the client where to look for these objects.
diff --git a/Doc/installing.rst b/Doc/installing.rst
index 90187a9..514cf99 100644
--- a/Doc/installing.rst
+++ b/Doc/installing.rst
@@ -146,8 +146,12 @@ Debian
 Packages for building and testing::
 
    # apt-get install build-essential python3-dev python2.7-dev \
-       libldap2-dev libsasl2-dev slapd ldap-utils python-tox \
+       libldap2-dev libsasl2-dev slapd ldap-utils tox \
        lcov valgrind
+       
+.. note::
+   
+   On older releases ``tox`` was called ``python-tox``.
 
 Fedora
 ------
diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst
index b13aa6f..747502f 100644
--- a/Doc/reference/ldap.rst
+++ b/Doc/reference/ldap.rst
@@ -29,10 +29,10 @@ Functions
 
 This module defines the following functions:
 
-.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None, [bytes_strictness=None]]]]]) -> LDAPObject object
+.. 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
 
    Initializes a new connection object for accessing the given LDAP server,
-   and return an LDAP object (see :ref:`ldap-objects`) used to perform operations
+   and return an :class:`~ldap.ldapobject.LDAPObject` used to perform operations
    on that server.
 
    The *uri* parameter may be a comma- or whitespace-separated list of URIs
@@ -40,6 +40,18 @@ This module defines the following functions:
    when using multiple URIs you cannot determine to which URI your client
    gets connected.
 
+   If *fileno* parameter is given then the file descriptor will be used to
+   connect to an LDAP server. The *fileno* must either be a socket file
+   descriptor as :class:`int` or a file-like object with a *fileno()* method
+   that returns a socket file descriptor. The socket file descriptor must
+   already be connected. :class:`~ldap.ldapobject.LDAPObject` does not take
+   ownership of the file descriptor. It must be kept open during operations
+   and explicitly closed after the :class:`~ldap.ldapobject.LDAPObject` is
+   unbound. The internal connection type is determined from the URI, ``TCP``
+   for ``ldap://`` / ``ldaps://``, ``IPC`` (``AF_UNIX``) for ``ldapi://``.
+   The parameter is not available on macOS when python-ldap is compiled with system
+   libldap, see :py:const:`INIT_FD_AVAIL`.
+
    Note that internally the OpenLDAP function
    `ldap_initialize(3) <https://www.openldap.org/software/man.cgi?query=ldap_init&sektion=3>`_
    is called which just initializes the LDAP connection struct in the C API
@@ -63,12 +75,19 @@ This module defines the following functions:
    :py:const:`2` for logging the method calls with arguments and the complete results and
    :py:const:`9` for also logging the traceback of method calls.
 
-   Additional keyword arguments are passed to :class:`LDAPObject`.
+   This function is a thin wrapper around instantiating
+   :class:`~ldap.ldapobject.LDAPObject`.
+   Any additional keyword arguments are passed to ``LDAPObject``.
+   It is also fine to instantiate a ``LDAPObject`` (or a subclass) directly.
 
    .. seealso::
 
       :rfc:`4516` - Lightweight Directory Access Protocol (LDAP): Uniform Resource Locator
 
+   .. versionadded:: 3.3
+
+      The *fileno* argument was added.
+
 
 .. py:function:: get_option(option) -> int|string
 
@@ -80,6 +99,12 @@ This module defines the following functions:
    This function sets the value of the global option specified by *option* to
    *invalue*.
 
+   .. note::
+
+      Most global settings do not affect existing :py:class:`LDAPObject`
+      connections. Applications should call :py:func:`set_option()` before
+      they establish connections with :py:func:`initialize`.
+
 .. versionchanged:: 3.1
 
    The deprecated functions ``ldap.init()`` and ``ldap.open()`` were removed.
@@ -112,6 +137,12 @@ General
    Integer where a non-zero value indicates that python-ldap was built with
    support for SSL/TLS (OpenSSL or similar libs).
 
+.. py:data:: INIT_FD_AVAIL
+
+   Integer where a non-zero value indicates that python-ldap supports
+   :py:func:`initialize` from a file descriptor. The feature is generally
+   available except on macOS when python-ldap is compiled with system libldap.
+
 
 .. _ldap-options:
 
@@ -218,33 +249,164 @@ SASL options
 TLS options
 :::::::::::
 
-.. py:data:: OPT_X_TLS
+.. warning::
+
+   libldap does not materialize all TLS settings immediately. You must use
+   :py:const:`OPT_X_TLS_NEWCTX` with value ``0`` to instruct libldap to
+   apply pending TLS settings and create a new internal TLS context::
+
+      conn = ldap.initialize("ldap://ldap.example")
+      conn.set_option(ldap.OPT_X_TLS_CACERTFILE, '/path/to/ca.pem')
+      conn.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
+      conn.start_tls_s()
+      conn.simple_bind_s(dn, password)
+
+
+.. py:data:: OPT_X_TLS_NEWCTX
+
+   set and apply TLS settings to internal TLS context. Value ``0`` creates
+   a new client-side context.
+
+.. py:data:: OPT_X_TLS_PACKAGE
+
+   Get TLS implementation, known values are
+
+   * ``GnuTLS``
+   * ``MozNSS`` (Mozilla NSS)
+   * ``OpenSSL``
 
-.. py:data:: OPT_X_TLS_ALLOW
 
 .. py:data:: OPT_X_TLS_CACERTDIR
 
+   get/set path to directory with CA certs
+
 .. py:data:: OPT_X_TLS_CACERTFILE
 
+   get/set path to PEM file with CA certs
+
 .. py:data:: OPT_X_TLS_CERTFILE
 
-.. py:data:: OPT_X_TLS_CIPHER_SUITE
+   get/set path to file with PEM encoded cert for client cert authentication,
+   requires :py:const:`OPT_X_TLS_KEYFILE`.
 
-.. py:data:: OPT_X_TLS_CTX
+.. py:data:: OPT_X_TLS_KEYFILE
+
+   get/set path to file with PEM encoded key for client cert authentication,
+   requires :py:const:`OPT_X_TLS_CERTFILE`.
+
+
+.. py:data:: OPT_X_TLS_CRLCHECK
+
+   get/set certificate revocation list (CRL) check mode. CRL validation
+   requires :py:const:`OPT_X_TLS_CRLFILE`.
+
+   :py:const:`OPT_X_TLS_CRL_NONE`
+      Don't perform CRL checks
+
+   :py:const:`OPT_X_TLS_CRL_PEER`
+      Perform CRL check for peer's end entity cert.
+
+   :py:const:`OPT_X_TLS_CRL_ALL`
+      Perform CRL checks for the whole cert chain
+
+.. py:data:: OPT_X_TLS_CRLFILE
+
+   get/set path to CRL file
+
+.. py:data:: OPT_X_TLS_CRL_ALL
+
+   value for :py:const:`OPT_X_TLS_CRLCHECK`
+
+.. py:data:: OPT_X_TLS_CRL_NONE
+
+   value for :py:const:`OPT_X_TLS_CRLCHECK`
+
+.. py:data:: OPT_X_TLS_CRL_PEER
+
+   value for :py:const:`OPT_X_TLS_CRLCHECK`
+
+
+.. py:data:: OPT_X_TLS_REQUIRE_CERT
+
+   get/set validation strategy for server cert.
+
+   :py:const:`OPT_X_TLS_NEVER`
+      Don't check server cert and host name
+
+   :py:const:`OPT_X_TLS_ALLOW`
+      Used internally by slapd server.
+
+   :py:const:`OPT_X_TLS_DEMAND`
+      Validate peer cert chain and host name
+
+   :py:const:`OPT_X_TLS_HARD`
+      Same as :py:const:`OPT_X_TLS_DEMAND`
+
+.. py:data:: OPT_X_TLS_ALLOW
+
+   Value for :py:const:`OPT_X_TLS_REQUIRE_CERT`
 
 .. py:data:: OPT_X_TLS_DEMAND
 
+   Value for :py:const:`OPT_X_TLS_REQUIRE_CERT`
+
 .. py:data:: OPT_X_TLS_HARD
 
-.. py:data:: OPT_X_TLS_KEYFILE
+   Value for :py:const:`OPT_X_TLS_REQUIRE_CERT`
 
 .. py:data:: OPT_X_TLS_NEVER
 
+   Value for :py:const:`OPT_X_TLS_REQUIRE_CERT`
+
+.. py:data:: OPT_X_TLS_TRY
+
+   .. deprecated:: 3.3.0
+      This value is only used by slapd server internally. It will be removed
+      in the future.
+
+
+.. py:data:: OPT_X_TLS_CIPHER
+
+   get cipher suite name from TLS session
+
+.. py:data:: OPT_X_TLS_CIPHER_SUITE
+
+   get/set allowed cipher suites
+
+.. py:data:: OPT_X_TLS_CTX
+
+   get address of internal memory address of TLS context (**DO NOT USE**)
+
+.. py:data:: OPT_X_TLS_PEERCERT
+
+   Get peer's certificate as binary ASN.1 data structure (not supported)
+
+.. py:data:: OPT_X_TLS_PROTOCOL_MIN
+
+   get/set minimum protocol version (wire protocol version as int)
+
+   * ``0x303`` for TLS 1.2
+   * ``0x304`` for TLS 1.3
+
+.. py:data:: OPT_X_TLS_VERSION
+
+   Get negotiated TLS protocol version as string
+
 .. py:data:: OPT_X_TLS_RANDOM_FILE
 
-.. py:data:: OPT_X_TLS_REQUIRE_CERT
+   get/set path to /dev/urandom (**DO NOT USE**)
 
-.. py:data:: OPT_X_TLS_TRY
+.. py:data:: OPT_X_TLS
+
+   .. deprecated:: 3.3.0
+      The option is deprecated in OpenLDAP and should no longer be used. It
+      will be removed in the future.
+
+.. note::
+
+   OpenLDAP supports several TLS/SSL libraries. OpenSSL is the most common
+   backend. Some options may not be available when libldap uses NSS, GnuTLS,
+   or Apple's Secure Transport backend.
 
 .. _ldap-keepalive-options:
 
@@ -308,16 +470,27 @@ The module defines the following exceptions:
    are instead turned into exceptions, raised as soon an the error condition
    is detected.
 
-   The exceptions are accompanied by a dictionary possibly
-   containing an string value for the key :py:const:`desc`
-   (giving an English description of the error class)
-   and/or a string value for the key :py:const:`info`
-   (giving a string containing more information that the server may have sent).
-
-   A third possible field of this dictionary is :py:const:`matched` and
-   is set to a truncated form of the name provided or alias dereferenced
-   for the lowest entry (object or alias) that was matched.
-
+   The exceptions are accompanied by a dictionary with additional information.
+   All fields are optional and more fields may be added in the future.
+   Currently, ``python-ldap`` may set the following fields:
+
+   * ``'result'``: a numeric code of the error class.
+   * ``'desc'``: string giving a description of the error class, as provided
+     by calling OpenLDAP's ``ldap_err2string`` on the ``result``.
+   * ``'info'``: string containing more information that the server may
+     have sent. The value is server-specific: for example, the OpenLDAP server
+     may send different info messages than Active Directory or 389-DS.
+   * ``'matched'``: truncated form of the name provided or alias.
+     dereferenced for the lowest entry (object or alias) that was matched.
+   * ``'msgid'``: ID of the matching asynchronous request.
+     This can be used in asynchronous code where :py:meth:`result()` raises the
+     result of an operation as an exception. For example, this is the case for
+     :py:meth:`~LDAPObject.compare()`, always raises the boolean result as an
+     exception (:py:exc:`COMPARE_TRUE` or :py:exc:`COMPARE_FALSE`).
+   * ``'ctrls'``: list of :py:class:`ldap.controls.LDAPControl` instances
+     attached to the error.
+   * ``'errno'``: the C ``errno``, usually set by system calls or ``libc``
+     rather than the LDAP libraries.
 
 .. py:exception:: ADMINLIMIT_EXCEEDED
 
@@ -351,14 +524,14 @@ The module defines the following exceptions:
 .. py:exception:: COMPARE_FALSE
 
    A compare operation returned false.
-   (This exception should never be seen because :py:meth:`compare()` returns
-   a boolean result.)
+   (This exception should only be seen asynchronous operations, because
+   :py:meth:`~LDAPObject.compare_s()` returns a boolean result.)
 
 .. py:exception:: COMPARE_TRUE
 
    A compare operation returned true.
-   (This exception should never be seen because :py:meth:`compare()` returns
-   a boolean result.)
+   (This exception should only be seen asynchronous operations, because
+   :py:meth:`~LDAPObject.compare_s()` returns a boolean result.)
 
 .. py:exception:: CONFIDENTIALITY_REQUIRED
 
@@ -455,10 +628,6 @@ The module defines the following exceptions:
 
 .. py:exception:: NO_MEMORY
 
-.. py:exception:: NO_OBJECT_CLASS_MODS
-
-   Object class modifications are not allowed.
-
 .. py:exception:: NO_RESULTS_RETURNED
 
 .. py:exception:: NO_SUCH_ATTRIBUTE
@@ -562,6 +731,8 @@ The above exceptions are raised when a result code from an underlying API
 call does not indicate success.
 
 
+.. _ldap-warnings:
+
 Warnings
 ========
 
@@ -579,14 +750,19 @@ Warnings
 LDAPObject classes
 ==================
 
-.. py:class:: LDAPObject
+.. py:class:: ldap.ldapobject.LDAPObject
 
    Instances of :py:class:`LDAPObject` are returned by :py:func:`initialize()`.
    The connection is automatically unbound
    and closed when the LDAP object is deleted.
 
-   Internally :py:class:`LDAPObject` is set to
-   :py:class:`~ldap.ldapobject.SimpleLDAPObject` by default.
+   :py:class:`LDAPObject` is an alias of
+   :py:class:`~ldap.ldapobject.SimpleLDAPObject`, the default connection class.
+   If you wish to use a different class, instantiate it directly instead of
+   calling :func:`initialize()`.
+
+   (It is also possible, but not recommended, to change the default by setting
+   ``ldap.ldapobject.LDAPObject`` to a different class.)
 
 .. autoclass:: ldap.ldapobject.SimpleLDAPObject
 
@@ -699,7 +875,9 @@ and wait for and return with the server's result, or with
    and the value *value*. The synchronous forms returns ``True`` or ``False``.
    The asynchronous forms returns the message ID of the initiated request, and
    the result of the asynchronous compare can be obtained using
-   :py:meth:`result()`.
+   :py:meth:`result()`. The operation can fail with an exception, e.g.
+   :py:exc:`ldap.NO_SUCH_OBJECT` when *dn* does not exist or
+   :py:exc:`ldap.UNDEFINED_TYPE` for an invalid attribute.
 
    Note that the asynchronous technique yields the answer
    by raising the exception objects :py:exc:`ldap.COMPARE_TRUE` or
@@ -808,7 +986,7 @@ and wait for and return with the server's result, or with
 
 .. py:method:: LDAPObject.passwd(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> int
 
-.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> None
+.. py:method:: LDAPObject.passwd_s(user, oldpw, newpw [, serverctrls=None [, clientctrls=None] [, extract_newpw=False]]]) -> (respoid, respvalue)
 
    Perform a ``LDAP Password Modify Extended Operation`` operation
    on the entry specified by *user*.
@@ -819,6 +997,13 @@ and wait for and return with the server's result, or with
    of the specified *user* which is sometimes used when a user changes
    his own password.
 
+   *respoid* is always :py:const:`None`. *respvalue* is also
+   :py:const:`None` unless *newpw* was :py:const:`None`. This requests that
+   the server generate a new random password. If *extract_newpw* is
+   :py:const:`True`, this password is a bytes object available through
+   ``respvalue.genPasswd``, otherwise *respvalue* is the raw ASN.1 response
+   (this is deprecated and only for backwards compatibility).
+
    *serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`.
 
    The asynchronous version returns the initiated message id.
@@ -828,6 +1013,7 @@ and wait for and return with the server's result, or with
    .. seealso::
 
       :rfc:`3062` - LDAP Password Modify Extended Operation
+      :py:mod:`ldap.extop.passwd`
 
 
 
diff --git a/Doc/reference/ldif.rst b/Doc/reference/ldif.rst
index c508f7d..87dcb70 100644
--- a/Doc/reference/ldif.rst
+++ b/Doc/reference/ldif.rst
@@ -61,11 +61,11 @@ Example
 The following example demonstrates how to write LDIF output
 of an LDAP entry with :mod:`ldif` module.
 
->>> import sys,ldif
->>> entry={'objectClass':['top','person'],'cn':['Michael Stroeder'],'sn':['Stroeder']}
+>>> import sys, ldif
+>>> entry={'objectClass': [b'top', b'person'], 'cn': [b'Michael Stroeder'], 'sn': [b'Stroeder']}
 >>> dn='cn=Michael Stroeder,ou=Test'
 >>> ldif_writer=ldif.LDIFWriter(sys.stdout)
->>> ldif_writer.unparse(dn,entry)
+>>> ldif_writer.unparse(dn, entry)
 dn: cn=Michael Stroeder,ou=Test
 cn: Michael Stroeder
 objectClass: top
diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt
index 3ee0e85..fb4a990 100644
--- a/Doc/spelling_wordlist.txt
+++ b/Doc/spelling_wordlist.txt
@@ -39,6 +39,7 @@ defresult
 dereferenced
 dereferencing
 desc
+dev
 directoryOperation
 distinguished
 distributedOperation
@@ -55,6 +56,7 @@ filterstr
 filterStr
 formatOID
 func
+GPG
 Heimdal
 hostport
 hrefTarget
@@ -104,6 +106,7 @@ processResultsCount
 Proxied
 py
 rdn
+readthedocs
 reentrant
 refmodule
 refreshAndPersist
@@ -145,6 +148,7 @@ UDP
 Umich
 unparsing
 unsigend
+urandom
 uri
 urlPrefix
 urlscheme
diff --git a/Lib/ldap/__init__.py b/Lib/ldap/__init__.py
index 951f957..8d67573 100644
--- a/Lib/ldap/__init__.py
+++ b/Lib/ldap/__init__.py
@@ -54,11 +54,10 @@ class DummyLock:
 
 try:
   # Check if Python installation was build with thread support
-  import thread
+  import threading
 except ImportError:
   LDAPLockBaseClass = DummyLock
 else:
-  import threading
   LDAPLockBaseClass = threading.Lock
 
 
diff --git a/Lib/ldap/cidict.py b/Lib/ldap/cidict.py
index 4f7e091..48aeacb 100644
--- a/Lib/ldap/cidict.py
+++ b/Lib/ldap/cidict.py
@@ -5,56 +5,70 @@ names of variable case.
 
 See https://www.python-ldap.org/ for details.
 """
+import warnings
 
+from ldap.compat import MutableMapping
 from ldap import __version__
 
-from ldap.compat import IterableUserDict
 
+class cidict(MutableMapping):
+    """
+    Case-insensitive but case-respecting dictionary.
+    """
+    __slots__ = ('_keys', '_data')
 
-class cidict(IterableUserDict):
-  """
-  Case-insensitive but case-respecting dictionary.
-  """
+    def __init__(self, default=None):
+        self._keys = {}
+        self._data = {}
+        if default:
+            self.update(default)
+
+    # MutableMapping abstract methods
+
+    def __getitem__(self, key):
+        return self._data[key.lower()]
 
-  def __init__(self,default=None):
-    self._keys = {}
-    IterableUserDict.__init__(self,{})
-    self.update(default or {})
+    def __setitem__(self, key, value):
+        lower_key = key.lower()
+        self._keys[lower_key] = key
+        self._data[lower_key] = value
 
-  def __getitem__(self,key):
-    return self.data[key.lower()]
+    def __delitem__(self, key):
+        lower_key = key.lower()
+        del self._keys[lower_key]
+        del self._data[lower_key]
 
-  def __setitem__(self,key,value):
-    lower_key = key.lower()
-    self._keys[lower_key] = key
-    self.data[lower_key] = value
+    def __iter__(self):
+        return iter(self._keys.values())
 
-  def __delitem__(self,key):
-    lower_key = key.lower()
-    del self._keys[lower_key]
-    del self.data[lower_key]
+    def __len__(self):
+        return len(self._keys)
 
-  def update(self,dict):
-    for key, value in dict.items():
-      self[key] = value
+    # Specializations for performance
 
-  def has_key(self,key):
-    return key in self
+    def __contains__(self, key):
+        return key.lower() in self._keys
 
-  def __contains__(self,key):
-    return IterableUserDict.__contains__(self, key.lower())
+    def clear(self):
+        self._keys.clear()
+        self._data.clear()
 
-  def __iter__(self):
-    return iter(self.keys())
+    # Backwards compatibility
 
-  def keys(self):
-    return self._keys.values()
+    def has_key(self, key):
+        """Compatibility with python-ldap 2.x"""
+        return key in self
 
-  def items(self):
-    result = []
-    for k in self._keys.values():
-      result.append((k,self[k]))
-    return result
+    @property
+    def data(self):
+        """Compatibility with older IterableUserDict-based implementation"""
+        warnings.warn(
+            'ldap.cidict.cidict.data is an internal attribute; it may be ' +
+            'removed at any time',
+            category=DeprecationWarning,
+            stacklevel=2,
+        )
+        return self._data
 
 
 def strlist_minus(a,b):
@@ -62,6 +76,11 @@ def strlist_minus(a,b):
   Return list of all items in a which are not in b (a - b).
   a,b are supposed to be lists of case-insensitive strings.
   """
+  warnings.warn(
+    "strlist functions are deprecated and will be removed in 3.4",
+    category=DeprecationWarning,
+    stacklevel=2,
+  )
   temp = cidict()
   for elt in b:
     temp[elt] = elt
@@ -77,6 +96,11 @@ def strlist_intersection(a,b):
   """
   Return intersection of two lists of case-insensitive strings a,b.
   """
+  warnings.warn(
+    "strlist functions are deprecated and will be removed in 3.4",
+    category=DeprecationWarning,
+    stacklevel=2,
+  )
   temp = cidict()
   for elt in a:
     temp[elt] = elt
@@ -92,6 +116,11 @@ def strlist_union(a,b):
   """
   Return union of two lists of case-insensitive strings a,b.
   """
+  warnings.warn(
+    "strlist functions are deprecated and will be removed in 3.4",
+    category=DeprecationWarning,
+    stacklevel=2,
+  )
   temp = cidict()
   for elt in a:
     temp[elt] = elt
diff --git a/Lib/ldap/compat.py b/Lib/ldap/compat.py
index cbfeef5..901457b 100644
--- a/Lib/ldap/compat.py
+++ b/Lib/ldap/compat.py
@@ -10,6 +10,7 @@ if sys.version_info[0] < 3:
     from urllib import unquote as urllib_unquote
     from urllib import urlopen
     from urlparse import urlparse
+    from collections import MutableMapping
 
     def unquote(uri):
         """Specialized unquote that uses UTF-8 for parsing."""
@@ -33,6 +34,7 @@ else:
     IterableUserDict = UserDict
     from urllib.parse import quote, quote_plus, unquote, urlparse
     from urllib.request import urlopen
+    from collections.abc import MutableMapping
 
     def reraise(exc_type, exc_value, exc_traceback):
         """Re-raise an exception given information from sys.exc_info()
diff --git a/Lib/ldap/constants.py b/Lib/ldap/constants.py
index 7a7982b..5e178a1 100644
--- a/Lib/ldap/constants.py
+++ b/Lib/ldap/constants.py
@@ -281,7 +281,6 @@ CONSTANTS = (
     TLSInt('OPT_X_TLS_DEMAND'),
     TLSInt('OPT_X_TLS_ALLOW'),
     TLSInt('OPT_X_TLS_TRY'),
-    TLSInt('OPT_X_TLS_PEERCERT', optional=True),
 
     TLSInt('OPT_X_TLS_VERSION', optional=True),
     TLSInt('OPT_X_TLS_CIPHER', optional=True),
@@ -345,6 +344,7 @@ CONSTANTS = (
     Feature('LIBLDAP_R', 'HAVE_LIBLDAP_R'),
     Feature('SASL_AVAIL', 'HAVE_SASL'),
     Feature('TLS_AVAIL', 'HAVE_TLS'),
+    Feature('INIT_FD_AVAIL', 'HAVE_LDAP_INIT_FD'),
 
     Str("CONTROL_MANAGEDSAIT"),
     Str("CONTROL_PROXY_AUTHZ"),
diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py
index 00c7b06..a066ac1 100644
--- a/Lib/ldap/dn.py
+++ b/Lib/ldap/dn.py
@@ -29,10 +29,10 @@ def escape_dn_chars(s):
     s = s.replace(';' ,'\\;')
     s = s.replace('=' ,'\\=')
     s = s.replace('\000' ,'\\\000')
-    if s[0]=='#' or s[0]==' ':
-      s = ''.join(('\\',s))
     if s[-1]==' ':
       s = ''.join((s[:-1],'\\ '))
+    if s[0]=='#' or s[0]==' ':
+      s = ''.join(('\\',s))
   return s
 
 
@@ -111,7 +111,7 @@ def explode_rdn(rdn, notypes=False, flags=0):
 
 def is_dn(s,flags=0):
   """
-  Returns True is `s' can be parsed by ldap.dn.str2dn() like as a
+  Returns True if `s' can be parsed by ldap.dn.str2dn() as a
   distinguished host_name (DN), otherwise False is returned.
   """
   try:
diff --git a/Lib/ldap/extop/__init__.py b/Lib/ldap/extop/__init__.py
index 874166d..39e653a 100644
--- a/Lib/ldap/extop/__init__.py
+++ b/Lib/ldap/extop/__init__.py
@@ -65,3 +65,4 @@ class ExtendedResponse:
 
 # Import sub-modules
 from ldap.extop.dds import *
+from ldap.extop.passwd import PasswordModifyResponse
diff --git a/Lib/ldap/extop/passwd.py b/Lib/ldap/extop/passwd.py
new file mode 100644
index 0000000..0a8346a
--- /dev/null
+++ b/Lib/ldap/extop/passwd.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+"""
+ldap.extop.passwd - Classes for Password Modify extended operation
+(see RFC 3062)
+
+See https://www.python-ldap.org/ for details.
+"""
+
+from ldap.extop import ExtendedResponse
+
+# Imports from pyasn1
+from pyasn1.type import namedtype, univ, tag
+from pyasn1.codec.der import decoder
+
+
+class PasswordModifyResponse(ExtendedResponse):
+    responseName = None
+
+    class PasswordModifyResponseValue(univ.Sequence):
+        componentType = namedtype.NamedTypes(
+            namedtype.OptionalNamedType(
+                'genPasswd',
+                univ.OctetString().subtype(
+                    implicitTag=tag.Tag(tag.tagClassContext,
+                                        tag.tagFormatSimple, 0)
+                )
+            )
+        )
+
+    def decodeResponseValue(self, value):
+        respValue, _ = decoder.decode(value, asn1Spec=self.PasswordModifyResponseValue())
+        self.genPasswd = bytes(respValue.getComponentByName('genPasswd'))
+        return self.genPasswd
diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py
index 529c4c8..31ab00f 100644
--- a/Lib/ldap/functions.py
+++ b/Lib/ldap/functions.py
@@ -67,7 +67,7 @@ def _ldap_function_call(lock,func,*args,**kwargs):
 
 def initialize(
     uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None,
-    bytes_mode=None, **kwargs
+    bytes_mode=None, fileno=None, **kwargs
 ):
   """
   Return LDAPObject instance by opening LDAP connection to
@@ -84,12 +84,17 @@ def initialize(
         Default is to use stdout.
   bytes_mode
         Whether to enable :ref:`bytes_mode` for backwards compatibility under Py2.
+  fileno
+        If not None the socket file descriptor is used to connect to an
+        LDAP server.
 
   Additional keyword arguments (such as ``bytes_strictness``) are
   passed to ``LDAPObject``.
   """
   return LDAPObject(
-      uri, trace_level, trace_file, trace_stack_limit, bytes_mode, **kwargs)
+      uri, trace_level, trace_file, trace_stack_limit, bytes_mode,
+      fileno=fileno, **kwargs
+  )
 
 
 def get_option(option):
diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py
index e4e6841..3c9634c 100644
--- a/Lib/ldap/ldapobject.py
+++ b/Lib/ldap/ldapobject.py
@@ -27,7 +27,7 @@ import warnings
 
 from ldap.schema import SCHEMA_ATTRS
 from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples
-from ldap.extop import ExtendedRequest,ExtendedResponse
+from ldap.extop import ExtendedRequest,ExtendedResponse,PasswordModifyResponse
 from ldap.compat import reraise
 
 from ldap import LDAPError
@@ -96,14 +96,23 @@ class SimpleLDAPObject:
   def __init__(
     self,uri,
     trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
-    bytes_strictness=None,
+    bytes_strictness=None, fileno=None
   ):
     self._trace_level = trace_level or ldap._trace_level
     self._trace_file = trace_file or ldap._trace_file
     self._trace_stack_limit = trace_stack_limit
     self._uri = uri
     self._ldap_object_lock = self._ldap_lock('opcall')
-    self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri)
+    if fileno is not None:
+      if not hasattr(_ldap, "initialize_fd"):
+        raise ValueError("libldap does not support initialize_fd")
+      if hasattr(fileno, "fileno"):
+        fileno = fileno.fileno()
+      self._l = ldap.functions._ldap_function_call(
+        ldap._ldap_module_lock, _ldap.initialize_fd, fileno, uri
+      )
+    else:
+      self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri)
     self.timeout = -1
     self.protocol_version = ldap.VERSION3
 
@@ -526,7 +535,7 @@ class SimpleLDAPObject:
     except ldap.COMPARE_FALSE:
       return False
     raise ldap.PROTOCOL_ERROR(
-        'Compare operation returned wrong result: %r' % (ldap_res)
+        'Compare operation returned wrong result: %r' % (ldap_res,)
     )
 
   def compare(self,dn,attr,value):
@@ -656,9 +665,16 @@ class SimpleLDAPObject:
         newpw = self._bytesify_input('newpw', newpw)
     return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
 
-  def passwd_s(self,user,oldpw,newpw,serverctrls=None,clientctrls=None):
-    msgid = self.passwd(user,oldpw,newpw,serverctrls,clientctrls)
-    return self.extop_result(msgid,all=1,timeout=self.timeout)
+  def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None, extract_newpw=False):
+    msgid = self.passwd(user, oldpw, newpw, serverctrls, clientctrls)
+    respoid, respvalue = self.extop_result(msgid, all=1, timeout=self.timeout)
+
+    if respoid != PasswordModifyResponse.responseName:
+      raise ldap.PROTOCOL_ERROR("Unexpected OID %s in extended response!" % respoid)
+    if extract_newpw and respvalue:
+      respvalue = PasswordModifyResponse(PasswordModifyResponse.responseName, respvalue)
+
+    return respoid, respvalue
 
   def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None):
     """
@@ -1086,7 +1102,7 @@ class ReconnectLDAPObject(SimpleLDAPObject):
   def __init__(
     self,uri,
     trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
-    bytes_strictness=None, retry_max=1, retry_delay=60.0
+    bytes_strictness=None, retry_max=1, retry_delay=60.0, fileno=None
   ):
     """
     Parameters like SimpleLDAPObject.__init__() with these
@@ -1102,7 +1118,8 @@ class ReconnectLDAPObject(SimpleLDAPObject):
     self._last_bind = None
     SimpleLDAPObject.__init__(self, uri, trace_level, trace_file,
                               trace_stack_limit, bytes_mode,
-                              bytes_strictness=bytes_strictness)
+                              bytes_strictness=bytes_strictness,
+                              fileno=fileno)
     self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self)))
     self._retry_max = retry_max
     self._retry_delay = retry_delay
@@ -1166,14 +1183,18 @@ class ReconnectLDAPObject(SimpleLDAPObject):
             counter_text,uri
           ))
         try:
-          # Do the connect
-          self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri)
-          self._restore_options()
-          # StartTLS extended operation in case this was called before
-          if self._start_tls:
-            SimpleLDAPObject.start_tls_s(self)
-          # Repeat last simple or SASL bind
-          self._apply_last_bind()
+          try:
+            # Do the connect
+            self._l = ldap.functions._ldap_function_call(ldap._ldap_module_lock,_ldap.initialize,uri)
+            self._restore_options()
+            # StartTLS extended operation in case this was called before
+            if self._start_tls:
+              SimpleLDAPObject.start_tls_s(self)
+            # Repeat last simple or SASL bind
+            self._apply_last_bind()
+          except ldap.LDAPError:
+            SimpleLDAPObject.unbind_s(self)
+            raise
         except (ldap.SERVER_DOWN,ldap.TIMEOUT):
           if __debug__ and self._trace_level>=1:
             self._trace_file.write('*** %s reconnect to %s failed\n' % (
@@ -1185,7 +1206,6 @@ class ReconnectLDAPObject(SimpleLDAPObject):
           if __debug__ and self._trace_level>=1:
             self._trace_file.write('=> delay %s...\n' % (retry_delay))
           time.sleep(retry_delay)
-          SimpleLDAPObject.unbind_s(self)
         else:
           if __debug__ and self._trace_level>=1:
             self._trace_file.write('*** %s reconnect to %s successful => repeat last operation\n' % (
diff --git a/Lib/ldap/modlist.py b/Lib/ldap/modlist.py
index 4acf4e9..bf4e481 100644
--- a/Lib/ldap/modlist.py
+++ b/Lib/ldap/modlist.py
@@ -50,7 +50,7 @@ def modifyModlist(
   case_ignore_attr_types = {v.lower() for v in case_ignore_attr_types or []}
   modlist = []
   attrtype_lower_map = {}
-  for a in old_entry.keys():
+  for a in old_entry:
     attrtype_lower_map[a.lower()]=a
   for attrtype, value in new_entry.items():
     attrtype_lower = attrtype.lower()
diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py
index df29b60..dd7b3a8 100644
--- a/Lib/ldap/pkginfo.py
+++ b/Lib/ldap/pkginfo.py
@@ -2,6 +2,6 @@
 """
 meta attributes for packaging which does not import any dependencies
 """
-__version__ = '3.2.0'
+__version__ = '3.3.1'
 __author__ = u'python-ldap project'
 __license__ = 'Python style'
diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py
index 5ccbce0..215f148 100644
--- a/Lib/ldap/schema/subentry.py
+++ b/Lib/ldap/schema/subentry.py
@@ -23,7 +23,7 @@ for o in list(vars().values()):
     SCHEMA_CLASS_MAPPING[o.schema_attribute] = o
     SCHEMA_ATTR_MAPPING[o] = o.schema_attribute
 
-SCHEMA_ATTRS = SCHEMA_CLASS_MAPPING.keys()
+SCHEMA_ATTRS = list(SCHEMA_CLASS_MAPPING)
 
 
 class SubschemaError(ValueError):
@@ -122,7 +122,7 @@ class SubSchema:
         self.sed[se_class][se_id] = se_instance
 
         if hasattr(se_instance,'names'):
-          for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)).keys():
+          for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)):
             if check_uniqueness and name in self.name2oid[se_class]:
               self.non_unique_names[se_class][se_id] = None
               raise NameNotUnique(attr_value)
@@ -130,7 +130,7 @@ class SubSchema:
               self.name2oid[se_class][name] = se_id
 
     # Turn dict into list maybe more handy for applications
-    self.non_unique_oids = self.non_unique_oids.keys()
+    self.non_unique_oids = list(self.non_unique_oids)
 
     return # subSchema.__init__()
 
@@ -168,7 +168,7 @@ class SubSchema:
           except AttributeError:
             pass
     else:
-      result = avail_se.keys()
+      result = list(avail_se)
     return result
 
 
@@ -422,14 +422,14 @@ class SubSchema:
 
     # Remove all mandantory attribute types from
     # optional attribute type list
-    for a in list(r_may.keys()):
+    for a in list(r_may):
       if a in r_must:
         del r_may[a]
 
     # Apply attr_type_filter to results
     if attr_type_filter:
       for l in [r_must,r_may]:
-        for a in list(l.keys()):
+        for a in list(l):
           for afk,afv in attr_type_filter:
             try:
               schema_attr_type = self.sed[AttributeType][a]
diff --git a/Lib/ldap/schema/tokenizer.py b/Lib/ldap/schema/tokenizer.py
index 20958c0..69823f2 100644
--- a/Lib/ldap/schema/tokenizer.py
+++ b/Lib/ldap/schema/tokenizer.py
@@ -13,12 +13,16 @@ TOKENS_FINDALL = re.compile(
     r"|"              # or
     r"([^'$()\s]+)"   # string of length >= 1 without '$() or whitespace
     r"|"              # or
-    r"('.*?'(?!\w))"  # any string or empty string surrounded by single quotes
-                      # except if right quote is succeeded by alphanumeric char
+    r"('(?:[^'\\]|\\\\|\\.)*?'(?!\w))"
+                      # any string or empty string surrounded by unescaped
+                      # single quotes except if right quote is succeeded by
+                      # alphanumeric char
     r"|"              # or
     r"([^\s]+?)",     # residue, all non-whitespace strings
 ).findall
 
+UNESCAPE_PATTERN = re.compile(r"\\(.)")
+
 
 def split_tokens(s):
     """
@@ -30,7 +34,7 @@ def split_tokens(s):
         if unquoted:
             parts.append(unquoted)
         elif quoted:
-            parts.append(quoted[1:-1])
+            parts.append(UNESCAPE_PATTERN.sub(r'\1', quoted[1:-1]))
         elif opar:
             parens += 1
             parts.append(opar)
diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py
index 0de5cec..f6ac2d1 100644
--- a/Lib/ldap/syncrepl.py
+++ b/Lib/ldap/syncrepl.py
@@ -314,34 +314,35 @@ class SyncInfoMessage:
         self.refreshPresent = None
         self.syncIdSet = None
 
-        for attr in ['newcookie', 'refreshDelete', 'refreshPresent', 'syncIdSet']:
-            comp = d[0].getComponentByName(attr)
-
-            if comp is not None and comp.hasValue():
-
-                if attr == 'newcookie':
-                    self.newcookie = str(comp)
-                    return
+        # Due to the way pyasn1 works, refreshDelete and refreshPresent are both
+        # valid in the components as they are fully populated defaults. We must
+        # get the component directly from the message, not by iteration.
+        attr = d[0].getName()
+        comp = d[0].getComponent()
+
+        if comp is not None and comp.hasValue():
+            if attr == 'newcookie':
+                self.newcookie = str(comp)
+                return
 
-                val = {}
+            val = {}
 
-                cookie = comp.getComponentByName('cookie')
-                if cookie.hasValue():
-                    val['cookie'] = str(cookie)
+            cookie = comp.getComponentByName('cookie')
+            if cookie.hasValue():
+                val['cookie'] = str(cookie)
 
-                if attr.startswith('refresh'):
-                    val['refreshDone'] = bool(comp.getComponentByName('refreshDone'))
-                elif attr == 'syncIdSet':
-                    uuids = []
-                    ids = comp.getComponentByName('syncUUIDs')
-                    for i in range(len(ids)):
-                        uuid = UUID(bytes=bytes(ids.getComponentByPosition(i)))
-                        uuids.append(str(uuid))
-                    val['syncUUIDs'] = uuids
-                    val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes'))
+            if attr.startswith('refresh'):
+                val['refreshDone'] = bool(comp.getComponentByName('refreshDone'))
+            elif attr == 'syncIdSet':
+                uuids = []
+                ids = comp.getComponentByName('syncUUIDs')
+                for i in range(len(ids)):
+                    uuid = UUID(bytes=bytes(ids.getComponentByPosition(i)))
+                    uuids.append(str(uuid))
+                val['syncUUIDs'] = uuids
+                val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes'))
 
-                setattr(self, attr, val)
-                return
+            setattr(self, attr, val)
 
 
 class SyncreplConsumer:
diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py
index 6de0645..513c08d 100644
--- a/Lib/ldapurl.py
+++ b/Lib/ldapurl.py
@@ -4,7 +4,7 @@ ldapurl - handling of LDAP URLs as described in RFC 4516
 See https://www.python-ldap.org/ for details.
 """
 
-__version__ = '3.2.0'
+__version__ = '3.3.1'
 
 __all__ = [
   # constants
@@ -16,7 +16,7 @@ __all__ = [
   'LDAPUrlExtension','LDAPUrlExtensions','LDAPUrl'
 ]
 
-from ldap.compat import UserDict, quote, unquote
+from ldap.compat import quote, unquote, MutableMapping
 
 LDAP_SCOPE_BASE = 0
 LDAP_SCOPE_ONELEVEL = 1
@@ -130,58 +130,71 @@ class LDAPUrlExtension(object):
     return not self.__eq__(other)
 
 
-class LDAPUrlExtensions(UserDict):
-  """
-  Models a collection of LDAP URL extensions as
-  dictionary type
-  """
-
-  def __init__(self,default=None):
-    UserDict.__init__(self)
-    for k,v in (default or {}).items():
-      self[k]=v
-
-  def __setitem__(self,name,value):
+class LDAPUrlExtensions(MutableMapping):
     """
-    value
-        Either LDAPUrlExtension instance, (critical,exvalue)
-        or string'ed exvalue
+    Models a collection of LDAP URL extensions as
+    a mapping type
     """
-    assert isinstance(value,LDAPUrlExtension)
-    assert name==value.extype
-    self.data[name] = value
-
-  def values(self):
-    return [
-      self[k]
-      for k in self.keys()
-    ]
-
-  def __str__(self):
-    return ','.join(str(v) for v in self.values())
-
-  def __repr__(self):
-    return '<%s.%s instance at %s: %s>' % (
-      self.__class__.__module__,
-      self.__class__.__name__,
-      hex(id(self)),
-      self.data
-    )
+    __slots__ = ('_data', )
+
+    def __init__(self, default=None):
+        self._data = {}
+        if default is not None:
+            self.update(default)
+
+    def __setitem__(self, name, value):
+        """Store an extension
+
+        name
+            string
+        value
+            LDAPUrlExtension instance, whose extype nust match `name`
+        """
+        if not isinstance(value, LDAPUrlExtension):
+            raise TypeError("value must be LDAPUrlExtension, not "
+                            + type(value).__name__)
+        if name != value.extype:
+            raise ValueError(
+                "key {!r} does not match extension type {!r}".format(
+                    name, value.extype))
+        self._data[name] = value
+
+    def __getitem__(self, name):
+        return self._data[name]
+
+    def __delitem__(self, name):
+        del self._data[name]
+
+    def __iter__(self):
+        return iter(self._data)
+
+    def __len__(self):
+        return len(self._data)
+
+    def __str__(self):
+        return ','.join(str(v) for v in self.values())
+
+    def __repr__(self):
+        return '<%s.%s instance at %s: %s>' % (
+            self.__class__.__module__,
+            self.__class__.__name__,
+            hex(id(self)),
+            self._data
+        )
 
-  def __eq__(self,other):
-    assert isinstance(other,self.__class__),TypeError(
-      "other has to be instance of %s" % (self.__class__)
-    )
-    return self.data==other.data
+    def __eq__(self,other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return self._data == other._data
 
-  def parse(self,extListStr):
-    for extension_str in extListStr.strip().split(','):
-      if extension_str:
-        e = LDAPUrlExtension(extension_str)
-        self[e.extype] = e
+    def parse(self,extListStr):
+        for extension_str in extListStr.strip().split(','):
+            if extension_str:
+                e = LDAPUrlExtension(extension_str)
+                self[e.extype] = e
 
-  def unparse(self):
-    return ','.join([ v.unparse() for v in self.values() ])
+    def unparse(self):
+        return ','.join(v.unparse() for v in self.values())
 
 
 class LDAPUrl(object):
@@ -366,17 +379,23 @@ class LDAPUrl(object):
     hrefTarget
         string added as link target attribute
     """
-    assert type(urlPrefix)==StringType, "urlPrefix must be StringType"
+    if not isinstance(urlPrefix, str):
+        raise TypeError("urlPrefix must be str, not "
+                        + type(urlPrefix).__name__)
     if hrefText is None:
-      hrefText = self.unparse()
-    assert type(hrefText)==StringType, "hrefText must be StringType"
+        hrefText = self.unparse()
+    if not isinstance(hrefText, str):
+        raise TypeError("hrefText must be str, not "
+                        + type(hrefText).__name__)
     if hrefTarget is None:
-      target = ''
+        target = ''
     else:
-      assert type(hrefTarget)==StringType, "hrefTarget must be StringType"
-      target = ' target="%s"' % hrefTarget
+        if not isinstance(hrefTarget, str):
+            raise TypeError("hrefTarget must be str, not "
+                            + type(hrefTarget).__name__)
+        target = ' target="%s"' % hrefTarget
     return '<a%s href="%s%s">%s</a>' % (
-      target,urlPrefix,self.unparse(),hrefText
+        target, urlPrefix, self.unparse(), hrefText
     )
 
   def __str__(self):
diff --git a/Lib/ldif.py b/Lib/ldif.py
index a26c8ac..cde1dd5 100644
--- a/Lib/ldif.py
+++ b/Lib/ldif.py
@@ -6,7 +6,7 @@ See https://www.python-ldap.org/ for details.
 
 from __future__ import unicode_literals
 
-__version__ = '3.2.0'
+__version__ = '3.3.1'
 
 __all__ = [
   # constants
diff --git a/Lib/python_ldap.egg-info/PKG-INFO b/Lib/python_ldap.egg-info/PKG-INFO
index 699c986..065f45b 100644
--- a/Lib/python_ldap.egg-info/PKG-INFO
+++ b/Lib/python_ldap.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: python-ldap
-Version: 3.2.0
+Version: 3.3.1
 Summary: Python modules for implementing LDAP clients
 Home-page: https://www.python-ldap.org/
 Author: python-ldap project
@@ -31,6 +31,7 @@ Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
 Classifier: Topic :: Database
 Classifier: Topic :: Internet
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/Lib/python_ldap.egg-info/SOURCES.txt b/Lib/python_ldap.egg-info/SOURCES.txt
index bfc49c4..52a418e 100644
--- a/Lib/python_ldap.egg-info/SOURCES.txt
+++ b/Lib/python_ldap.egg-info/SOURCES.txt
@@ -103,6 +103,7 @@ Lib/ldap/controls/sss.py
 Lib/ldap/controls/vlv.py
 Lib/ldap/extop/__init__.py
 Lib/ldap/extop/dds.py
+Lib/ldap/extop/passwd.py
 Lib/ldap/schema/__init__.py
 Lib/ldap/schema/models.py
 Lib/ldap/schema/subentry.py
diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py
index 6b8c986..c56f8c3 100644
--- a/Lib/slapdtest/__init__.py
+++ b/Lib/slapdtest/__init__.py
@@ -5,8 +5,9 @@ slapdtest - module for spawning test instances of OpenLDAP's slapd server
 See https://www.python-ldap.org/ for details.
 """
 
-__version__ = '3.2.0'
+__version__ = '3.3.1'
 
 from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler
 from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls
+from slapdtest._slapdtest import requires_init_fd
 from slapdtest._slapdtest import skip_unless_ci
diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py
index f1885ca..afd5304 100644
--- a/Lib/slapdtest/_slapdtest.py
+++ b/Lib/slapdtest/_slapdtest.py
@@ -109,6 +109,14 @@ def requires_ldapi():
     else:
         return identity
 
+def requires_init_fd():
+    if not ldap.INIT_FD_AVAIL:
+        return skip_unless_ci(
+            "test needs ldap.INIT_FD", feature='INIT_FD')
+    else:
+        return identity
+
+
 def _add_sbin(path):
     """Add /sbin and related directories to a command search path"""
     directories = path.split(os.pathsep)
@@ -179,7 +187,7 @@ class SlapdObject(object):
     root_cn = 'Manager'
     root_pw = 'password'
     slapd_loglevel = 'stats stats2'
-    local_host = '127.0.0.1'
+    local_host = LOCALHOST
     testrunsubdirs = (
         'schema',
     )
@@ -214,7 +222,7 @@ class SlapdObject(object):
         self._schema_prefix = os.path.join(self.testrundir, 'schema')
         self._slapd_conf = os.path.join(self.testrundir, 'slapd.conf')
         self._db_directory = os.path.join(self.testrundir, "openldap-data")
-        self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port)
+        self.ldap_uri = "ldap://%s:%d/" % (self.local_host, self._port)
         if HAVE_LDAPI:
             ldapi_path = os.path.join(self.testrundir, 'ldapi')
             self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path)
@@ -243,6 +251,14 @@ class SlapdObject(object):
     def root_dn(self):
         return 'cn={self.root_cn},{self.suffix}'.format(self=self)
 
+    @property
+    def hostname(self):
+        return self.local_host
+
+    @property
+    def port(self):
+        return self._port
+
     def _find_commands(self):
         self.PATH_LDAPADD = self._find_command('ldapadd')
         self.PATH_LDAPDELETE = self._find_command('ldapdelete')
@@ -252,7 +268,6 @@ class SlapdObject(object):
         self.PATH_SLAPD = os.environ.get('SLAPD', None)
         if not self.PATH_SLAPD:
             self.PATH_SLAPD = self._find_command('slapd', in_sbin=True)
-        self.PATH_SLAPTEST = self._find_command('slaptest', in_sbin=True)
 
     def _find_command(self, cmd, in_sbin=False):
         if in_sbin:
@@ -378,7 +393,8 @@ class SlapdObject(object):
     def _test_config(self):
         self._log.debug('testing config %s', self._slapd_conf)
         popen_list = [
-            self.PATH_SLAPTEST,
+            self.PATH_SLAPD,
+            '-Ttest',
             "-f", self._slapd_conf,
             '-u',
         ]
diff --git a/Lib/slapdtest/certs/ca.conf b/Lib/slapdtest/certs/ca.conf
index 5046b0d..d1d89e1 100644
--- a/Lib/slapdtest/certs/ca.conf
+++ b/Lib/slapdtest/certs/ca.conf
@@ -32,7 +32,7 @@ serial = $tmpdir/$ca.crt.srl
 crlnumber = $tmpdir/$ca.crl.srl
 database = $tmpdir/$ca.db
 unique_subject = no
-default_days = 3652
+default_days = 365200
 default_md = sha256
 policy = match_pol
 email_in_dn = no
@@ -40,7 +40,7 @@ preserve = no
 name_opt = $name_opt
 cert_opt = ca_default
 copy_extensions = none
-default_crl_days = 3651
+default_crl_days = 365100
 
 [match_pol]
 countryName = match
diff --git a/Lib/slapdtest/certs/ca.pem b/Lib/slapdtest/certs/ca.pem
index cf2ff33..b52ffaf 100644
--- a/Lib/slapdtest/certs/ca.pem
+++ b/Lib/slapdtest/certs/ca.pem
@@ -5,31 +5,31 @@ Certificate:
     Signature Algorithm: sha256WithRSAEncryption
         Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
         Validity
-            Not Before: Dec  2 11:57:47 2017 GMT
-            Not After : Sep  4 11:57:47 2027 GMT
+            Not Before: Apr 12 18:52:38 2019 GMT
+            Not After : Oct 17 18:52:38 2994 GMT
         Subject: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
         Subject Public Key Info:
             Public Key Algorithm: rsaEncryption
                 Public-Key: (2048 bit)
                 Modulus:
-                    00:af:1f:cf:0f:c5:95:66:2d:eb:85:cc:21:fc:0d:
-                    0f:44:d8:2f:a8:85:08:ef:60:67:57:fa:0b:c5:e4:
-                    b3:fb:f1:6f:cb:30:7a:47:0d:a7:f1:b5:37:81:5f:
-                    f6:39:28:e2:f9:4d:6c:2e:a6:5c:0e:3c:db:4d:c9:
-                    2a:64:ce:0d:15:30:c7:75:52:b8:74:c5:0b:00:4c:
-                    2f:94:1b:dd:fb:83:2c:58:02:73:b0:86:3a:6a:aa:
-                    55:f2:d5:49:99:17:a5:e2:44:ec:dd:62:5f:8d:ce:
-                    77:29:0b:8d:87:23:e2:4b:d6:1c:25:f3:06:a9:ee:
-                    33:6f:ac:ed:22:9e:35:ec:55:e7:1b:38:68:7e:46:
-                    e3:c3:42:ac:06:0b:0a:7a:84:c9:3d:ef:3d:a5:6e:
-                    e9:10:24:c3:28:fe:1f:4a:9a:23:8a:3c:db:0a:66:
-                    5d:07:f8:c5:17:68:53:e4:0e:37:33:c4:d2:ad:58:
-                    62:6b:8a:87:ab:73:eb:bc:2b:ac:07:69:84:8d:e3:
-                    c4:a9:78:9b:6c:1e:03:63:df:b4:96:18:bd:3c:2e:
-                    be:7f:2c:d5:a8:f8:12:b9:ab:27:52:b0:de:38:62:
-                    3c:54:a7:f3:aa:37:a3:11:12:b2:a7:6f:8d:96:10:
-                    ce:01:cb:25:24:a6:51:18:93:69:9b:9e:5c:8a:ff:
-                    fe:89
+                    00:d7:30:73:20:44:7d:83:d4:c7:01:b8:ab:1e:7c:
+                    91:f4:38:ac:9c:41:43:64:0c:31:99:48:70:22:7d:
+                    ae:1b:47:e7:2a:28:4d:f7:46:4e:b4:ba:ae:c0:9d:
+                    d5:1f:4b:7a:79:2f:b9:dc:68:7f:79:84:88:50:51:
+                    3b:7d:dc:d5:57:17:66:45:c0:2c:20:13:f7:99:d6:
+                    9d:e2:12:7c:41:76:82:51:19:2c:b6:ff:46:cb:04:
+                    56:38:22:2a:c3:7a:b5:71:51:49:4e:62:68:a0:99:
+                    6f:de:f3:a2:0f:a2:aa:1b:72:a5:87:bc:42:5a:a7:
+                    22:8d:33:b4:88:a8:dc:5d:72:ca:dd:a0:9a:4e:db:
+                    7d:8b:10:de:c5:41:e9:e9:8d:fa:6c:dd:94:6e:b1:
+                    31:c2:6d:a1:69:6c:7a:3a:b2:76:65:c9:e5:95:38:
+                    62:40:81:c6:29:26:26:d1:d1:c1:f4:5e:fa:24:ef:
+                    13:da:24:13:6f:f5:5c:ba:b1:31:8f:30:94:71:7b:
+                    c6:e5:da:b9:b5:64:39:39:09:c2:4a:80:64:58:1d:
+                    99:f5:65:3c:a7:26:08:95:26:35:7b:fa:e7:20:08:
+                    ff:72:df:9b:8f:9f:da:8b:c3:a7:8b:fc:8c:c0:a5:
+                    31:87:1d:4c:14:f6:cf:90:5e:2e:6e:a6:db:27:08:
+                    eb:df
                 Exponent: 65537 (0x10001)
         X509v3 extensions:
             X509v3 Basic Constraints: critical
@@ -37,44 +37,44 @@ Certificate:
             X509v3 Key Usage: critical
                 Certificate Sign, CRL Sign
             X509v3 Subject Key Identifier: 
-                3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4
+                BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
             X509v3 Authority Key Identifier: 
-                keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4
+                keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
 
     Signature Algorithm: sha256WithRSAEncryption
-         0a:e7:dc:38:ce:03:dd:a8:99:11:d0:24:be:ef:1a:18:9d:7c:
-         95:75:4a:4a:29:44:23:28:fc:66:d5:81:ce:05:c2:c0:6b:71:
-         d6:8d:33:a9:53:a6:1c:f1:4e:50:ae:a3:b1:72:d6:69:53:ad:
-         a9:62:a9:45:27:68:17:35:41:97:ec:e9:65:91:62:12:ed:eb:
-         45:3a:9b:cc:09:bc:e3:ad:22:6b:13:6b:b0:67:ef:ce:01:83:
-         5e:6c:95:e2:b3:73:b9:69:9a:33:49:f9:5f:52:4e:39:94:c9:
-         db:93:6f:d8:ba:10:92:ce:fa:12:6b:bc:31:ff:c1:67:70:63:
-         07:dc:53:7a:3a:a3:51:20:15:44:cf:1c:a9:cd:b7:30:1d:8e:
-         55:93:8a:56:8c:3d:e9:8b:ae:0c:77:8d:5c:8b:fd:22:d8:4c:
-         3e:e4:76:e8:d9:e8:c3:98:f4:98:ff:02:60:95:8e:3e:26:7a:
-         e2:fe:2c:0a:a4:52:8d:4c:3d:dd:4c:fd:2f:2c:db:83:4c:2b:
-         25:24:37:78:9a:07:27:52:f9:1c:c0:65:65:cb:50:77:b4:2d:
-         fa:f4:af:bb:42:1c:43:65:c6:01:6e:f1:4b:fe:b8:4a:3c:29:
-         8b:b6:84:1e:17:99:61:98:65:fe:f2:e9:ce:bb:ac:87:69:cb:
-         e6:13:42:bf
+         06:20:1f:eb:42:6a:42:62:b1:ee:69:c8:cd:47:a6:2e:69:95:
+         59:dc:49:09:69:40:93:25:a1:ec:6d:3a:dd:dc:e5:74:ab:33:
+         9d:8f:cc:e3:bb:7a:3f:5b:51:58:74:f7:bd:6c:7c:3c:b6:5a:
+         05:50:a8:8c:c3:fb:5b:75:2a:c2:6c:06:93:4c:a9:93:71:1c:
+         51:e5:be:a1:24:93:e2:79:ca:ea:08:86:90:b9:70:e7:7a:40:
+         bf:f4:d6:71:f4:4d:c0:0f:e0:31:a0:23:46:77:30:72:a9:62:
+         8a:2a:12:c4:dd:3d:86:ae:f7:6b:33:80:26:58:49:53:ff:cd:
+         8a:c6:f6:11:2c:b3:ff:a5:8e:1c:f8:22:e2:1b:8e:04:33:fb:
+         0d:da:31:86:12:9f:d1:03:86:9c:6a:78:5e:3c:5e:8a:52:aa:
+         68:1f:ff:f9:17:75:b0:da:f2:99:3c:80:3c:96:2a:33:07:54:
+         59:84:e7:92:34:0f:99:76:e3:d6:4d:4d:9c:fb:21:35:f9:cb:
+         a5:30:80:8b:9d:61:90:d3:d4:59:3a:2f:f2:f6:20:13:7e:26:
+         dc:50:b0:49:3e:19:fe:eb:7d:cf:b9:1a:5d:5c:3a:76:30:d9:
+         0e:d7:df:de:ce:a9:c4:21:df:63:b9:d0:64:86:0b:28:9a:2e:
+         ab:51:73:e4
 -----BEGIN CERTIFICATE-----
-MIIDijCCAnKgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
+MIIDjDCCAnSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
 MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV
-BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ3WhcNMjcwOTA0
-MTE1NzQ3WjBWMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR
-BgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNVBAMME1B5dGhvbiBMREFQIFRlc3QgQ0Ew
-ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvH88PxZVmLeuFzCH8DQ9E
-2C+ohQjvYGdX+gvF5LP78W/LMHpHDafxtTeBX/Y5KOL5TWwuplwOPNtNySpkzg0V
-MMd1Urh0xQsATC+UG937gyxYAnOwhjpqqlXy1UmZF6XiROzdYl+NzncpC42HI+JL
-1hwl8wap7jNvrO0injXsVecbOGh+RuPDQqwGCwp6hMk97z2lbukQJMMo/h9KmiOK
-PNsKZl0H+MUXaFPkDjczxNKtWGJrioerc+u8K6wHaYSN48SpeJtsHgNj37SWGL08
-Lr5/LNWo+BK5qydSsN44YjxUp/OqN6MRErKnb42WEM4ByyUkplEYk2mbnlyK//6J
-AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud
-DgQWBBQ7HzL0/lfRb0mRVfIk8QpmO6Xu1DAfBgNVHSMEGDAWgBQ7HzL0/lfRb0mR
-VfIk8QpmO6Xu1DANBgkqhkiG9w0BAQsFAAOCAQEACufcOM4D3aiZEdAkvu8aGJ18
-lXVKSilEIyj8ZtWBzgXCwGtx1o0zqVOmHPFOUK6jsXLWaVOtqWKpRSdoFzVBl+zp
-ZZFiEu3rRTqbzAm8460iaxNrsGfvzgGDXmyV4rNzuWmaM0n5X1JOOZTJ25Nv2LoQ
-ks76Emu8Mf/BZ3BjB9xTejqjUSAVRM8cqc23MB2OVZOKVow96YuuDHeNXIv9IthM
-PuR26Nnow5j0mP8CYJWOPiZ64v4sCqRSjUw93Uz9Lyzbg0wrJSQ3eJoHJ1L5HMBl
-ZctQd7Qt+vSvu0IcQ2XGAW7xS/64Sjwpi7aEHheZYZhl/vLpzrush2nL5hNCvw==
+BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMjk5NDEw
+MTcxODUyMzhaMFYxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET
+MBEGA1UECwwKc2xhcGQtdGVzdDEcMBoGA1UEAwwTUHl0aG9uIExEQVAgVGVzdCBD
+QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANcwcyBEfYPUxwG4qx58
+kfQ4rJxBQ2QMMZlIcCJ9rhtH5yooTfdGTrS6rsCd1R9Lenkvudxof3mEiFBRO33c
+1VcXZkXALCAT95nWneISfEF2glEZLLb/RssEVjgiKsN6tXFRSU5iaKCZb97zog+i
+qhtypYe8QlqnIo0ztIio3F1yyt2gmk7bfYsQ3sVB6emN+mzdlG6xMcJtoWlsejqy
+dmXJ5ZU4YkCBxikmJtHRwfRe+iTvE9okE2/1XLqxMY8wlHF7xuXaubVkOTkJwkqA
+ZFgdmfVlPKcmCJUmNXv65yAI/3Lfm4+f2ovDp4v8jMClMYcdTBT2z5BeLm6m2ycI
+698CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
+VR0OBBYEFL141UrxkJbF6OxmSSNHA18mc4ayMB8GA1UdIwQYMBaAFL141UrxkJbF
+6OxmSSNHA18mc4ayMA0GCSqGSIb3DQEBCwUAA4IBAQAGIB/rQmpCYrHuacjNR6Yu
+aZVZ3EkJaUCTJaHsbTrd3OV0qzOdj8zju3o/W1FYdPe9bHw8tloFUKiMw/tbdSrC
+bAaTTKmTcRxR5b6hJJPiecrqCIaQuXDnekC/9NZx9E3AD+AxoCNGdzByqWKKKhLE
+3T2GrvdrM4AmWElT/82KxvYRLLP/pY4c+CLiG44EM/sN2jGGEp/RA4acanhePF6K
+UqpoH//5F3Ww2vKZPIA8liozB1RZhOeSNA+ZduPWTU2c+yE1+culMICLnWGQ09RZ
+Oi/y9iATfibcULBJPhn+633PuRpdXDp2MNkO19/ezqnEId9judBkhgsomi6rUXPk
 -----END CERTIFICATE-----
diff --git a/Lib/slapdtest/certs/client.key b/Lib/slapdtest/certs/client.key
index 70600ba..7213c0b 100644
--- a/Lib/slapdtest/certs/client.key
+++ b/Lib/slapdtest/certs/client.key
@@ -1,28 +1,28 @@
 -----BEGIN PRIVATE KEY-----
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDGxvbMEbahViK4
-P6aoWUkciIf1dEYTBuU8M1eShREUZ3Ytq/ee425pXRyxxDrAa8ygRjqs7tauwhgA
-KNuPRGyw7hyZ1Ku4vQObwX9rzyHQ6Fj606U4HHbYfAVb0AF7OzLbhNH3isCRtNcm
-EUSYG1Nkfn9zQkV4pz2KJM4ePt4GyGJV0NhRUdHdwgsWiuRt2EIRPwLXdkf/4svm
-2EahAJax+SMaZYe/lg9w9SBxl6DTaF9lFpoPqzeW+KnXCmE8e+qSWea8EjkzIRXa
-4KJ/EnUMP2fUL+Je2agGF2YfCId9YvLTQV8YDv7Im1Sp0sbIUDoWX14GDUbZ8cBr
-wfOyI0TfAgMBAAECggEATWv1eGp1zcU05Lq1+OA938U1316YZJTM+HOu6jy1+FKL
-7yIJ4nMG8Db6FCswDv5txwdTl0O3jn2+x2Eik1y9UPSNY0U4VU4Zd7MYJC+bJjk5
-XwjMU1yS1aMIm0gbK5pVJrdG6Lm8Y4QiQIt9Qhlyk7PJhGUNlf7ds06+kX0/ETiO
-vx5SatExeKu5F+JRnGFdAN0106SF5vBum+UbrgOSnJmfwX5VoOXARD21ppxgMzAr
-JyGBpgBgy++GpV15gXGuA7DVMIADdHw8hV4OuBLjpkUL+ntArjhpUi7TP7VU3WKR
-uUmvLm9CX1l8O/xZMpt9N1+o71a//7asnz8AMtT6cQKBgQD4FgefUkVnXDA1xKDW
-1JbArVQeHiLGlRdLakRUY/HdGj72YgAOLt3UsrON4VQXl0C6rks/8HKCFaMexBlF
-OecJNWsEVgBEAfsQ+NvrApOQsTszc8Zqna0Kqe2vA0VNa+SAzdHzhBbFcaVkzXJb
-JB7M0/OIt5IaqXg6Y5eX2eZF1QKBgQDNHkIoJ/2hYtlSgXpGaniM+0XemQJgJXig
-edAQdGKKfqwmjSFjByDM01ZaidMu5fEkeGhMRE73IbwNw0pWsMXylD6bI6+sk7yQ
-biM+fslFEEDbgSJe41Jy2eerh5am+dnrMWNhd7QZV1K6tmaqrIzkmIV21/EPXIPp
-BNHO8GV14wKBgGOybrO/GzcTXChvcXeEDWU3AqPr1mvZhHgBJ56GX69MGdtnvL/2
-Y51Th0bQM7wbQ58B5im21j2itl/pzIH+Z/NSbURbz1WFOkEy0SYbbfPq1XCy6Rz1
-apHrgiIf/VzErBp7HBFxlrkYF7Bvw7IOzPXhg3AA3Y0rZ66HUWdr4NdVAoGBAJfC
-E2Bydgy5feC1OypuC9MC9abDviY0kxLoDTCfa2jcX7IGKPWDiJkCo5lI7557Mfax
-vzjuMR5XLzNfkdih4VKgq9FMjeU5SQHy+tB6LZ+Tbuj4md1qgs3GuskGAEh6Auko
-GUc7sVwuZ18NJNiR4Ywf7F8JVajv4gi9MB3Tbr3RAoGARSnVu+6rYSQTyEqvbsaB
-gIW7Ezea5q06GcQF072nk3tNSXuU/52YMlodAJ1UfFPbBAtaa7wEFN8oRG1IyKON
-MGyf6RD8GoInJjaDihkdCsR28RkchwymG1UMPnPzqRxSAb7da5YuMR8PEioVbL68
-dxhsgNi1Wtc2nGqN96qufG0=
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDjt5O6nRrnAWPm
+T0JvRLBHMclll92IWF/O4GEdcJ5fbBxP3BxK0Dv+6aRcR7b2o0f6fk/bgNepXfv/
+MXDQcFlESbfmUNGshFmZr0sjPrYPD1R06TZs+/7RsMXnx1c79mFGEQ4wqzDOBHKQ
+xeDhNJk+BcE0QABsqF8AA2XC2/dK14QCljKLC84k1zTFTnh8duN2eAalaPQFFOoj
+4AnonUnswJ45zIx5V2BdG+oqO5dwo/cEukKgAEL8T2IJ9Cqlmh2sPbMqYC8cODq6
+YcugMznxrfHV5LNThfkvwMe26+vv68r65zalPDy0M+cUMTMyBVY4TL3fejrloY2t
+YMhPJIclAgMBAAECggEAPXdd/u9NRbGQX6hhTFuEIZOEw1F80MLaCaNzU1kExskN
+01icom0W5LX4UZhiAK0OTsUtlRhwHh1qWfXkd777uX0UkKycDC8laGByra7Nwb7n
+ky8oK77Rh5RptyiNmXflxd3wsJ5k7BczPXTMQL3L53vyLMJh2vKPwhcorrJlS+Pi
+JjINMaR4IrDlpMYlrn9NTjsGr+mj/pdmKfU/KVXeKzFcwKTjUnDJNSbGDIC0AxaJ
+dGU0yIX9MPW+p5szcA9o22UWW4LsEFY4YABeCqbm9/UQt3jWVMjCy4AOgr/9HWSR
+DvXI/Xtdl3CTCr8+qDnhBaUI27z+UelZfTBFKUb8AQKBgQD6SmtrTBgEfb6tuxJw
+AAHRuUcWGjatZ7X+meHRC9B7UPxUrKl9tU5NC7Gz6YMt+vr4bNMwykI6Ndj+4tSJ
+KqsAC86v19CH4usMBLZ68MeTRvtQGiPah71syYrxf0uvYOx/KzUUBX240Ls+lEbE
+W33psMoNAezUPpJwKx7CMjcBgQKBgQDo6VaT59bKRc3DXJvqFjd7TPIex+ny6JK+
+8oOwyyFFBwkzfymoOxN4lxSrE6yf7uTemRRn+RIH3UGDottIDqzhjvtcV5uODeIN
+8WzxTbl759qIxt+z7aF7SkwJLJAAZS3qqCXKtMBo7ln4xKaoRLT2RohqD1YXGrg8
+wmYcUZoPpQKBgQCm2QVSuZ8pH0oFNjfMQbT0wbYJnd/lKMXBu4M1f9Ky4gHT0GYM
+Ttirs6f6byfrduvmv2TpmWscsti80SktZywnE7fssMlqTHKzyFB9FBV2sFLHyyUr
+gGFeK9xbsKgbeVkuTPdNKXvtv/eSd/XU38jIB/opQadGtY+ZBqWyfxb8AQKBgBLc
+SlmBzZ/llSr7xdhn4ihG69hYQfacpL13r/hSCqinUDRuWLY5ynLacR8FYdY1pyzr
+Yn6k6bPfU93QA0fLgG5ngK1SntMbBrIwWa0UqS+Cb+zhhd3xIUF1m8CmbibKCrTU
+1vKaPnaAzqJZclFv9uN2hLdp9IO8cyzgZRpn9TzNAoGAUfZF1983qknfBgD8Lgm3
+zzKYtc8q2Ukatfo4VCp66CEprbLcBq5mKx6JiBoMGqU8SI5XVG0F0aHH2n8gImcu
+bO0vtEldDc1ylZ/H7xhHFWlMzmTlsbHdHVtetFfKLTpjq6duvgLA12lJNHNVu3OU
+Z1bRWDeZIP70+jdYrmSoVi8=
 -----END PRIVATE KEY-----
diff --git a/Lib/slapdtest/certs/client.pem b/Lib/slapdtest/certs/client.pem
index 33b95a7..ca2989c 100644
--- a/Lib/slapdtest/certs/client.pem
+++ b/Lib/slapdtest/certs/client.pem
@@ -5,31 +5,31 @@ Certificate:
     Signature Algorithm: sha256WithRSAEncryption
         Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
         Validity
-            Not Before: Dec  2 11:57:48 2017 GMT
-            Not After : Dec  2 11:57:48 2027 GMT
+            Not Before: Apr 12 18:52:38 2019 GMT
+            Not After : Mar  1 18:52:38 3019 GMT
         Subject: C=DE, O=python-ldap, OU=slapd-test, CN=client
         Subject Public Key Info:
             Public Key Algorithm: rsaEncryption
                 Public-Key: (2048 bit)
                 Modulus:
-                    00:c6:c6:f6:cc:11:b6:a1:56:22:b8:3f:a6:a8:59:
-                    49:1c:88:87:f5:74:46:13:06:e5:3c:33:57:92:85:
-                    11:14:67:76:2d:ab:f7:9e:e3:6e:69:5d:1c:b1:c4:
-                    3a:c0:6b:cc:a0:46:3a:ac:ee:d6:ae:c2:18:00:28:
-                    db:8f:44:6c:b0:ee:1c:99:d4:ab:b8:bd:03:9b:c1:
-                    7f:6b:cf:21:d0:e8:58:fa:d3:a5:38:1c:76:d8:7c:
-                    05:5b:d0:01:7b:3b:32:db:84:d1:f7:8a:c0:91:b4:
-                    d7:26:11:44:98:1b:53:64:7e:7f:73:42:45:78:a7:
-                    3d:8a:24:ce:1e:3e:de:06:c8:62:55:d0:d8:51:51:
-                    d1:dd:c2:0b:16:8a:e4:6d:d8:42:11:3f:02:d7:76:
-                    47:ff:e2:cb:e6:d8:46:a1:00:96:b1:f9:23:1a:65:
-                    87:bf:96:0f:70:f5:20:71:97:a0:d3:68:5f:65:16:
-                    9a:0f:ab:37:96:f8:a9:d7:0a:61:3c:7b:ea:92:59:
-                    e6:bc:12:39:33:21:15:da:e0:a2:7f:12:75:0c:3f:
-                    67:d4:2f:e2:5e:d9:a8:06:17:66:1f:08:87:7d:62:
-                    f2:d3:41:5f:18:0e:fe:c8:9b:54:a9:d2:c6:c8:50:
-                    3a:16:5f:5e:06:0d:46:d9:f1:c0:6b:c1:f3:b2:23:
-                    44:df
+                    00:e3:b7:93:ba:9d:1a:e7:01:63:e6:4f:42:6f:44:
+                    b0:47:31:c9:65:97:dd:88:58:5f:ce:e0:61:1d:70:
+                    9e:5f:6c:1c:4f:dc:1c:4a:d0:3b:fe:e9:a4:5c:47:
+                    b6:f6:a3:47:fa:7e:4f:db:80:d7:a9:5d:fb:ff:31:
+                    70:d0:70:59:44:49:b7:e6:50:d1:ac:84:59:99:af:
+                    4b:23:3e:b6:0f:0f:54:74:e9:36:6c:fb:fe:d1:b0:
+                    c5:e7:c7:57:3b:f6:61:46:11:0e:30:ab:30:ce:04:
+                    72:90:c5:e0:e1:34:99:3e:05:c1:34:40:00:6c:a8:
+                    5f:00:03:65:c2:db:f7:4a:d7:84:02:96:32:8b:0b:
+                    ce:24:d7:34:c5:4e:78:7c:76:e3:76:78:06:a5:68:
+                    f4:05:14:ea:23:e0:09:e8:9d:49:ec:c0:9e:39:cc:
+                    8c:79:57:60:5d:1b:ea:2a:3b:97:70:a3:f7:04:ba:
+                    42:a0:00:42:fc:4f:62:09:f4:2a:a5:9a:1d:ac:3d:
+                    b3:2a:60:2f:1c:38:3a:ba:61:cb:a0:33:39:f1:ad:
+                    f1:d5:e4:b3:53:85:f9:2f:c0:c7:b6:eb:eb:ef:eb:
+                    ca:fa:e7:36:a5:3c:3c:b4:33:e7:14:31:33:32:05:
+                    56:38:4c:bd:df:7a:3a:e5:a1:8d:ad:60:c8:4f:24:
+                    87:25
                 Exponent: 65537 (0x10001)
         X509v3 extensions:
             X509v3 Basic Constraints: critical
@@ -39,45 +39,45 @@ Certificate:
             X509v3 Extended Key Usage: critical
                 TLS Web Client Authentication
             X509v3 Subject Key Identifier: 
-                67:63:38:F4:B4:BC:F3:6B:BC:74:0E:7C:27:C9:BB:C2:CC:58:AC:16
+                4F:E7:35:C7:C8:C1:01:C3:7C:53:86:B9:BF:AE:8B:D6:45:A2:78:20
             X509v3 Authority Key Identifier: 
-                keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4
+                keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
 
     Signature Algorithm: sha256WithRSAEncryption
-         76:24:42:6b:33:4f:d6:59:07:48:5b:04:9c:3c:d3:3f:63:80:
-         75:4d:78:d7:d5:85:b1:77:81:31:a3:91:cb:c9:a3:8c:0e:00:
-         28:08:74:71:6c:fc:83:8c:80:ec:1c:e8:ee:83:e0:7f:49:3b:
-         f3:42:33:5a:1f:68:0c:a5:41:42:ce:bf:77:29:07:f2:18:a7:
-         81:17:d7:76:47:04:d9:8a:dd:e8:5a:26:26:ea:a4:76:70:e1:
-         f1:fa:e1:db:bc:f2:24:b2:37:a8:58:2f:e3:66:89:77:02:55:
-         87:ef:3c:1f:66:ce:4e:86:b3:4c:57:43:86:7f:4c:ab:5a:33:
-         dd:ca:e3:2f:3b:af:b4:43:5a:53:8b:e0:12:da:e7:c0:13:76:
-         b2:68:d5:14:f8:1a:07:ce:8a:87:5c:91:bd:35:d7:83:c6:2a:
-         a4:e0:92:50:01:b9:c2:fa:69:06:5c:8a:80:ee:9c:24:f9:49:
-         64:e3:59:c1:a6:69:29:ce:b7:89:20:a9:7c:d6:9f:df:2a:d1:
-         a4:98:2a:6d:7b:93:6a:52:e3:ae:de:1a:d8:f3:2e:cf:02:7e:
-         ba:9a:fa:f4:b3:b5:6e:9a:23:10:70:53:53:30:d5:8a:32:35:
-         01:52:58:6d:9d:f5:8e:bb:b9:76:bd:41:16:88:26:f8:d3:ce:
-         70:03:c8:59
+         1c:90:5f:cf:18:48:95:4d:9d:d3:8e:6d:d1:69:19:1e:7b:3f:
+         1f:48:7c:c8:0d:2f:c4:53:0f:89:23:f4:be:ea:b4:7a:c6:dd:
+         cc:18:0f:e7:34:ea:2c:d4:07:0d:65:78:e8:20:40:3f:36:ef:
+         2c:00:31:69:e6:20:48:65:be:57:03:0e:69:ff:b9:83:59:99:
+         7d:4d:86:98:14:5b:8e:39:25:3a:a8:6d:51:dc:45:a5:0f:cd:
+         f3:7a:fd:55:af:5f:55:75:20:03:f5:4a:75:6a:79:2f:76:84:
+         f6:4e:3d:1d:59:45:9a:b1:6a:57:6f:16:76:76:f8:df:6e:96:
+         d5:25:27:34:4b:21:d8:c9:9a:36:55:45:a0:43:16:43:68:93:
+         37:af:81:89:06:d1:56:1b:9e:0f:62:40:ad:3c:4c:f5:ef:6c:
+         a2:a4:7f:f2:fa:78:9c:0d:c0:19:f1:10:e8:d8:cf:03:67:3c:
+         2d:4d:f3:5d:67:5c:41:a7:4f:d6:c5:0e:ff:2c:04:dd:23:bb:
+         85:44:8e:25:ac:15:a3:82:fa:a4:4f:fa:1d:87:f0:58:dc:ae:
+         53:05:b9:81:e8:cb:e5:0c:ac:a5:74:68:03:f9:22:a0:45:b6:
+         62:58:e0:98:d9:8c:54:a4:22:03:7a:37:12:eb:7d:b1:ad:45:
+         60:8e:7a:df
 -----BEGIN CERTIFICATE-----
-MIIDkjCCAnqgAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
+MIIDlDCCAnygAwIBAgIBAzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
 MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV
-BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ4WhcNMjcxMjAy
-MTE1NzQ4WjBJMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR
-BgNVBAsMCnNsYXBkLXRlc3QxDzANBgNVBAMMBmNsaWVudDCCASIwDQYJKoZIhvcN
-AQEBBQADggEPADCCAQoCggEBAMbG9swRtqFWIrg/pqhZSRyIh/V0RhMG5TwzV5KF
-ERRndi2r957jbmldHLHEOsBrzKBGOqzu1q7CGAAo249EbLDuHJnUq7i9A5vBf2vP
-IdDoWPrTpTgcdth8BVvQAXs7MtuE0feKwJG01yYRRJgbU2R+f3NCRXinPYokzh4+
-3gbIYlXQ2FFR0d3CCxaK5G3YQhE/Atd2R//iy+bYRqEAlrH5Ixplh7+WD3D1IHGX
-oNNoX2UWmg+rN5b4qdcKYTx76pJZ5rwSOTMhFdrgon8SdQw/Z9Qv4l7ZqAYXZh8I
-h31i8tNBXxgO/sibVKnSxshQOhZfXgYNRtnxwGvB87IjRN8CAwEAAaN4MHYwDAYD
-VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
-AwIwHQYDVR0OBBYEFGdjOPS0vPNrvHQOfCfJu8LMWKwWMB8GA1UdIwQYMBaAFDsf
-MvT+V9FvSZFV8iTxCmY7pe7UMA0GCSqGSIb3DQEBCwUAA4IBAQB2JEJrM0/WWQdI
-WwScPNM/Y4B1TXjX1YWxd4Exo5HLyaOMDgAoCHRxbPyDjIDsHOjug+B/STvzQjNa
-H2gMpUFCzr93KQfyGKeBF9d2RwTZit3oWiYm6qR2cOHx+uHbvPIksjeoWC/jZol3
-AlWH7zwfZs5OhrNMV0OGf0yrWjPdyuMvO6+0Q1pTi+AS2ufAE3ayaNUU+BoHzoqH
-XJG9NdeDxiqk4JJQAbnC+mkGXIqA7pwk+Ulk41nBpmkpzreJIKl81p/fKtGkmCpt
-e5NqUuOu3hrY8y7PAn66mvr0s7VumiMQcFNTMNWKMjUBUlhtnfWOu7l2vUEWiCb4
-085wA8hZ
+BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz
+MDExODUyMzhaMEkxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET
+MBEGA1UECwwKc2xhcGQtdGVzdDEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA47eTup0a5wFj5k9Cb0SwRzHJZZfdiFhfzuBh
+HXCeX2wcT9wcStA7/umkXEe29qNH+n5P24DXqV37/zFw0HBZREm35lDRrIRZma9L
+Iz62Dw9UdOk2bPv+0bDF58dXO/ZhRhEOMKswzgRykMXg4TSZPgXBNEAAbKhfAANl
+wtv3SteEApYyiwvOJNc0xU54fHbjdngGpWj0BRTqI+AJ6J1J7MCeOcyMeVdgXRvq
+KjuXcKP3BLpCoABC/E9iCfQqpZodrD2zKmAvHDg6umHLoDM58a3x1eSzU4X5L8DH
+tuvr7+vK+uc2pTw8tDPnFDEzMgVWOEy933o65aGNrWDITySHJQIDAQABo3gwdjAM
+BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEF
+BQcDAjAdBgNVHQ4EFgQUT+c1x8jBAcN8U4a5v66L1kWieCAwHwYDVR0jBBgwFoAU
+vXjVSvGQlsXo7GZJI0cDXyZzhrIwDQYJKoZIhvcNAQELBQADggEBAByQX88YSJVN
+ndOObdFpGR57Px9IfMgNL8RTD4kj9L7qtHrG3cwYD+c06izUBw1leOggQD827ywA
+MWnmIEhlvlcDDmn/uYNZmX1NhpgUW445JTqobVHcRaUPzfN6/VWvX1V1IAP1SnVq
+eS92hPZOPR1ZRZqxaldvFnZ2+N9ultUlJzRLIdjJmjZVRaBDFkNokzevgYkG0VYb
+ng9iQK08TPXvbKKkf/L6eJwNwBnxEOjYzwNnPC1N811nXEGnT9bFDv8sBN0ju4VE
+jiWsFaOC+qRP+h2H8FjcrlMFuYHoy+UMrKV0aAP5IqBFtmJY4JjZjFSkIgN6NxLr
+fbGtRWCOet8=
 -----END CERTIFICATE-----
diff --git a/Lib/slapdtest/certs/gencerts.sh b/Lib/slapdtest/certs/gencerts.sh
index 7a971a3..8a99db5 100755
--- a/Lib/slapdtest/certs/gencerts.sh
+++ b/Lib/slapdtest/certs/gencerts.sh
@@ -29,7 +29,7 @@ openssl ca -selfsign \
     -in $CATMPDIR/ca.csr \
     -out $CAOUTDIR/ca.pem \
     -extensions ca_ext \
-    -days 3563 \
+    -days 356300 \
     -batch
 
 # server cert
diff --git a/Lib/slapdtest/certs/server.key b/Lib/slapdtest/certs/server.key
index a48ee56..a891670 100644
--- a/Lib/slapdtest/certs/server.key
+++ b/Lib/slapdtest/certs/server.key
@@ -1,28 +1,28 @@
 -----BEGIN PRIVATE KEY-----
-MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAgcI7Pj89Aw4r
-rb+N8j3t1ynJgXRhQNxbxQcQmUCi8AtpGKNXu+aM9u2HxZ677ALfhsEivtQA5QKz
-Ll5G2G2IQa7uzgIco73OL/kMZIJt7sKfnvfACtSoOlD0IyOzVEEu0AVA7hMHb6Ul
-I0mCNfdk2FWFbc1nUmIAEBhIODbFoNW+Rc6lz94NsUqArDayvWpAsXkUbubQikpi
-KNX1OpOOC9GUbEhwI/G90ZnUg9STk/2oxsvwLlYdOhGhpyfl53tTu7eLMBriMxFl
-UTvkY1GUgPj0fA/giWtCerGOyeKu1hFlbS4LjborsfrlyYPwfwTg3YL4hVnt9fF0
-rpjzY1mlAgMBAAECggEAJY6rSEeiqtKXxynEv3rNXkOmIWwiOn8e/sB32mMr2x4d
-+8kUxR8hocrjGKQTjfJDtTxjHdZBIlOLrU2UkxnSdMzrxidm/hNsCngNjL9nOu9k
-BSRMjakPSCrodFkOtAPyG6H2BG7uQ3siqxYxVzgUJhaWyMtdUZUfDYgWVLCy7udU
-5ML/OTOi7virueMmshjXoyrDug9OpiEMKiLu3ndAaDk/26m05ePAXB6TjW8SFw1B
-qn7cITSG0G5MZ9pOw0KwT9irY1SdppBHVWIg7dkYWRCni0BPCFewastU+GVKH5PJ
-+dYSvafhkEGD1bBu484KN9yX1BcHV41ZKR8pGgMM2QKBgQD3/0R2vZsTxoO1CHNI
-IT7nBnuPIOP45iTFm/SNRY7e4dhQBy6HM6JD3Sr6Iksm8jRoboz+tnAso6l6QHRS
-842uqBiOHdnka2RslDmrEun1lJv1MWuPM8JN0o8pYjVG/IRtaAFnYSEk72UoNy2h
-bHC4OGFNwMbAadVm7DK5OiMfXwKBgQDGuBRxz7jkVZoMbbaeIqmGZAIejWkJweDZ
-AK+txM+6Sg+Li14t190N3Xf6tyyidKhUAEWaINzLjZB+luxNaDXtxqWzLYHCwQKA
-qfrjWVeZOS1clLya7jwl1jJqBtBiGKHv9eRL21hgX/9gX3odxqFMvX3vm6L7F1q1
-5CNApW0ZewKBgGO8qNcsWBLy8oM7G8n1fOvCwqyEaMrwG/fRSeALCnN+1tUQnljH
-nkm2yBMC+cB3Bja9xzylOKXrSDyfcWjvBJsqhX2aacggnKnCTxMLL0aR9sr8jipw
-gYN03Bijo5Oh+MxbWL0v5fmJweATmOljyE1+dzui/QvjRGz5L0kpJXj3AoGBAIa4
-3+t1B4WN312TuB4no8Tf4mvyNQcPcS/Nfk0RxD8o3Lcfal8sHMq8ng3Ux6bv7frd
-IFLo+qfpts+L5HJqNz2X0ljSfkmZ7udp1hTySigwEmfU0rU61H5WZGFrczU+O/Ni
-Qj+HWrgj/Q/KSxEKy+oqAcpDOtB+Odpc6+V1Aa0nAoGBAItWHP9UjTNFqOfyjZhG
-qaUiZd1S2KyRR0l/lVcn+rJ46Yg5i+lMGwHMF1xPyWH4ELz+QCUX3doOI4yB2ikg
-XXFcc8/bqgaR4AfOvP98T86s7+f33kaAKZsgyAFB2cjo+fz8ArTz+GjPeHbiOPaR
-Ra7+BVwl9GE0+bCdirq+99GO
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsBk0ml3ERFJyg
+I6ujIJYERVU4doTZZd4r4z/LOef0hyiYiIQAc9wetaoZpM+bl4Eherxy9SBaCBwR
+zefbaYQz2f2hdEDb+sISOiTke1eiF2ugYNlS55Wk1KnCnORE9bjcSNLPsscoUSzE
+2bnBSoUwdiVK18YOCZR6GTeC8eA3ekvlR+9g+FBOgQ9+StXPDdq+iIAGXZREJIua
+munErtTOw85De4YFCnzGw3UeCITDD4wFmI2IWphRFwWPsSDwUJfATA8S+7Rm4vwr
+Qj726gUDlicTzPXKhJjXjj6XL7xXHfpQwMPkBCrxesKceHMJ+mrRsuuqHciuixRi
+g94mILElAgMBAAECggEADG5oJOHMye8zYl8xiBhSvvxDrFDkSNGTvJgvhAArQwCB
+boRvBZlZzt5R7Ih8eEH6kvDLrYMJU3hCjwbSOojlhNm7+m7sQPleDPMmt1wyeQQ4
+Qt681cDmj4LOwcGUvWcEdObOVTQWMFOtaIxTYCSCe34OM9pj9Z+7mxc3a78O9PND
+Ib/CwcTA1OyoupzkKirqkdLXwK3x2aT/1TMaPX94taHB51cxXc7AglL9QnuCkuaG
+krqrexy3rGimzsP3OwQGEUjWKcZVSSPT8/k1pPE9hRgOqBy05BfkAzlebdvc3GO5
+AbZk0NX2sfVHl4dTEXs/hTBCTQ3XmaltumQ9MdL+AQKBgQDg2I5QxBA2UHb8vCtK
+f31kfG6YQc4MkoslrrMrtJjZqDYaLZPS1ARPSfYRqcc+7GDreuLmw39f8ZECd+2W
+BYUqzZv9g13R9DY99g0/sINnZGsESwfIdLNNlHvVx2UrD5ybCj4vLhuPsVV7XlWs
+cpl+rcuBVpqy8UIXifQ/Z3xLvwKBgQDD3CLjuC0mcTO2sIWqEHqVkc8CY2NJA2Qh
+C78fwpaCqJUUdWnS69QbRGWgkFJL+oO8lQVQ1bXhZLHyQmy7Z5d5olCH6AW4GRnf
+hBAnKJ+QTm9B6QVWzjUuHuOeCukfiTQbha14pOS9ar3X2QFWjDnzCRrnAxJmoY3H
+BJATLHhMGwKBgQDSxAy7xt4Pm+O9y8Gk5tcq771X+i9k96V54EZRzMuPFDAK3/h2
+o4marZD9Q7Hi2P+NHTc+67klvbKZpsPOYkRPOEdmH9M9cPe7oz8OGa9DpwzuDEsy
+a7p8GZjvbyb1c3/wkWxzG3x4eNnReD9FFHOwHMfr6LvAy4iRuh57pM0NzwKBgDY3
+1DixnV4M7EHgb7/6O9T3vhRtKujlVWyIcen61etpe4tkTV0kB11c+70M9pstyBYG
+MqiD4It6coAbvznJnXcAZcaZhivGVxE237nXVwR9kfLu7JlxD+uqhVwUrSAbvR75
+TGIfU2rUB6We3u30d349wQK+KPPcOQEk1DValBqNAoGBAKfXOXgFBkIVW79fOkup
+aIZXdEmU3Up61Oo0KDbxsg4l73NnnvuEnNMBTx3nT3KCVIAcQL9MNpLX/Z0HjOn1
+aiWVtTNq2OFL0V0HueBhbkFiWp551jTS7LjndCYHpUB/B8/wXP0kxHUm8HrQrRvK
+DhV3zcxsXts1INidXjzzOkPi
 -----END PRIVATE KEY-----
diff --git a/Lib/slapdtest/certs/server.pem b/Lib/slapdtest/certs/server.pem
index 7e75059..25ba06c 100644
--- a/Lib/slapdtest/certs/server.pem
+++ b/Lib/slapdtest/certs/server.pem
@@ -5,31 +5,31 @@ Certificate:
     Signature Algorithm: sha256WithRSAEncryption
         Issuer: C=DE, O=python-ldap, OU=slapd-test, CN=Python LDAP Test CA
         Validity
-            Not Before: Dec  2 11:57:48 2017 GMT
-            Not After : Dec  2 11:57:48 2027 GMT
+            Not Before: Apr 12 18:52:38 2019 GMT
+            Not After : Mar  1 18:52:38 3019 GMT
         Subject: C=DE, O=python-ldap, OU=slapd-test, CN=server cert for localhost
         Subject Public Key Info:
             Public Key Algorithm: rsaEncryption
                 Public-Key: (2048 bit)
                 Modulus:
-                    00:c0:81:c2:3b:3e:3f:3d:03:0e:2b:ad:bf:8d:f2:
-                    3d:ed:d7:29:c9:81:74:61:40:dc:5b:c5:07:10:99:
-                    40:a2:f0:0b:69:18:a3:57:bb:e6:8c:f6:ed:87:c5:
-                    9e:bb:ec:02:df:86:c1:22:be:d4:00:e5:02:b3:2e:
-                    5e:46:d8:6d:88:41:ae:ee:ce:02:1c:a3:bd:ce:2f:
-                    f9:0c:64:82:6d:ee:c2:9f:9e:f7:c0:0a:d4:a8:3a:
-                    50:f4:23:23:b3:54:41:2e:d0:05:40:ee:13:07:6f:
-                    a5:25:23:49:82:35:f7:64:d8:55:85:6d:cd:67:52:
-                    62:00:10:18:48:38:36:c5:a0:d5:be:45:ce:a5:cf:
-                    de:0d:b1:4a:80:ac:36:b2:bd:6a:40:b1:79:14:6e:
-                    e6:d0:8a:4a:62:28:d5:f5:3a:93:8e:0b:d1:94:6c:
-                    48:70:23:f1:bd:d1:99:d4:83:d4:93:93:fd:a8:c6:
-                    cb:f0:2e:56:1d:3a:11:a1:a7:27:e5:e7:7b:53:bb:
-                    b7:8b:30:1a:e2:33:11:65:51:3b:e4:63:51:94:80:
-                    f8:f4:7c:0f:e0:89:6b:42:7a:b1:8e:c9:e2:ae:d6:
-                    11:65:6d:2e:0b:8d:ba:2b:b1:fa:e5:c9:83:f0:7f:
-                    04:e0:dd:82:f8:85:59:ed:f5:f1:74:ae:98:f3:63:
-                    59:a5
+                    00:ac:06:4d:26:97:71:11:14:9c:a0:23:ab:a3:20:
+                    96:04:45:55:38:76:84:d9:65:de:2b:e3:3f:cb:39:
+                    e7:f4:87:28:98:88:84:00:73:dc:1e:b5:aa:19:a4:
+                    cf:9b:97:81:21:7a:bc:72:f5:20:5a:08:1c:11:cd:
+                    e7:db:69:84:33:d9:fd:a1:74:40:db:fa:c2:12:3a:
+                    24:e4:7b:57:a2:17:6b:a0:60:d9:52:e7:95:a4:d4:
+                    a9:c2:9c:e4:44:f5:b8:dc:48:d2:cf:b2:c7:28:51:
+                    2c:c4:d9:b9:c1:4a:85:30:76:25:4a:d7:c6:0e:09:
+                    94:7a:19:37:82:f1:e0:37:7a:4b:e5:47:ef:60:f8:
+                    50:4e:81:0f:7e:4a:d5:cf:0d:da:be:88:80:06:5d:
+                    94:44:24:8b:9a:9a:e9:c4:ae:d4:ce:c3:ce:43:7b:
+                    86:05:0a:7c:c6:c3:75:1e:08:84:c3:0f:8c:05:98:
+                    8d:88:5a:98:51:17:05:8f:b1:20:f0:50:97:c0:4c:
+                    0f:12:fb:b4:66:e2:fc:2b:42:3e:f6:ea:05:03:96:
+                    27:13:cc:f5:ca:84:98:d7:8e:3e:97:2f:bc:57:1d:
+                    fa:50:c0:c3:e4:04:2a:f1:7a:c2:9c:78:73:09:fa:
+                    6a:d1:b2:eb:aa:1d:c8:ae:8b:14:62:83:de:26:20:
+                    b1:25
                 Exponent: 65537 (0x10001)
         X509v3 extensions:
             X509v3 Basic Constraints: critical
@@ -39,48 +39,48 @@ Certificate:
             X509v3 Extended Key Usage: critical
                 TLS Web Server Authentication
             X509v3 Subject Key Identifier: 
-                1B:78:45:40:0D:50:8A:8B:3B:C1:0A:F8:3F:7A:48:7B:A6:3C:28:09
+                08:D1:86:1B:82:0A:4F:71:31:E4:F5:31:23:CC:67:3B:FA:84:3B:A0
             X509v3 Authority Key Identifier: 
-                keyid:3B:1F:32:F4:FE:57:D1:6F:49:91:55:F2:24:F1:0A:66:3B:A5:EE:D4
+                keyid:BD:78:D5:4A:F1:90:96:C5:E8:EC:66:49:23:47:03:5F:26:73:86:B2
 
             X509v3 Subject Alternative Name: 
                 DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
     Signature Algorithm: sha256WithRSAEncryption
-         ad:08:3f:7d:b1:09:a1:a5:6c:c3:58:80:1d:e5:33:a5:bb:c0:
-         33:39:95:aa:88:ee:c4:8e:38:3b:59:a7:0e:39:74:6c:fe:11:
-         33:5e:fa:50:cb:20:4b:67:b7:c9:5e:96:a7:9e:d8:47:46:e1:
-         ab:fe:5d:8b:9a:2d:1a:1b:43:08:f9:93:0f:2a:e3:ce:83:4a:
-         94:cd:02:f0:8e:25:f2:41:0d:55:10:f5:4c:5b:39:8b:77:5e:
-         ab:78:16:64:a1:48:d5:e1:f6:69:9a:0f:d8:30:a6:cc:92:4d:
-         81:df:46:74:ab:cf:1d:b7:d4:01:b9:6d:d5:f4:14:b8:d5:54:
-         84:79:11:42:69:55:7f:74:ce:01:96:2f:3f:51:23:b3:11:fb:
-         72:dc:4c:b9:a3:89:ef:31:e4:c0:49:06:fa:8d:09:71:e1:c1:
-         74:a9:ed:f8:96:87:67:16:b5:5d:16:5d:59:70:ff:1c:b5:a1:
-         6c:d2:22:11:3a:0e:6f:76:9b:69:cb:f3:85:a7:79:ad:53:f5:
-         34:e8:87:cc:dd:09:51:25:e0:28:ee:79:a0:a3:dc:0a:dd:f0:
-         1b:e3:c9:5f:14:d3:95:f5:12:4d:23:95:45:2c:3c:32:94:ad:
-         ce:1e:a0:5f:e6:e8:28:c6:f9:c7:fb:57:06:ad:0b:eb:86:ca:
-         0e:d2:a8:67
+         88:60:af:be:11:c4:aa:dc:9b:f1:e7:14:da:20:aa:6f:2f:06:
+         ae:38:b2:7c:ac:90:81:22:51:7e:cb:26:15:6e:fe:67:98:c1:
+         0d:dc:aa:39:98:2b:d2:cc:3c:ff:1a:92:2f:56:0a:a9:6e:d8:
+         9a:3d:c5:4d:6f:cc:91:2e:e3:4e:bf:22:ab:cb:92:1a:a0:8f:
+         43:cd:82:bc:48:55:c4:95:cf:10:6b:6a:31:19:92:7d:e0:06:
+         05:6f:0b:33:e7:2a:37:42:f9:ec:1b:29:99:e1:58:0c:01:a7:
+         c3:8b:58:71:21:9f:61:8c:a7:fb:b6:7e:32:8b:a9:4e:c7:1f:
+         f6:46:e8:dd:ac:a6:4c:53:f8:4d:93:e4:ec:73:ab:0b:be:98:
+         c5:78:c4:92:c0:4c:78:47:52:2f:93:07:67:20:a4:5a:7f:59:
+         7e:4f:48:53:20:0d:37:bb:06:f8:44:42:64:b4:94:15:43:d1:
+         4c:51:f3:97:1d:2d:cd:db:b9:bb:1a:69:10:89:7d:ae:1d:0d:
+         94:78:45:29:cd:c4:42:67:67:96:05:bf:da:aa:23:65:7b:04:
+         ff:b7:ac:9d:ee:0b:e7:0f:c1:c5:0b:48:fe:0f:d6:3f:d8:b4:
+         77:12:bb:f5:91:4f:43:e6:01:3f:a4:c0:ea:8c:c6:68:99:8e:
+         49:e8:c4:8b
 -----BEGIN CERTIFICATE-----
-MIID1TCCAr2gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
+MIID1zCCAr+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJERTEU
 MBIGA1UECgwLcHl0aG9uLWxkYXAxEzARBgNVBAsMCnNsYXBkLXRlc3QxHDAaBgNV
-BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwHhcNMTcxMjAyMTE1NzQ4WhcNMjcxMjAy
-MTE1NzQ4WjBcMQswCQYDVQQGEwJERTEUMBIGA1UECgwLcHl0aG9uLWxkYXAxEzAR
-BgNVBAsMCnNsYXBkLXRlc3QxIjAgBgNVBAMMGXNlcnZlciBjZXJ0IGZvciBsb2Nh
-bGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAgcI7Pj89Aw4r
-rb+N8j3t1ynJgXRhQNxbxQcQmUCi8AtpGKNXu+aM9u2HxZ677ALfhsEivtQA5QKz
-Ll5G2G2IQa7uzgIco73OL/kMZIJt7sKfnvfACtSoOlD0IyOzVEEu0AVA7hMHb6Ul
-I0mCNfdk2FWFbc1nUmIAEBhIODbFoNW+Rc6lz94NsUqArDayvWpAsXkUbubQikpi
-KNX1OpOOC9GUbEhwI/G90ZnUg9STk/2oxsvwLlYdOhGhpyfl53tTu7eLMBriMxFl
-UTvkY1GUgPj0fA/giWtCerGOyeKu1hFlbS4LjborsfrlyYPwfwTg3YL4hVnt9fF0
-rpjzY1mlAgMBAAGjgacwgaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAw
-FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFBt4RUANUIqLO8EK+D96
-SHumPCgJMB8GA1UdIwQYMBaAFDsfMvT+V9FvSZFV8iTxCmY7pe7UMCwGA1UdEQQl
-MCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0B
-AQsFAAOCAQEArQg/fbEJoaVsw1iAHeUzpbvAMzmVqojuxI44O1mnDjl0bP4RM176
-UMsgS2e3yV6Wp57YR0bhq/5di5otGhtDCPmTDyrjzoNKlM0C8I4l8kENVRD1TFs5
-i3deq3gWZKFI1eH2aZoP2DCmzJJNgd9GdKvPHbfUAblt1fQUuNVUhHkRQmlVf3TO
-AZYvP1EjsxH7ctxMuaOJ7zHkwEkG+o0JceHBdKnt+JaHZxa1XRZdWXD/HLWhbNIi
-EToOb3abacvzhad5rVP1NOiHzN0JUSXgKO55oKPcCt3wG+PJXxTTlfUSTSOVRSw8
-MpStzh6gX+boKMb5x/tXBq0L64bKDtKoZw==
+BAMME1B5dGhvbiBMREFQIFRlc3QgQ0EwIBcNMTkwNDEyMTg1MjM4WhgPMzAxOTAz
+MDExODUyMzhaMFwxCzAJBgNVBAYTAkRFMRQwEgYDVQQKDAtweXRob24tbGRhcDET
+MBEGA1UECwwKc2xhcGQtdGVzdDEiMCAGA1UEAwwZc2VydmVyIGNlcnQgZm9yIGxv
+Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKwGTSaXcREU
+nKAjq6MglgRFVTh2hNll3ivjP8s55/SHKJiIhABz3B61qhmkz5uXgSF6vHL1IFoI
+HBHN59tphDPZ/aF0QNv6whI6JOR7V6IXa6Bg2VLnlaTUqcKc5ET1uNxI0s+yxyhR
+LMTZucFKhTB2JUrXxg4JlHoZN4Lx4Dd6S+VH72D4UE6BD35K1c8N2r6IgAZdlEQk
+i5qa6cSu1M7DzkN7hgUKfMbDdR4IhMMPjAWYjYhamFEXBY+xIPBQl8BMDxL7tGbi
+/CtCPvbqBQOWJxPM9cqEmNeOPpcvvFcd+lDAw+QEKvF6wpx4cwn6atGy66odyK6L
+FGKD3iYgsSUCAwEAAaOBpzCBpDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF
+oDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUCNGGG4IKT3Ex5PUx
+I8xnO/qEO6AwHwYDVR0jBBgwFoAUvXjVSvGQlsXo7GZJI0cDXyZzhrIwLAYDVR0R
+BCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3
+DQEBCwUAA4IBAQCIYK++EcSq3Jvx5xTaIKpvLwauOLJ8rJCBIlF+yyYVbv5nmMEN
+3Ko5mCvSzDz/GpIvVgqpbtiaPcVNb8yRLuNOvyKry5IaoI9DzYK8SFXElc8Qa2ox
+GZJ94AYFbwsz5yo3QvnsGymZ4VgMAafDi1hxIZ9hjKf7tn4yi6lOxx/2RujdrKZM
+U/hNk+Tsc6sLvpjFeMSSwEx4R1IvkwdnIKRaf1l+T0hTIA03uwb4REJktJQVQ9FM
+UfOXHS3N27m7GmkQiX2uHQ2UeEUpzcRCZ2eWBb/aqiNlewT/t6yd7gvnD8HFC0j+
+D9Y/2LR3Erv1kU9D5gE/pMDqjMZomY5J6MSL
 -----END CERTIFICATE-----
diff --git a/Makefile b/Makefile
index 2d3293e..8ec46a6 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,11 @@ AUTOPEP8_OPTS=--aggressive
 .PHONY: all
 all:
 
+Modules/constants_generated.h: Lib/ldap/constants.py
+	$(PYTHON) $^ > $@
+	indent Modules/constants_generated.h
+	rm -f Modules/constants_generated.h~
+
 .PHONY: clean
 clean:
 	rm -rf build dist *.egg-info .tox MANIFEST
diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c
index bc26727..da18d57 100644
--- a/Modules/LDAPObject.c
+++ b/Modules/LDAPObject.c
@@ -126,6 +126,7 @@ Tuple_to_LDAPMod(PyObject *tup, int no_op)
     }
 
     lm = PyMem_NEW(LDAPMod, 1);
+
     if (lm == NULL)
         goto nomem;
 
@@ -236,6 +237,7 @@ List_to_LDAPMods(PyObject *list, int no_op)
     }
 
     lms = PyMem_NEW(LDAPMod *, len + 1);
+
     if (lms == NULL)
         goto nomem;
 
@@ -335,7 +337,7 @@ attrs_from_List(PyObject *attrlist, char ***attrsp)
              * internal values that must be treated like const char. Python
              * 3.7 actually returns a const char.
              */
-            attrs[i] = (char *)PyMem_NEW(char *, strlen + 1);
+            attrs[i] = (char *)PyMem_NEW(char, strlen + 1);
 
             if (attrs[i] == NULL)
                 goto nomem;
@@ -415,7 +417,7 @@ l_ldap_unbind_ext(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_unbind_ext");
+        return LDAPerror(self->ldap);
 
     self->valid = 0;
     Py_INCREF(Py_None);
@@ -461,7 +463,7 @@ l_ldap_abandon_ext(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_abandon_ext");
+        return LDAPerror(self->ldap);
 
     Py_INCREF(Py_None);
     return Py_None;
@@ -517,7 +519,7 @@ l_ldap_add_ext(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_add_ext");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -568,7 +570,7 @@ l_ldap_simple_bind(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_simple_bind");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -727,7 +729,7 @@ l_ldap_sasl_bind_s(LDAPObject *self, PyObject *args)
                                              servercred->bv_len);
     }
     else if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "l_ldap_sasl_bind_s");
+        return LDAPerror(self->ldap);
     return PyInt_FromLong(ldaperror);
 }
 
@@ -806,7 +808,7 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (msgid != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_sasl_interactive_bind_s");
+        return LDAPerror(self->ldap);
     return PyInt_FromLong(msgid);
 }
 #endif
@@ -854,7 +856,7 @@ l_ldap_cancel(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_cancel");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -908,7 +910,7 @@ l_ldap_compare_ext(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_compare_ext");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -954,7 +956,7 @@ l_ldap_delete_ext(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_delete_ext");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -1011,7 +1013,7 @@ l_ldap_modify_ext(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_modify_ext");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -1061,7 +1063,7 @@ l_ldap_rename(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_rename");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -1081,12 +1083,11 @@ l_ldap_result4(LDAPObject *self, PyObject *args)
     struct timeval *tvp;
     int res_type;
     LDAPMessage *msg = NULL;
-    PyObject *result_str, *retval, *pmsg, *pyctrls = 0;
+    PyObject *retval, *pmsg, *pyctrls = 0;
     int res_msgid = 0;
     char *retoid = 0;
     PyObject *valuestr = NULL;
     int result = LDAP_SUCCESS;
-    char **refs = NULL;
     LDAPControl **serverctrls = 0;
 
     if (!PyArg_ParseTuple
@@ -1109,7 +1110,7 @@ l_ldap_result4(LDAPObject *self, PyObject *args)
     LDAP_END_ALLOW_THREADS(self);
 
     if (res_type < 0)   /* LDAP or system error */
-        return LDAPerror(self->ldap, "ldap_result4");
+        return LDAPerror(self->ldap);
 
     if (res_type == 0) {
         /* Polls return (None, None, None, None); timeouts raise an exception */
@@ -1157,23 +1158,15 @@ l_ldap_result4(LDAPObject *self, PyObject *args)
         }
 
         LDAP_BEGIN_ALLOW_THREADS(self);
-        rc = ldap_parse_result(self->ldap, msg, &result, NULL, NULL, &refs,
+        rc = ldap_parse_result(self->ldap, msg, &result, NULL, NULL, NULL,
                                &serverctrls, 0);
         LDAP_END_ALLOW_THREADS(self);
     }
 
     if (result != LDAP_SUCCESS) {       /* result error */
-        char *e, err[1024];
-
-        if (result == LDAP_REFERRAL && refs && refs[0]) {
-            snprintf(err, sizeof(err), "Referral:\n%s", refs[0]);
-            e = err;
-        }
-        else
-            e = "ldap_parse_result";
-        ldap_msgfree(msg);
+        ldap_controls_free(serverctrls);
         Py_XDECREF(valuestr);
-        return LDAPerror(self->ldap, e);
+        return LDAPraise_for_message(self->ldap, msg);
     }
 
     if (!(pyctrls = LDAPControls_to_List(serverctrls))) {
@@ -1182,36 +1175,29 @@ l_ldap_result4(LDAPObject *self, PyObject *args)
         LDAP_BEGIN_ALLOW_THREADS(self);
         ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &err);
         LDAP_END_ALLOW_THREADS(self);
+        ldap_controls_free(serverctrls);
         ldap_msgfree(msg);
         Py_XDECREF(valuestr);
-        return LDAPerror(self->ldap, "LDAPControls_to_List");
+        return LDAPerror(self->ldap);
     }
     ldap_controls_free(serverctrls);
 
     pmsg =
         LDAPmessage_to_python(self->ldap, msg, add_ctrls, add_intermediates);
 
-    if (res_type == 0) {
-        result_str = Py_None;
-        Py_INCREF(Py_None);
-    }
-    else {
-        result_str = PyInt_FromLong(res_type);
-    }
-
     if (pmsg == NULL) {
         retval = NULL;
     }
     else {
         /* s handles NULL, but O does not */
         if (add_extop) {
-            retval = Py_BuildValue("(OOiOsO)", result_str, pmsg, res_msgid,
+            retval = Py_BuildValue("(iOiOsO)", res_type, pmsg, res_msgid,
                                    pyctrls, retoid,
                                    valuestr ? valuestr : Py_None);
         }
         else {
             retval =
-                Py_BuildValue("(OOiO)", result_str, pmsg, res_msgid, pyctrls);
+                Py_BuildValue("(iOiO)", res_type, pmsg, res_msgid, pyctrls);
         }
 
         if (pmsg != Py_None) {
@@ -1220,7 +1206,6 @@ l_ldap_result4(LDAPObject *self, PyObject *args)
     }
     Py_XDECREF(valuestr);
     Py_XDECREF(pyctrls);
-    Py_DECREF(result_str);
     return retval;
 }
 
@@ -1294,7 +1279,7 @@ l_ldap_search_ext(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_search_ext");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -1341,7 +1326,7 @@ l_ldap_whoami_s(LDAPObject *self, PyObject *args)
 
     if (ldaperror != LDAP_SUCCESS) {
         ber_bvfree(bvalue);
-        return LDAPerror(self->ldap, "ldap_whoami_s");
+        return LDAPerror(self->ldap);
     }
 
     result = LDAPberval_to_unicode_object(bvalue);
@@ -1368,7 +1353,7 @@ l_ldap_start_tls_s(LDAPObject *self, PyObject *args)
     LDAP_END_ALLOW_THREADS(self);
     if (ldaperror != LDAP_SUCCESS) {
         ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &ldaperror);
-        return LDAPerror(self->ldap, "ldap_start_tls_s");
+        return LDAPerror(self->ldap);
     }
 
     Py_INCREF(Py_None);
@@ -1380,14 +1365,16 @@ l_ldap_start_tls_s(LDAPObject *self, PyObject *args)
 /* ldap_set_option */
 
 static PyObject *
-l_ldap_set_option(PyObject *self, PyObject *args)
+l_ldap_set_option(LDAPObject *self, PyObject *args)
 {
     PyObject *value;
     int option;
 
     if (!PyArg_ParseTuple(args, "iO:set_option", &option, &value))
         return NULL;
-    if (!LDAP_set_option((LDAPObject *)self, option, value))
+    if (not_valid(self))
+        return NULL;
+    if (!LDAP_set_option(self, option, value))
         return NULL;
     Py_INCREF(Py_None);
     return Py_None;
@@ -1396,13 +1383,15 @@ l_ldap_set_option(PyObject *self, PyObject *args)
 /* ldap_get_option */
 
 static PyObject *
-l_ldap_get_option(PyObject *self, PyObject *args)
+l_ldap_get_option(LDAPObject *self, PyObject *args)
 {
     int option;
 
     if (!PyArg_ParseTuple(args, "i:get_option", &option))
         return NULL;
-    return LDAP_get_option((LDAPObject *)self, option);
+    if (not_valid(self))
+        return NULL;
+    return LDAP_get_option(self, option);
 }
 
 /* ldap_passwd */
@@ -1460,7 +1449,7 @@ l_ldap_passwd(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_passwd");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
@@ -1511,7 +1500,7 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args)
     LDAPControl_List_DEL(client_ldcs);
 
     if (ldaperror != LDAP_SUCCESS)
-        return LDAPerror(self->ldap, "ldap_extended_operation");
+        return LDAPerror(self->ldap);
 
     return PyInt_FromLong(msgid);
 }
diff --git a/Modules/LDAPObject.h b/Modules/LDAPObject.h
index a456bce..1b6066d 100644
--- a/Modules/LDAPObject.h
+++ b/Modules/LDAPObject.h
@@ -5,12 +5,6 @@
 
 #include "common.h"
 
-#include "lber.h"
-#include "ldap.h"
-#if LDAP_API_VERSION < 2040
-#error Current python-ldap requires OpenLDAP 2.4.x
-#endif
-
 #if PYTHON_API_VERSION < 1007
 typedef PyObject *_threadstate;
 #else
diff --git a/Modules/berval.h b/Modules/berval.h
index 2aa9c97..9c42724 100644
--- a/Modules/berval.h
+++ b/Modules/berval.h
@@ -4,7 +4,6 @@
 #define __h_berval
 
 #include "common.h"
-#include "lber.h"
 
 PyObject *LDAPberval_to_object(const struct berval *bv);
 PyObject *LDAPberval_to_unicode_object(const struct berval *bv);
diff --git a/Modules/common.h b/Modules/common.h
index affa5f9..886024f 100644
--- a/Modules/common.h
+++ b/Modules/common.h
@@ -12,6 +12,35 @@
 #include "config.h"
 #endif
 
+#include <lber.h>
+#include <ldap.h>
+#include <ldap_features.h>
+
+#if LDAP_API_VERSION < 2040
+#error Current python-ldap requires OpenLDAP 2.4.x
+#endif
+
+#if LDAP_VENDOR_VERSION >= 20448
+  /* openldap.h with ldap_init_fd() was introduced in 2.4.48
+   * see https://bugs.openldap.org/show_bug.cgi?id=8671
+   */
+#define HAVE_LDAP_INIT_FD 1
+#include <openldap.h>
+#elif (defined(__APPLE__) && (LDAP_VENDOR_VERSION == 20428))
+/* macOS system libldap 2.4.28 does not have ldap_init_fd symbol */
+#undef HAVE_LDAP_INIT_FD
+#else
+  /* ldap_init_fd() has been around for a very long time
+   * SSSD has been defining the function for a while, so it's probably OK.
+   */
+#define HAVE_LDAP_INIT_FD 1
+#define LDAP_PROTO_TCP 1
+#define LDAP_PROTO_UDP 2
+#define LDAP_PROTO_IPC 3
+extern int ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url,
+                        LDAP **ldp);
+#endif
+
 #if defined(MS_WINDOWS)
 #include <winsock.h>
 #else /* unix */
diff --git a/Modules/constants.c b/Modules/constants.c
index f8da373..8b902e0 100644
--- a/Modules/constants.c
+++ b/Modules/constants.c
@@ -3,8 +3,7 @@
 
 #include "common.h"
 #include "constants.h"
-#include "lber.h"
-#include "ldap.h"
+#include "ldapcontrol.h"
 
 /* the base exception class */
 
@@ -48,29 +47,46 @@ LDAPerr(int errnum)
 
 /* Convert an LDAP error into an informative python exception */
 PyObject *
-LDAPerror(LDAP *l, char *msg)
+LDAPraise_for_message(LDAP *l, LDAPMessage *m)
 {
     if (l == NULL) {
         PyErr_SetFromErrno(LDAPexception_class);
+        ldap_msgfree(m);
         return NULL;
     }
     else {
-        int myerrno, errnum, opt_errnum;
+        int myerrno, errnum, opt_errnum, msgid = -1, msgtype = 0;
         PyObject *errobj;
         PyObject *info;
         PyObject *str;
         PyObject *pyerrno;
-        char *matched, *error;
+        PyObject *pyresult;
+        PyObject *pyctrls = NULL;
+        char *matched = NULL, *error = NULL, **refs = NULL;
+        LDAPControl **serverctrls = NULL;
 
         /* at first save errno for later use before it gets overwritten by another call */
         myerrno = errno;
 
-        opt_errnum = ldap_get_option(l, LDAP_OPT_ERROR_NUMBER, &errnum);
-        if (opt_errnum != LDAP_OPT_SUCCESS)
-            errnum = opt_errnum;
+        if (m != NULL) {
+            msgid = ldap_msgid(m);
+            msgtype = ldap_msgtype(m);
+            ldap_parse_result(l, m, &errnum, &matched, &error, &refs,
+                              &serverctrls, 1);
+        }
 
-        if (errnum == LDAP_NO_MEMORY)
-            return PyErr_NoMemory();
+        if (msgtype <= 0) {
+            opt_errnum = ldap_get_option(l, LDAP_OPT_ERROR_NUMBER, &errnum);
+            if (opt_errnum != LDAP_OPT_SUCCESS)
+                errnum = opt_errnum;
+
+            if (errnum == LDAP_NO_MEMORY) {
+                return PyErr_NoMemory();
+            }
+
+            ldap_get_option(l, LDAP_OPT_MATCHED_DN, &matched);
+            ldap_get_option(l, LDAP_OPT_ERROR_STRING, &error);
+        }
 
         if (errnum >= LDAP_ERROR_MIN && errnum <= LDAP_ERROR_MAX)
             errobj = errobjects[errnum + LDAP_ERROR_OFFSET];
@@ -78,8 +94,32 @@ LDAPerror(LDAP *l, char *msg)
             errobj = LDAPexception_class;
 
         info = PyDict_New();
-        if (info == NULL)
+        if (info == NULL) {
+            ldap_memfree(matched);
+            ldap_memfree(error);
+            ldap_memvfree((void **)refs);
+            ldap_controls_free(serverctrls);
             return NULL;
+        }
+
+        if (msgtype > 0) {
+            pyresult = PyInt_FromLong(msgtype);
+            if (pyresult)
+                PyDict_SetItemString(info, "msgtype", pyresult);
+            Py_XDECREF(pyresult);
+        }
+
+        if (msgid >= 0) {
+            pyresult = PyInt_FromLong(msgid);
+            if (pyresult)
+                PyDict_SetItemString(info, "msgid", pyresult);
+            Py_XDECREF(pyresult);
+        }
+
+        pyresult = PyInt_FromLong(errnum);
+        if (pyresult)
+            PyDict_SetItemString(info, "result", pyresult);
+        Py_XDECREF(pyresult);
 
         str = PyUnicode_FromString(ldap_err2string(errnum));
         if (str)
@@ -93,8 +133,21 @@ LDAPerror(LDAP *l, char *msg)
             Py_XDECREF(pyerrno);
         }
 
-        if (ldap_get_option(l, LDAP_OPT_MATCHED_DN, &matched) >= 0
-            && matched != NULL) {
+        if (!(pyctrls = LDAPControls_to_List(serverctrls))) {
+            int err = LDAP_NO_MEMORY;
+
+            ldap_set_option(l, LDAP_OPT_ERROR_NUMBER, &err);
+            ldap_memfree(matched);
+            ldap_memfree(error);
+            ldap_memvfree((void **)refs);
+            ldap_controls_free(serverctrls);
+            return PyErr_NoMemory();
+        }
+        ldap_controls_free(serverctrls);
+        PyDict_SetItemString(info, "ctrls", pyctrls);
+        Py_XDECREF(pyctrls);
+
+        if (matched != NULL) {
             if (*matched != '\0') {
                 str = PyUnicode_FromString(matched);
                 if (str)
@@ -104,33 +157,42 @@ LDAPerror(LDAP *l, char *msg)
             ldap_memfree(matched);
         }
 
-        if (errnum == LDAP_REFERRAL) {
-            str = PyUnicode_FromString(msg);
+        if (errnum == LDAP_REFERRAL && refs != NULL && refs[0] != NULL) {
+            /* Keep old behaviour, overshadow error message */
+            char err[1024];
+
+            snprintf(err, sizeof(err), "Referral:\n%s", refs[0]);
+            str = PyUnicode_FromString(err);
+            PyDict_SetItemString(info, "info", str);
+            Py_XDECREF(str);
+        }
+        else if (error != NULL && *error != '\0') {
+            str = PyUnicode_FromString(error);
             if (str)
                 PyDict_SetItemString(info, "info", str);
             Py_XDECREF(str);
         }
-        else if (ldap_get_option(l, LDAP_OPT_ERROR_STRING, &error) >= 0) {
-            if (error != NULL && *error != '\0') {
-                str = PyUnicode_FromString(error);
-                if (str)
-                    PyDict_SetItemString(info, "info", str);
-                Py_XDECREF(str);
-            }
-            ldap_memfree(error);
-        }
+
         PyErr_SetObject(errobj, info);
         Py_DECREF(info);
+        ldap_memvfree((void **)refs);
+        ldap_memfree(error);
         return NULL;
     }
 }
 
+PyObject *
+LDAPerror(LDAP *l)
+{
+    return LDAPraise_for_message(l, NULL);
+}
+
 /* initialise the module constants */
 
 int
 LDAPinit_constants(PyObject *m)
 {
-    PyObject *exc;
+    PyObject *exc, *nobj;
 
     /* simple constants */
 
@@ -160,6 +222,10 @@ LDAPinit_constants(PyObject *m)
 #define add_err(n) do {  \
     exc = PyErr_NewException("ldap." #n, LDAPexception_class, NULL);  \
     if (exc == NULL) return -1;  \
+    nobj = PyLong_FromLong(LDAP_##n); \
+    if (nobj == NULL) return -1; \
+    if (PyObject_SetAttrString(exc, "errnum", nobj) != 0) return -1; \
+    Py_DECREF(nobj); \
     errobjects[LDAP_##n+LDAP_ERROR_OFFSET] = exc;  \
     if (PyModule_AddObject(m, #n, exc) != 0) return -1;  \
     Py_INCREF(exc);  \
diff --git a/Modules/constants.h b/Modules/constants.h
index 8a390b5..7b9ce53 100644
--- a/Modules/constants.h
+++ b/Modules/constants.h
@@ -4,14 +4,13 @@
 #define __h_constants_
 
 #include "common.h"
-#include "lber.h"
-#include "ldap.h"
 
 extern int LDAPinit_constants(PyObject *m);
 extern PyObject *LDAPconstant(int);
 
 extern PyObject *LDAPexception_class;
-extern PyObject *LDAPerror(LDAP *, char *msg);
+extern PyObject *LDAPerror(LDAP *);
+extern PyObject *LDAPraise_for_message(LDAP *, LDAPMessage *m);
 PyObject *LDAPerr(int errnum);
 
 #ifndef LDAP_CONTROL_PAGE_OID
diff --git a/Modules/constants_generated.h b/Modules/constants_generated.h
index 455852e..4a4cdb3 100644
--- a/Modules/constants_generated.h
+++ b/Modules/constants_generated.h
@@ -213,10 +213,6 @@ add_int(OPT_X_TLS_DEMAND);
 add_int(OPT_X_TLS_ALLOW);
 add_int(OPT_X_TLS_TRY);
 
-#if defined(LDAP_OPT_X_TLS_PEERCERT)
-add_int(OPT_X_TLS_PEERCERT);
-#endif
-
 #if defined(LDAP_OPT_X_TLS_VERSION)
 add_int(OPT_X_TLS_VERSION);
 #endif
@@ -333,6 +329,14 @@ if (PyModule_AddIntConstant(m, "TLS_AVAIL", 0) != 0)
     return -1;
 #endif
 
+#ifdef HAVE_LDAP_INIT_FD
+if (PyModule_AddIntConstant(m, "INIT_FD_AVAIL", 1) != 0)
+    return -1;
+#else
+if (PyModule_AddIntConstant(m, "INIT_FD_AVAIL", 0) != 0)
+    return -1;
+#endif
+
 add_string(CONTROL_MANAGEDSAIT);
 add_string(CONTROL_PROXY_AUTHZ);
 add_string(CONTROL_SUBENTRIES);
diff --git a/Modules/functions.c b/Modules/functions.c
index 4731efb..b811708 100644
--- a/Modules/functions.c
+++ b/Modules/functions.c
@@ -15,15 +15,75 @@ l_ldap_initialize(PyObject *unused, PyObject *args)
     char *uri;
     LDAP *ld = NULL;
     int ret;
+    PyThreadState *save;
 
     if (!PyArg_ParseTuple(args, "s:initialize", &uri))
         return NULL;
 
-    Py_BEGIN_ALLOW_THREADS ret = ldap_initialize(&ld, uri);
-    Py_END_ALLOW_THREADS if (ret != LDAP_SUCCESS)
-        return LDAPerror(ld, "ldap_initialize");
+    save = PyEval_SaveThread();
+    ret = ldap_initialize(&ld, uri);
+    PyEval_RestoreThread(save);
+
+    if (ret != LDAP_SUCCESS)
+        return LDAPerror(ld);
+
+    return (PyObject *)newLDAPObject(ld);
+}
+
+#ifdef HAVE_LDAP_INIT_FD
+/* initialize_fd(fileno, url) */
+
+static PyObject *
+l_ldap_initialize_fd(PyObject *unused, PyObject *args)
+{
+    char *url;
+    LDAP *ld = NULL;
+    int ret;
+    int fd;
+    int proto = -1;
+    LDAPURLDesc *lud = NULL;
+
+    PyThreadState *save;
+
+    if (!PyArg_ParseTuple(args, "is:initialize_fd", &fd, &url))
+        return NULL;
+
+    /* Get LDAP protocol from scheme */
+    ret = ldap_url_parse(url, &lud);
+    if (ret != LDAP_SUCCESS)
+        return LDAPerr(ret);
+
+    if (strcmp(lud->lud_scheme, "ldap") == 0) {
+        proto = LDAP_PROTO_TCP;
+    }
+    else if (strcmp(lud->lud_scheme, "ldaps") == 0) {
+        proto = LDAP_PROTO_TCP;
+    }
+    else if (strcmp(lud->lud_scheme, "ldapi") == 0) {
+        proto = LDAP_PROTO_IPC;
+    }
+#ifdef LDAP_CONNECTIONLESS
+    else if (strcmp(lud->lud_scheme, "cldap") == 0) {
+        proto = LDAP_PROTO_UDP;
+    }
+#endif
+    else {
+        ldap_free_urldesc(lud);
+        PyErr_SetString(PyExc_ValueError, "unsupported URL scheme");
+        return NULL;
+    }
+    ldap_free_urldesc(lud);
+
+    save = PyEval_SaveThread();
+    ret = ldap_init_fd((ber_socket_t) fd, proto, url, &ld);
+    PyEval_RestoreThread(save);
+
+    if (ret != LDAP_SUCCESS)
+        return LDAPerror(ld);
+
     return (PyObject *)newLDAPObject(ld);
 }
+#endif
 
 /* ldap_str2dn */
 
@@ -75,9 +135,8 @@ l_ldap_str2dn(PyObject *unused, PyObject *args)
             tuple = Py_BuildValue("(O&O&i)",
                                   LDAPberval_to_unicode_object, &ava->la_attr,
                                   LDAPberval_to_unicode_object, &ava->la_value,
-                                  ava->
-                                  la_flags & ~(LDAP_AVA_FREE_ATTR |
-                                               LDAP_AVA_FREE_VALUE));
+                                  ava->la_flags & ~(LDAP_AVA_FREE_ATTR |
+                                                    LDAP_AVA_FREE_VALUE));
             if (!tuple) {
                 Py_DECREF(rdnlist);
                 goto failed;
@@ -133,6 +192,9 @@ l_ldap_get_option(PyObject *self, PyObject *args)
 
 static PyMethodDef methods[] = {
     {"initialize", (PyCFunction)l_ldap_initialize, METH_VARARGS},
+#ifdef HAVE_LDAP_INIT_FD
+    {"initialize_fd", (PyCFunction)l_ldap_initialize_fd, METH_VARARGS},
+#endif
     {"str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS},
     {"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS},
     {"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS},
diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c
index f53e681..e287e9a 100644
--- a/Modules/ldapcontrol.c
+++ b/Modules/ldapcontrol.c
@@ -6,8 +6,6 @@
 #include "berval.h"
 #include "constants.h"
 
-#include "lber.h"
-
 /* Prints to stdout the contents of an array of LDAPControl objects */
 
 /* XXX: This is a debugging tool, and the printf generates some warnings
@@ -82,6 +80,7 @@ Tuple_to_LDAPControl(PyObject *tup)
         return NULL;
 
     lc = PyMem_NEW(LDAPControl, 1);
+
     if (lc == NULL) {
         PyErr_NoMemory();
         return NULL;
@@ -91,6 +90,7 @@ Tuple_to_LDAPControl(PyObject *tup)
 
     len = strlen(oid);
     lc->ldctl_oid = PyMem_NEW(char, len + 1);
+
     if (lc->ldctl_oid == NULL) {
         PyErr_NoMemory();
         LDAPControl_DEL(lc);
@@ -137,6 +137,7 @@ LDAPControls_from_object(PyObject *list, LDAPControl ***controls_ret)
 
     len = PySequence_Length(list);
     ldcs = PyMem_NEW(LDAPControl *, len + 1);
+
     if (ldcs == NULL) {
         PyErr_NoMemory();
         return 0;
@@ -339,6 +340,7 @@ encode_assertion_control(PyObject *self, PyObject *args)
     char *assertion_filterstr;
     struct berval ctrl_val;
     LDAP *ld = NULL;
+    PyThreadState *save;
 
     if (!PyArg_ParseTuple(args, "s:encode_assertion_control",
                           &assertion_filterstr)) {
@@ -348,21 +350,27 @@ encode_assertion_control(PyObject *self, PyObject *args)
     /* XXX: ldap_create() is a nasty and slow hack. It's creating a full blown
      * LDAP object just to encode assertion controls.
      */
-    Py_BEGIN_ALLOW_THREADS err = ldap_create(&ld);
-    Py_END_ALLOW_THREADS if (err != LDAP_SUCCESS)
-        return LDAPerror(ld, "ldap_create");
+    save = PyEval_SaveThread();
+    err = ldap_create(&ld);
+    PyEval_RestoreThread(save);
+    if (err != LDAP_SUCCESS)
+        return LDAPerror(ld);
 
-    err =
-        ldap_create_assertion_control_value(ld, assertion_filterstr,
-                                            &ctrl_val);
+    err = ldap_create_assertion_control_value(ld, assertion_filterstr,
+                                              &ctrl_val);
 
     if (err != LDAP_SUCCESS) {
-        LDAPerror(ld, "ldap_create_assertion_control_value");
-        Py_BEGIN_ALLOW_THREADS ldap_unbind_ext(ld, NULL, NULL);
-        Py_END_ALLOW_THREADS return NULL;
+        LDAPerror(ld);
+        save = PyEval_SaveThread();
+        ldap_unbind_ext(ld, NULL, NULL);
+        PyEval_RestoreThread(save);
+        return NULL;
     }
-    Py_BEGIN_ALLOW_THREADS ldap_unbind_ext(ld, NULL, NULL);
-    Py_END_ALLOW_THREADS res = LDAPberval_to_object(&ctrl_val);
+    save = PyEval_SaveThread();
+    ldap_unbind_ext(ld, NULL, NULL);
+    PyEval_RestoreThread(save);
+    res = LDAPberval_to_object(&ctrl_val);
+
     if (ctrl_val.bv_val != NULL) {
         ber_memfree(ctrl_val.bv_val);
     }
diff --git a/Modules/ldapcontrol.h b/Modules/ldapcontrol.h
index de694c0..74cae42 100644
--- a/Modules/ldapcontrol.h
+++ b/Modules/ldapcontrol.h
@@ -4,7 +4,6 @@
 #define __h_ldapcontrol
 
 #include "common.h"
-#include "ldap.h"
 
 void LDAPinit_control(PyObject *d);
 void LDAPControl_List_DEL(LDAPControl **);
diff --git a/Modules/message.c b/Modules/message.c
index 2c05488..22aa313 100644
--- a/Modules/message.c
+++ b/Modules/message.c
@@ -53,7 +53,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
         if (dn == NULL) {
             Py_DECREF(result);
             ldap_msgfree(m);
-            return LDAPerror(ld, "ldap_get_dn");
+            return LDAPerror(ld);
         }
 
         attrdict = PyDict_New();
@@ -69,7 +69,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
             Py_DECREF(result);
             ldap_msgfree(m);
             ldap_memfree(dn);
-            return LDAPerror(ld, "ldap_get_entry_controls");
+            return LDAPerror(ld);
         }
 
         /* convert serverctrls to list of tuples */
@@ -81,7 +81,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
             ldap_msgfree(m);
             ldap_memfree(dn);
             ldap_controls_free(serverctrls);
-            return LDAPerror(ld, "LDAPControls_to_List");
+            return LDAPerror(ld);
         }
         ldap_controls_free(serverctrls);
 
@@ -201,7 +201,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
             Py_DECREF(reflist);
             Py_DECREF(result);
             ldap_msgfree(m);
-            return LDAPerror(ld, "ldap_parse_reference");
+            return LDAPerror(ld);
         }
         /* convert serverctrls to list of tuples */
         if (!(pyctrls = LDAPControls_to_List(serverctrls))) {
@@ -212,7 +212,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
             Py_DECREF(result);
             ldap_msgfree(m);
             ldap_controls_free(serverctrls);
-            return LDAPerror(ld, "LDAPControls_to_List");
+            return LDAPerror(ld);
         }
         ldap_controls_free(serverctrls);
         if (refs) {
@@ -255,7 +255,7 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
                      0) != LDAP_SUCCESS) {
                     Py_DECREF(result);
                     ldap_msgfree(m);
-                    return LDAPerror(ld, "ldap_parse_intermediate");
+                    return LDAPerror(ld);
                 }
                 /* convert serverctrls to list of tuples */
                 if (!(pyctrls = LDAPControls_to_List(serverctrls))) {
@@ -267,26 +267,41 @@ LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
                     ldap_controls_free(serverctrls);
                     ldap_memfree(retoid);
                     ber_bvfree(retdata);
-                    return LDAPerror(ld, "LDAPControls_to_List");
+                    return LDAPerror(ld);
                 }
                 ldap_controls_free(serverctrls);
 
                 valuestr = LDAPberval_to_object(retdata);
                 ber_bvfree(retdata);
+                if (valuestr == NULL) {
+                    ldap_memfree(retoid);
+                    Py_DECREF(result);
+                    ldap_msgfree(m);
+                    return NULL;
+                }
+
                 pyoid = PyUnicode_FromString(retoid);
                 ldap_memfree(retoid);
                 if (pyoid == NULL) {
+                    Py_DECREF(valuestr);
+                    Py_DECREF(result);
+                    ldap_msgfree(m);
+                    return NULL;
+                }
+
+                valtuple = Py_BuildValue("(NNN)", pyoid, valuestr, pyctrls);
+                if (valtuple == NULL) {
+                    Py_DECREF(result);
+                    ldap_msgfree(m);
+                    return NULL;
+                }
+
+                if (PyList_Append(result, valtuple) == -1) {
+                    Py_DECREF(valtuple);
                     Py_DECREF(result);
                     ldap_msgfree(m);
                     return NULL;
                 }
-                valtuple = Py_BuildValue("(OOO)", pyoid,
-                                         valuestr ? valuestr : Py_None,
-                                         pyctrls);
-                Py_DECREF(pyoid);
-                Py_DECREF(valuestr);
-                Py_XDECREF(pyctrls);
-                PyList_Append(result, valtuple);
                 Py_DECREF(valtuple);
             }
         }
diff --git a/Modules/message.h b/Modules/message.h
index 2978ea5..ed73f32 100644
--- a/Modules/message.h
+++ b/Modules/message.h
@@ -4,8 +4,6 @@
 #define __h_message
 
 #include "common.h"
-#include "lber.h"
-#include "ldap.h"
 
 extern PyObject *LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
                                        int add_intermediates);
diff --git a/Modules/options.c b/Modules/options.c
index 85560e6..549a672 100644
--- a/Modules/options.c
+++ b/Modules/options.c
@@ -185,11 +185,18 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value)
         return 0;
     }
 
-    if (self)
+    if (self) {
         LDAP_BEGIN_ALLOW_THREADS(self);
-    res = ldap_set_option(ld, option, ptr);
-    if (self)
+        res = ldap_set_option(ld, option, ptr);
         LDAP_END_ALLOW_THREADS(self);
+    }
+    else {
+        PyThreadState *save;
+
+        save = PyEval_SaveThread();
+        res = ldap_set_option(NULL, option, ptr);
+        PyEval_RestoreThread(save);
+    }
 
     if ((option == LDAP_OPT_SERVER_CONTROLS) ||
         (option == LDAP_OPT_CLIENT_CONTROLS))
@@ -203,6 +210,26 @@ LDAP_set_option(LDAPObject *self, int option, PyObject *value)
     return 1;
 }
 
+static int
+LDAP_int_get_option(LDAPObject *self, int option, void *value)
+{
+    int res;
+
+    if (self != NULL) {
+        LDAP_BEGIN_ALLOW_THREADS(self);
+        res = ldap_get_option(self->ldap, option, value);
+        LDAP_END_ALLOW_THREADS(self);
+    }
+    else {
+        PyThreadState *save;
+
+        save = PyEval_SaveThread();
+        res = ldap_get_option(NULL, option, value);
+        PyEval_RestoreThread(save);
+    }
+    return res;
+}
+
 PyObject *
 LDAP_get_option(LDAPObject *self, int option)
 {
@@ -214,18 +241,11 @@ LDAP_get_option(LDAPObject *self, int option)
     char *strval;
     PyObject *extensions, *v;
     Py_ssize_t i, num_extensions;
-    LDAP *ld;
-
-    ld = self ? self->ldap : NULL;
 
     switch (option) {
     case LDAP_OPT_API_INFO:
         apiinfo.ldapai_info_version = LDAP_API_INFO_VERSION;
-        if (self)
-            LDAP_BEGIN_ALLOW_THREADS(self);
-        res = ldap_get_option(ld, option, &apiinfo);
-        if (self)
-            LDAP_END_ALLOW_THREADS(self);
+        res = LDAP_int_get_option(self, option, &apiinfo);
         if (res != LDAP_OPT_SUCCESS)
             return option_error(res, "ldap_get_option");
 
@@ -236,8 +256,8 @@ LDAP_get_option(LDAPObject *self, int option)
         extensions = PyTuple_New(num_extensions);
         for (i = 0; i < num_extensions; i++)
             PyTuple_SET_ITEM(extensions, i,
-                             PyUnicode_FromString(apiinfo.
-                                                  ldapai_extensions[i]));
+                             PyUnicode_FromString(apiinfo.ldapai_extensions
+                                                  [i]));
 
         /* return api info as a dictionary */
         v = Py_BuildValue("{s:i, s:i, s:i, s:s, s:i, s:O}",
@@ -299,11 +319,7 @@ LDAP_get_option(LDAPObject *self, int option)
     case LDAP_OPT_X_KEEPALIVE_INTERVAL:
 #endif
         /* Integer-valued options */
-        if (self)
-            LDAP_BEGIN_ALLOW_THREADS(self);
-        res = ldap_get_option(ld, option, &intval);
-        if (self)
-            LDAP_END_ALLOW_THREADS(self);
+        res = LDAP_int_get_option(self, option, &intval);
         if (res != LDAP_OPT_SUCCESS)
             return option_error(res, "ldap_get_option");
         return PyInt_FromLong(intval);
@@ -347,11 +363,7 @@ LDAP_get_option(LDAPObject *self, int option)
 #endif
 #endif
         /* String-valued options */
-        if (self)
-            LDAP_BEGIN_ALLOW_THREADS(self);
-        res = ldap_get_option(ld, option, &strval);
-        if (self)
-            LDAP_END_ALLOW_THREADS(self);
+        res = LDAP_int_get_option(self, option, &strval);
         if (res != LDAP_OPT_SUCCESS)
             return option_error(res, "ldap_get_option");
         if (strval == NULL) {
@@ -365,11 +377,7 @@ LDAP_get_option(LDAPObject *self, int option)
     case LDAP_OPT_TIMEOUT:
     case LDAP_OPT_NETWORK_TIMEOUT:
         /* Double-valued timeval options */
-        if (self)
-            LDAP_BEGIN_ALLOW_THREADS(self);
-        res = ldap_get_option(ld, option, &tv);
-        if (self)
-            LDAP_END_ALLOW_THREADS(self);
+        res = LDAP_int_get_option(self, option, &tv);
         if (res != LDAP_OPT_SUCCESS)
             return option_error(res, "ldap_get_option");
         if (tv == NULL) {
@@ -384,12 +392,7 @@ LDAP_get_option(LDAPObject *self, int option)
 
     case LDAP_OPT_SERVER_CONTROLS:
     case LDAP_OPT_CLIENT_CONTROLS:
-        if (self)
-            LDAP_BEGIN_ALLOW_THREADS(self);
-        res = ldap_get_option(ld, option, &lcs);
-        if (self)
-            LDAP_END_ALLOW_THREADS(self);
-
+        res = LDAP_int_get_option(self, option, &lcs);
         if (res != LDAP_OPT_SUCCESS)
             return option_error(res, "ldap_get_option");
 
diff --git a/PKG-INFO b/PKG-INFO
index 699c986..065f45b 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: python-ldap
-Version: 3.2.0
+Version: 3.3.1
 Summary: Python modules for implementing LDAP clients
 Home-page: https://www.python-ldap.org/
 Author: python-ldap project
@@ -31,6 +31,7 @@ Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
 Classifier: Topic :: Database
 Classifier: Topic :: Internet
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/Tests/t_cext.py b/Tests/t_cext.py
index 96c3b2c..1e27588 100644
--- a/Tests/t_cext.py
+++ b/Tests/t_cext.py
@@ -7,8 +7,10 @@ See https://www.python-ldap.org/ for details.
 
 from __future__ import unicode_literals
 
+import contextlib
 import errno
 import os
+import socket
 import unittest
 
 # Switch off processing .ldaprc or ldap.conf before importing _ldap
@@ -16,7 +18,7 @@ os.environ['LDAPNOINIT'] = '1'
 
 # import the plain C wrapper module
 import _ldap
-from slapdtest import SlapdTestCase, requires_tls
+from slapdtest import SlapdTestCase, requires_tls, requires_init_fd
 
 
 class TestLdapCExtension(SlapdTestCase):
@@ -92,14 +94,35 @@ class TestLdapCExtension(SlapdTestCase):
         """
         l = _ldap.initialize(self.server.ldap_uri)
         if bind:
-            # Perform a simple bind
-            l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3)
-            m = l.simple_bind(self.server.root_dn, self.server.root_pw)
-            result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout)
-            self.assertEqual(result, _ldap.RES_BIND)
-            self.assertEqual(type(msgid), type(0))
+            self._bind_conn(l)
         return l
 
+    @contextlib.contextmanager
+    def _open_conn_fd(self, bind=True):
+        sock = socket.create_connection(
+            (self.server.hostname, self.server.port)
+        )
+        try:
+            l = _ldap.initialize_fd(sock.fileno(), self.server.ldap_uri)
+            if bind:
+                self._bind_conn(l)
+            yield sock, l
+        finally:
+            try:
+                sock.close()
+            except OSError:
+                # already closed
+                pass
+
+    def _bind_conn(self, l):
+        # Perform a simple bind
+        l.set_option(_ldap.OPT_PROTOCOL_VERSION, _ldap.VERSION3)
+        m = l.simple_bind(self.server.root_dn, self.server.root_pw)
+        result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ONE, self.timeout)
+        self.assertEqual(result, _ldap.RES_BIND)
+        self.assertEqual(type(msgid), type(0))
+
+
     # Test for the existence of a whole bunch of constants
     # that the C module is supposed to export
     def test_constants(self):
@@ -224,6 +247,33 @@ class TestLdapCExtension(SlapdTestCase):
     def test_simple_bind(self):
         l = self._open_conn()
 
+    def test_simple_bind_fileno(self):
+        with self._open_conn_fd() as (sock, l):
+            self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn)
+
+    @requires_init_fd()
+    def test_simple_bind_fileno_invalid(self):
+        with open(os.devnull) as f:
+            l = _ldap.initialize_fd(f.fileno(), self.server.ldap_uri)
+            with self.assertRaises(_ldap.SERVER_DOWN):
+                self._bind_conn(l)
+
+    @requires_init_fd()
+    def test_simple_bind_fileno_closed(self):
+        with self._open_conn_fd() as (sock, l):
+            self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn)
+            sock.close()
+            with self.assertRaises(_ldap.SERVER_DOWN):
+                l.whoami_s()
+
+    @requires_init_fd()
+    def test_simple_bind_fileno_rebind(self):
+        with self._open_conn_fd() as (sock, l):
+            self.assertEqual(l.whoami_s(), "dn:" + self.server.root_dn)
+            l.unbind_ext()
+            with self.assertRaises(_ldap.LDAPError):
+                self._bind_conn(l)
+
     def test_simple_anonymous_bind(self):
         l = self._open_conn(bind=False)
         m = l.simple_bind("", "")
@@ -381,30 +431,36 @@ class TestLdapCExtension(SlapdTestCase):
         self.assertEqual(type(m), type(0))
         result, pmsg, msgid, ctrls = l.result4(m, _ldap.MSG_ALL, self.timeout)
         self.assertEqual(result, _ldap.RES_ADD)
+
         # try a false compare
         m = l.compare_ext(dn, "userPassword", "bad_string")
-        try:
+        with self.assertRaises(_ldap.COMPARE_FALSE) as e:
             r = l.result4(m, _ldap.MSG_ALL, self.timeout)
-        except _ldap.COMPARE_FALSE:
-            pass
-        else:
-            self.fail("expected COMPARE_FALSE, got %r" % r)
+
+        self.assertEqual(e.exception.args[0]['msgid'], m)
+        self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE)
+        self.assertEqual(e.exception.args[0]['result'], 5)
+        self.assertFalse(e.exception.args[0]['ctrls'])
+
         # try a true compare
         m = l.compare_ext(dn, "userPassword", "the_password")
-        try:
+        with self.assertRaises(_ldap.COMPARE_TRUE) as e:
             r = l.result4(m, _ldap.MSG_ALL, self.timeout)
-        except _ldap.COMPARE_TRUE:
-            pass
-        else:
-            self.fail("expected COMPARE_TRUE, got %r" % r)
+
+        self.assertEqual(e.exception.args[0]['msgid'], m)
+        self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE)
+        self.assertEqual(e.exception.args[0]['result'], 6)
+        self.assertFalse(e.exception.args[0]['ctrls'])
+
         # try a compare on bad attribute
         m = l.compare_ext(dn, "badAttribute", "ignoreme")
-        try:
+        with self.assertRaises(_ldap.error) as e:
             r = l.result4(m, _ldap.MSG_ALL, self.timeout)
-        except _ldap.error:
-            pass
-        else:
-            self.fail("expected LDAPError, got %r" % r)
+
+        self.assertEqual(e.exception.args[0]['msgid'], m)
+        self.assertEqual(e.exception.args[0]['msgtype'], _ldap.RES_COMPARE)
+        self.assertEqual(e.exception.args[0]['result'], 17)
+        self.assertFalse(e.exception.args[0]['ctrls'])
 
     def test_delete_no_such_object(self):
         """
diff --git a/Tests/t_cidict.py b/Tests/t_cidict.py
index fa5a39b..6878617 100644
--- a/Tests/t_cidict.py
+++ b/Tests/t_cidict.py
@@ -7,6 +7,7 @@ See https://www.python-ldap.org/ for details.
 
 import os
 import unittest
+import warnings
 
 # Switch off processing .ldaprc or ldap.conf before importing _ldap
 os.environ['LDAPNOINIT'] = '1'
@@ -48,6 +49,29 @@ class TestCidict(unittest.TestCase):
         self.assertEqual(cix.has_key("abcdef"), False)
         self.assertEqual(cix.has_key("AbCDef"), False)
 
+    def test_strlist_deprecated(self):
+        strlist_funcs = [
+            ldap.cidict.strlist_intersection,
+            ldap.cidict.strlist_minus,
+            ldap.cidict.strlist_union
+        ]
+        for strlist_func in strlist_funcs:
+            with warnings.catch_warnings(record=True) as w:
+                warnings.resetwarnings()
+                warnings.simplefilter("always", DeprecationWarning)
+                strlist_func(["a"], ["b"])
+            self.assertEqual(len(w), 1)
+
+    def test_cidict_data(self):
+        """test the deprecated data atrtribute"""
+        d = ldap.cidict.cidict({'A': 1, 'B': 2})
+        with warnings.catch_warnings(record=True) as w:
+            warnings.resetwarnings()
+            warnings.simplefilter('always', DeprecationWarning)
+            data = d.data
+        assert data == {'a': 1, 'b': 2}
+        self.assertEqual(len(w), 1)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py
index 4b4dd31..fd36f86 100644
--- a/Tests/t_ldap_dn.py
+++ b/Tests/t_ldap_dn.py
@@ -50,6 +50,11 @@ class TestDN(unittest.TestCase):
         self.assertEqual(ldap.dn.escape_dn_chars('#foobar'), '\\#foobar')
         self.assertEqual(ldap.dn.escape_dn_chars('foo bar'), 'foo bar')
         self.assertEqual(ldap.dn.escape_dn_chars(' foobar'), '\\ foobar')
+        self.assertEqual(ldap.dn.escape_dn_chars(' '), '\\ ')
+        self.assertEqual(ldap.dn.escape_dn_chars('  '), '\\ \\ ')
+        self.assertEqual(ldap.dn.escape_dn_chars('foobar '), 'foobar\\ ')
+        self.assertEqual(ldap.dn.escape_dn_chars('f+o>o,b<a;r="\00"'), 'f\\+o\\>o\\,b\\<a\\;r\\=\\"\\\x00\\"')
+        self.assertEqual(ldap.dn.escape_dn_chars('foo\\,bar'), 'foo\\\\\\,bar')
 
     def test_str2dn(self):
         """
diff --git a/Tests/t_ldap_schema_tokenizer.py b/Tests/t_ldap_schema_tokenizer.py
index c858177..0890379 100644
--- a/Tests/t_ldap_schema_tokenizer.py
+++ b/Tests/t_ldap_schema_tokenizer.py
@@ -44,8 +44,8 @@ TESTCASES_UTF8 = (
 
 # broken schema of Oracle Internet Directory
 TESTCASES_BROKEN_OID = (
-    ("BLUBB DI 'BLU B B ER'MUST 'BLAH' ", ['BLUBB', 'DI', 'BLU B B ER', 'MUST', 'BLAH']),
-    ("BLUBBER DI 'BLU'BB ER' DA 'BLAH' ", ["BLUBBER", "DI", "BLU'BB ER", "DA", "BLAH"]),
+    "BLUBB DI 'BLU B B ER'MUST 'BLAH' ", #['BLUBB', 'DI', 'BLU B B ER', 'MUST', 'BLAH']
+    "BLUBBER DI 'BLU'BB ER' DA 'BLAH' ", #["BLUBBER", "DI", "BLU'BB ER", "DA", "BLAH"]
 )
 
 # for quoted single quotes inside string values
@@ -104,14 +104,12 @@ class TestSplitTokens(unittest.TestCase):
         """
         self._run_split_tokens_tests(TESTCASES_UTF8)
 
-    @unittest.expectedFailure
     def test_broken_oid(self):
         """
         run test cases specified in constant TESTCASES_BROKEN_OID
         """
         self._run_failure_tests(TESTCASES_BROKEN_OID)
 
-    @unittest.expectedFailure
     def test_escaped_quotes(self):
         """
         run test cases specified in constant TESTCASES_ESCAPED_QUOTES
diff --git a/Tests/t_ldap_syncrepl.py b/Tests/t_ldap_syncrepl.py
index 73ba1fb..b8a6ab6 100644
--- a/Tests/t_ldap_syncrepl.py
+++ b/Tests/t_ldap_syncrepl.py
@@ -10,6 +10,7 @@ import os
 import shelve
 import sys
 import unittest
+import binascii
 
 if sys.version_info[0] <= 2:
     PY2 = True
@@ -21,7 +22,7 @@ os.environ['LDAPNOINIT'] = '1'
 
 import ldap
 from ldap.ldapobject import SimpleLDAPObject
-from ldap.syncrepl import SyncreplConsumer
+from ldap.syncrepl import SyncreplConsumer, SyncInfoMessage
 
 from slapdtest import SlapdObject, SlapdTestCase
 
@@ -242,7 +243,7 @@ class SyncreplClient(SimpleLDAPObject, SyncreplConsumer):
 
         elif (uuids is None) and (refreshDeletes is False):
             deleted_uuids = []
-            for uuid in self.uuid_dn.keys():
+            for uuid in self.uuid_dn:
                 if uuid not in self.present:
                     deleted_uuids.append(uuid)
 
@@ -445,6 +446,47 @@ class TestSyncreplBytesMode(BaseSyncreplTests, SlapdTestCase):
         )
         self.suffix = self.server.suffix.encode('utf-8')
 
+class DecodeSyncreplProtoTests(unittest.TestCase):
+    """
+    Tests of the ASN.1 decoder for tricky cases or past issues to ensure that
+    syncrepl messages are handled correctly.
+    """
+
+    def test_syncidset_message(self):
+        """
+        A syncrepl server may send a sync info message, with a syncIdSet
+        of uuids to delete. A regression was found in the original
+        sync info message implementation due to how the choice was
+        evaluated, because refreshPresent and refreshDelete were both
+        able to be fully expressed as defaults, causing the parser
+        to mistakenly catch a syncIdSet as a refreshPresent/refereshDelete.
+
+        This tests that a syncIdSet request is properly decoded.
+
+        reference: https://tools.ietf.org/html/rfc4533#section-2.5
+        """
+
+        # This is a dump of a syncidset message from wireshark + 389-ds
+        msg = """
+        a36b04526c6461706b64632e6578616d706c652e636f6d3a333839303123636e
+        3d6469726563746f7279206d616e616765723a64633d6578616d706c652c6463
+        3d636f6d3a286f626a656374436c6173733d2a2923330101ff311204108dc446
+        01a93611ea8aaff248c5fa5780
+        """.replace(' ', '').replace('\n', '')
+
+        msgraw = binascii.unhexlify(msg)
+        sim = SyncInfoMessage(msgraw)
+        self.assertEqual(sim.refreshDelete, None)
+        self.assertEqual(sim.refreshPresent, None)
+        self.assertEqual(sim.newcookie, None)
+        self.assertEqual(sim.syncIdSet,
+            {
+                'cookie': 'ldapkdc.example.com:38901#cn=directory manager:dc=example,dc=com:(objectClass=*)#3',
+                'syncUUIDs': ['8dc44601-a936-11ea-8aaf-f248c5fa5780'],
+                'refreshDeletes': True
+            }
+        )
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py
index 0619d51..6f1f2d2 100644
--- a/Tests/t_ldapobject.py
+++ b/Tests/t_ldapobject.py
@@ -20,6 +20,7 @@ import errno
 import contextlib
 import linecache
 import os
+import socket
 import unittest
 import warnings
 import pickle
@@ -32,6 +33,7 @@ from ldap.ldapobject import SimpleLDAPObject, ReconnectLDAPObject
 
 from slapdtest import SlapdTestCase
 from slapdtest import requires_ldapi, requires_sasl, requires_tls
+from slapdtest import requires_init_fd
 
 
 LDIF_TEMPLATE = """dn: %(suffix)s
@@ -103,6 +105,9 @@ class Test00_SimpleLDAPObject(SlapdTestCase):
             # open local LDAP connection
             self._ldap_conn = self._open_ldap_conn(bytes_mode=False)
 
+    def tearDown(self):
+        del self._ldap_conn
+
     def test_reject_bytes_base(self):
         base = self.server.suffix
         l = self._ldap_conn
@@ -465,7 +470,7 @@ class Test00_SimpleLDAPObject(SlapdTestCase):
             info = ldap_err.args[0]['info']
             expected_info = os.strerror(errno.ENOTCONN)
             if info != expected_info:
-                self.fail("expected info=%r, got %d" % (expected_info, info))
+                self.fail("expected info=%r, got %r" % (expected_info, info))
         else:
             self.fail("expected SERVER_DOWN, got %r" % r)
 
@@ -661,6 +666,71 @@ class Test00_SimpleLDAPObject(SlapdTestCase):
         result = l.compare_s('cn=Foo1,%s' % base, 'cn', b'Foo2')
         self.assertIs(result, False)
 
+    def test_compare_s_notfound(self):
+        base = self.server.suffix
+        l = self._ldap_conn
+        with self.assertRaises(ldap.NO_SUCH_OBJECT):
+            result = l.compare_s('cn=invalid,%s' % base, 'cn', b'Foo2')
+
+    def test_compare_s_invalidattr(self):
+        base = self.server.suffix
+        l = self._ldap_conn
+        with self.assertRaises(ldap.UNDEFINED_TYPE):
+            result = l.compare_s('cn=Foo1,%s' % base, 'invalidattr', b'invalid')
+
+    def test_compare_true_exception_contains_message_id(self):
+        base = self.server.suffix
+        l = self._ldap_conn
+        msgid = l.compare('cn=Foo1,%s' % base, 'cn', b'Foo1')
+        with self.assertRaises(ldap.COMPARE_TRUE) as cm:
+            l.result()
+        self.assertEqual(cm.exception.args[0]["msgid"], msgid)
+
+    def test_async_search_no_such_object_exception_contains_message_id(self):
+        msgid = self._ldap_conn.search("CN=XXX", ldap.SCOPE_SUBTREE)
+        with self.assertRaises(ldap.NO_SUCH_OBJECT) as cm:
+            self._ldap_conn.result()
+        self.assertEqual(cm.exception.args[0]["msgid"], msgid)
+
+    def test_passwd_s(self):
+        l = self._ldap_conn
+
+        # first, create a user to change password on
+        dn = "cn=PasswordTest," + self.server.suffix
+        result, pmsg, msgid, ctrls = l.add_ext_s(
+            dn,
+            [
+                ('objectClass', b'person'),
+                ('sn', b'PasswordTest'),
+                ('cn', b'PasswordTest'),
+                ('userPassword', b'initial'),
+            ]
+        )
+        self.assertEqual(result, ldap.RES_ADD)
+        self.assertIsInstance(msgid, int)
+        self.assertEqual(pmsg, [])
+        self.assertEqual(ctrls, [])
+
+        # try changing password with a wrong old-pw
+        with self.assertRaises(ldap.UNWILLING_TO_PERFORM):
+            l.passwd_s(dn, "bogus", "ignored")
+
+        # have the server generate a new random pw
+        respoid, respvalue = l.passwd_s(dn, "initial", None, extract_newpw=True)
+        self.assertEqual(respoid, None)
+
+        password = respvalue.genPasswd
+        self.assertIsInstance(password, bytes)
+        if PY2:
+            password = password.decode('utf-8')
+
+        # try changing password back
+        respoid, respvalue = l.passwd_s(dn, password, "initial")
+        self.assertEqual(respoid, None)
+        self.assertEqual(respvalue, None)
+
+        l.delete_s(dn)
+
 
 class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
     """
@@ -724,6 +794,47 @@ class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
         l2 = pickle.loads(l1_state)
         self.assertEqual(l2.whoami_s(), 'dn:'+bind_dn)
 
+    def test105_reconnect_restore(self):
+        l1 = self.ldap_object_class(self.server.ldap_uri, retry_max=2, retry_delay=1)
+        bind_dn = 'cn=user1,'+self.server.suffix
+        l1.simple_bind_s(bind_dn, 'user1_pw')
+        self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn)
+        self.server._proc.terminate()
+        self.server.wait()
+        try:
+            l1.whoami_s()
+        except ldap.SERVER_DOWN:
+            pass
+        else:
+            self.assertEqual(True, False)
+        finally:
+            self.server._start_slapd()
+        self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn)
+
+
+@requires_init_fd()
+class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject):
+    def _get_bytes_ldapobject(self, explicit=True, **kwargs):
+        raise unittest.SkipTest("Test opens two sockets")
+
+    def _search_wrong_type(self, bytes_mode, strictness):
+        raise unittest.SkipTest("Test opens two sockets")
+
+    def _open_ldap_conn(self, who=None, cred=None, **kwargs):
+        if hasattr(self, '_sock'):
+            raise RuntimeError("socket already connected")
+        self._sock = socket.create_connection(
+            (self.server.hostname, self.server.port)
+        )
+        return super(Test03_SimpleLDAPObjectWithFileno, self)._open_ldap_conn(
+            who=who, cred=cred, fileno=self._sock.fileno(), **kwargs
+        )
+
+    def tearDown(self):
+        self._sock.close()
+        del self._sock
+        super(Test03_SimpleLDAPObjectWithFileno, self).tearDown()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/debian/changelog b/debian/changelog
index bb497b1..71a1e55 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-python-ldap (3.2.0-5) UNRELEASED; urgency=medium
+python-ldap (3.3.1-1) UNRELEASED; urgency=medium
 
   [ Ondřej Nový ]
   * d/control: Update Maintainer field with new Debian Python Team
@@ -9,8 +9,9 @@ python-ldap (3.2.0-5) UNRELEASED; urgency=medium
   [ Debian Janitor ]
   * debian/copyright: use spaces rather than tabs to start continuation lines.
   * Update standards version to 4.5.0, no changes needed.
+  * New upstream release.
 
- -- Ondřej Nový <onovy@debian.org>  Thu, 24 Sep 2020 08:46:42 +0200
+ -- Ondřej Nový <onovy@debian.org>  Sat, 05 Jun 2021 01:21:36 -0000
 
 python-ldap (3.2.0-4) unstable; urgency=medium
 
diff --git a/setup.cfg b/setup.cfg
index 4b2427a..2e372ba 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,6 @@
+[metadata]
+license_file = LICENCE
+
 [_ldap]
 defines = HAVE_SASL HAVE_TLS HAVE_LIBLDAP_R
 extra_compile_args = 
diff --git a/setup.py b/setup.py
index e66ecbd..6974785 100644
--- a/setup.py
+++ b/setup.py
@@ -95,6 +95,7 @@ setup(
     'Programming Language :: Python :: 3.5',
     'Programming Language :: Python :: 3.6',
     'Programming Language :: Python :: 3.7',
+    'Programming Language :: Python :: 3.8',
     # Note: when updating Python versions, also change .travis.yml and tox.ini
 
     'Topic :: Database',
diff --git a/tox.ini b/tox.ini
index 1434ba0..f7bc6c8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,54 +5,51 @@
 
 [tox]
 # Note: when updating Python versions, also change setup.py and .travis.yml
-envlist = py27,py34,py35,py36,py37,py38,{py2,py3}-nosasltls,doc,py3-trace,coverage-report
+envlist = py27,py34,py35,py36,py37,py38,py39,{py2,py3}-nosasltls,doc,py3-trace
 minver = 1.8
 
 [testenv]
-deps = coverage
+deps =
 passenv = WITH_GCOV
 # - Enable BytesWarning
 # - Turn all warnings into exceptions.
-# - 'ignore:the imp module is deprecated' is required to ignore import of 'imp'
-#   in distutils. Python < 3.6 use PendingDeprecationWarning; Python >= 3.6 use
-#   DeprecationWarning.
 commands = {envpython} -bb -Werror \
-    "-Wignore:the imp module is deprecated:DeprecationWarning" \
-    "-Wignore:the imp module is deprecated:PendingDeprecationWarning" \
-    "-Wignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning" \
-    -m coverage run --parallel setup.py \
-        clean --all \
-        test
+    -m unittest discover -v -s Tests -p 't_*'
 
 [testenv:py27]
 # No warnings with Python 2.7
 passenv = {[testenv]passenv}
-commands = {envpython} \
-    -m coverage run --parallel setup.py test
+commands =
+    {envpython} -m unittest discover -v -s Tests -p 't_*'
 
 [testenv:py34]
 # No warnings with Python 3.4
 passenv = {[testenv]passenv}
-commands = {envpython} \
-    -m coverage run --parallel setup.py test
+commands = {[testenv:py27]commands}
 
 [testenv:py2-nosasltls]
 basepython = python2
-deps = {[testenv]deps}
+# don't install, install dependencies manually
+skip_install = true
+deps =
+    {[testenv]deps}
+    pyasn1
+    pyasn1_modules
 passenv = {[testenv]passenv}
 setenv =
     CI_DISABLED=LDAPI:SASL:TLS
-# rebuild without SASL and TLS, run without LDAPI
-commands = {envpython} \
-    -m coverage run --parallel setup.py \
-        clean --all \
-        build_ext -UHAVE_SASL,HAVE_TLS \
-        test
+# build and install without SASL and TLS, run without LDAPI
+commands =
+    {envpython} setup.py clean --all
+    {envpython} setup.py build_ext -UHAVE_SASL,HAVE_TLS
+    {envpython} setup.py install --single-version-externally-managed --root=/
+    {[testenv:py27]commands}
 
 [testenv:py3-nosasltls]
 basepython = python3
-deps = {[testenv]deps}
-passenv = {[testenv]passenv}
+skip_install = {[testenv:py2-nosasltls]skip_install}
+deps = {[testenv:py2-nosasltls]deps}
+passenv = {[testenv:py2-nosasltls]passenv}
 setenv = {[testenv:py2-nosasltls]setenv}
 commands = {[testenv:py2-nosasltls]commands}
 
@@ -76,13 +73,25 @@ basepython = pypy3.5
 deps = {[testenv:pypy]deps}
 commands = {[testenv:pypy]commands}
 
-[testenv:coverage-report]
-deps = coverage
-skip_install = true
+[testenv:macos]
+# Travis CI macOS image does not have slapd
+# SDK libldap does not support ldap_init_fd
+basepython = python3
+deps = {[testenv]deps}
+passenv = {[testenv]passenv}
+setenv =
+    CI_DISABLED=INIT_FD
 commands =
-    {envpython} -m coverage combine
-    {envpython} -m coverage report --show-missing
-    {envpython} -m coverage html
+    {envpython} -m unittest -v \
+        Tests/t_cidict.py \
+        Tests/t_ldap_dn.py \
+        Tests/t_ldap_filter.py \
+        Tests/t_ldap_functions.py \
+        Tests/t_ldap_modlist.py \
+        Tests/t_ldap_schema_tokenizer.py \
+        Tests/t_ldapurl.py \
+        Tests/t_ldif.py \
+        Tests/t_untested_mods.py
 
 [testenv:doc]
 basepython = python3