예제 #1
0
    def authenticate_xmpp(self):
        """Authenticate the user to the XMPP server via the BOSH connection."""

        self.request_sid()

        self.log.debug('Prepare the XMPP authentication')

        # Instantiate a sasl object 
        sasl = SASLClient(host=self.to,
                         service='xmpp',
                         username=self.jid,
                         password=self.password)

        # Choose an auth mechanism
        sasl.choose_mechanism(self.server_auth, allow_anonymous=False)

        # Request challenge
        challenge = self.get_challenge(sasl.mechanism)
        
        # Process challenge and generate response
        response = sasl.process(base64.b64decode(challenge))

        # Send response
        success = self.send_challenge_response(response)
        if not success:
            return False

        self.request_restart()

        self.bind_resource()
        
        return True
예제 #2
0
    def authenticate_xmpp(self):
        """Authenticate the user to the XMPP server via the BOSH connection."""

        self.request_sid()

        self.log.debug('Prepare the XMPP authentication')

        # Instantiate a sasl object
        sasl = SASLClient(host=self.to,
                          service='xmpp',
                          username=self.jid,
                          password=self.password)

        # Choose an auth mechanism
        sasl.choose_mechanism(self.server_auth, allow_anonymous=False)

        # Request challenge
        challenge = self.get_challenge(sasl.mechanism)

        # Process challenge and generate response
        challengeString = base64.b64decode(challenge)
        if not 'realm=' in challengeString:
            challengeString += ',realm="random"'
        response = sasl.process(challengeString)
        # Send response
        resp_root = self.send_challenge_response(response)

        success = self.check_authenticate_success(resp_root)
        if success is None and\
                resp_root.find('{{{0}}}challenge'.format(XMPP_SASL_NS)) is not None:
            resp_root = self.send_challenge_response('')
            return self.check_authenticate_success(resp_root)
        return success
예제 #3
0
    def authenticate_xmpp(self):
        """Authenticate the user to the XMPP server via the BOSH connection."""

        self.request_sid()

        self.log.debug('Prepare the XMPP authentication')

        # Instantiate a sasl object
        sasl = SASLClient(
            host=self.to,
            service='xmpp',
            username=self.jid,
            password=self.password
        )

        # Choose an auth mechanism
        sasl.choose_mechanism(self.server_auth, allow_anonymous=False)

        # Request challenge
        challenge = self.get_challenge(sasl.mechanism)

        # Process challenge and generate response
        response = sasl.process(base64.b64decode(challenge))

        # Send response
        resp_root = self.send_challenge_response(response)

        success = self.check_authenticate_success(resp_root)
        if success is None and\
                resp_root.find('{{{0}}}challenge'.format(XMPP_SASL_NS)) is not None:
            resp_root = self.send_challenge_response('')
            return self.check_authenticate_success(resp_root)
        return success
예제 #4
0
    def test_choose_mechanism(self):
        client = SASLClient('localhost', service='something')
        choices = ['invalid']
        self.assertRaises(SASLError, client.choose_mechanism, choices)

        choices = [m for m in mechanisms.values() if m is not DigestMD5Mechanism]
        mech_names = set(m.name for m in choices)
        client.choose_mechanism(mech_names)
        self.assertIsInstance(client._chosen_mech, max(choices, key=lambda m: m.score))

        anon_names = set(m.name for m in choices if m.allows_anonymous)
        client.choose_mechanism(anon_names)
        self.assertIn(client.mechanism, anon_names)
        self.assertRaises(SASLError, client.choose_mechanism, anon_names, allow_anonymous=False)

        plain_names = set(m.name for m in choices if m.uses_plaintext)
        client.choose_mechanism(plain_names)
        self.assertIn(client.mechanism, plain_names)
        self.assertRaises(SASLError, client.choose_mechanism, plain_names, allow_plaintext=False)

        not_active_names = set(m.name for m in choices if not m.active_safe)
        client.choose_mechanism(not_active_names)
        self.assertIn(client.mechanism, not_active_names)
        self.assertRaises(SASLError, client.choose_mechanism, not_active_names, allow_active=False)

        not_dict_names = set(m.name for m in choices if not m.dictionary_safe)
        client.choose_mechanism(not_dict_names)
        self.assertIn(client.mechanism, not_dict_names)
        self.assertRaises(SASLError, client.choose_mechanism, not_dict_names, allow_dictionary=False)
예제 #5
0
    def test_choose_mechanism(self):
        client = SASLClient('localhost', service='something')
        choices = ['invalid']
        self.assertRaises(SASLError, client.choose_mechanism, choices)

        choices = [m for m in mechanisms.values() if m is not DigestMD5Mechanism]
        mech_names = set(m.name for m in choices)
        client.choose_mechanism(mech_names)
        self.assertIsInstance(client._chosen_mech, max(choices, key=lambda m: m.score))

        anon_names = set(m.name for m in choices if m.allows_anonymous)
        client.choose_mechanism(anon_names)
        self.assertIn(client.mechanism, anon_names)
        self.assertRaises(SASLError, client.choose_mechanism, anon_names, allow_anonymous=False)

        plain_names = set(m.name for m in choices if m.uses_plaintext)
        client.choose_mechanism(plain_names)
        self.assertIn(client.mechanism, plain_names)
        self.assertRaises(SASLError, client.choose_mechanism, plain_names, allow_plaintext=False)

        not_active_names = set(m.name for m in choices if not m.active_safe)
        client.choose_mechanism(not_active_names)
        self.assertIn(client.mechanism, not_active_names)
        self.assertRaises(SASLError, client.choose_mechanism, not_active_names, allow_active=False)

        not_dict_names = set(m.name for m in choices if not m.dictionary_safe)
        client.choose_mechanism(not_dict_names)
        self.assertIn(client.mechanism, not_dict_names)
        self.assertRaises(SASLError, client.choose_mechanism, not_dict_names, allow_dictionary=False)
예제 #6
0
class SaslRpcClient:
    def __init__(self, trans, hdfs_namenode_principal=None):
        self.sasl = None
        self._trans = trans
        self.hdfs_namenode_principal = hdfs_namenode_principal

    def _send_sasl_message(self, message):
        rpcheader = RpcRequestHeaderProto()
        rpcheader.rpcKind = 2  # RPC_PROTOCOL_BUFFER
        rpcheader.rpcOp = 0
        rpcheader.callId = -33  # SASL
        rpcheader.retryCount = -1
        rpcheader.clientId = b""

        s_rpcheader = rpcheader.SerializeToString()
        s_message = message.SerializeToString()

        header_length = len(s_rpcheader) + encoder._VarintSize(
            len(s_rpcheader)) + len(s_message) + encoder._VarintSize(
                len(s_message))

        self._trans.write(struct.pack('!I', header_length))
        self._trans.write_delimited(s_rpcheader)
        self._trans.write_delimited(s_message)

        log_protobuf_message("Send out", message)

    def _recv_sasl_message(self):
        bytestream = self._trans.recv_rpc_message()
        sasl_response = self._trans.parse_response(bytestream, RpcSaslProto)

        return sasl_response

    def connect(self):
        # use service name component from principal
        service = re.split('[\/@]', str(self.hdfs_namenode_principal))[0]

        if not self.sasl:
            self.sasl = SASLClient(self._trans.host, service)

        negotiate = RpcSaslProto()
        negotiate.state = 1
        self._send_sasl_message(negotiate)

        # do while true
        while True:
            res = self._recv_sasl_message()
            # TODO: check mechanisms
            if res.state == 1:
                mechs = []
                for auth in res.auths:
                    mechs.append(auth.mechanism)

                log.debug("Available mechs: %s" % (",".join(mechs)))
                self.sasl.choose_mechanism(mechs, allow_anonymous=False)
                log.debug("Chosen mech: %s" % self.sasl.mechanism)

                initiate = RpcSaslProto()
                initiate.state = 2
                initiate.token = self.sasl.process()

                for auth in res.auths:
                    if auth.mechanism == self.sasl.mechanism:
                        auth_method = initiate.auths.add()
                        auth_method.mechanism = self.sasl.mechanism
                        auth_method.method = auth.method
                        auth_method.protocol = auth.protocol
                        auth_method.serverId = self._trans.host

                self._send_sasl_message(initiate)
                continue

            if res.state == 3:
                res_token = self._evaluate_token(res)
                response = RpcSaslProto()
                response.token = res_token
                response.state = 4
                self._send_sasl_message(response)
                continue

            if res.state == 0:
                return True

    def _evaluate_token(self, sasl_response):
        return self.sasl.process(challenge=sasl_response.token)

    def wrap(self, message):
        encoded = self.sasl.wrap(message)

        sasl_message = RpcSaslProto()
        sasl_message.state = 5  #  WRAP
        sasl_message.token = encoded

        self._send_sasl_message(sasl_message)

    def unwrap(self):
        response = self._recv_sasl_message()
        if response.state != 5:
            raise Exception("Server send non-wrapped response")

        return self.sasl.unwrap(response.token)

    def use_wrap(self):
        # SASL wrapping is only used if the connection has a QOP, and
        # the value is not auth.  ex. auth-int & auth-priv
        if self.sasl.qop.decode() == 'auth-int' or self.sasl.qop.decode(
        ) == 'auth-conf':
            return True
        return False
예제 #7
0
파일: net.py 프로젝트: ashafer01/laurelin
class LDAPSocket(object):
    """Holds a connection to an LDAP server.

    :param str host_uri: "scheme://netloc" to connect to
    :param int connect_timeout: Number of seconds to wait for connection to be accepted
    :param bool ssl_verify: Validate the certificate and hostname on an SSL/TLS connection
    :param str ssl_ca_file: Path to PEM-formatted concatenated CA certficates file
    :param str ssl_ca_path: Path to directory with CA certs under hashed file names. See
                            https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_load_verify_locations.html for more
                            information about the format of this directory.
    :param ssl_ca_data: An ASCII string of one or more PEM-encoded certs or a bytes object containing DER-encoded
                        certificates.
    :type ssl_ca_data: str or bytes
    """

    RECV_BUFFER = 4096

    # For ldapi:/// try to connect to these socket files in order
    # Globs must match exactly one result
    LDAPI_SOCKET_PATHS = ['/var/run/ldapi', '/var/run/slapd/ldapi', '/var/run/slapd-*.socket']

    # OIDs of unsolicited messages

    OID_DISCONNECTION_NOTICE = '1.3.6.1.4.1.1466.20036'  # RFC 4511 sec 4.4.1 Notice of Disconnection

    def __init__(self, host_uri, connect_timeout=5, ssl_verify=True, ssl_ca_file=None, ssl_ca_path=None,
                 ssl_ca_data=None):

        self._prop_init(connect_timeout)
        self._uri_connect(host_uri, ssl_verify, ssl_ca_file, ssl_ca_path, ssl_ca_data)

    def _prop_init(self, connect_timeout=5):
        # get socket ID number
        global _next_sock_id
        self.ID = _next_sock_id
        _next_sock_id += 1

        # misc init
        self._message_queues = {}
        self._next_message_id = 1
        self._sasl_client = None

        self.refcount = 0
        self.bound = False
        self.unbound = False
        self.abandoned_mids = []
        self.started_tls = False
        self.connect_timeout = connect_timeout

    def _parse_uri(self, host_uri):
        # parse host_uri
        parts = host_uri.split('://')
        if len(parts) == 1:
            netloc = unquote(parts[0])
            if netloc[0] == '/':
                scheme = 'ldapi'
            else:
                scheme = 'ldap'
        elif len(parts) == 2:
            scheme = parts[0]
            netloc = unquote(parts[1])
        else:
            raise LDAPError('Invalid host_uri')
        self.uri = '{0}://{1}'.format(scheme, netloc)
        return scheme, netloc

    def _uri_connect(self, host_uri, ssl_verify, ssl_ca_file, ssl_ca_path, ssl_ca_data):
        # connect
        scheme, netloc = self._parse_uri(host_uri)
        logger.info('Connecting to {0} on #{1}'.format(self.uri, self.ID))
        if scheme == 'ldap':
            self._inet_connect(netloc, 389)
        elif scheme == 'ldaps':
            self._inet_connect(netloc, 636)
            self.start_tls(ssl_verify, ssl_ca_file, ssl_ca_path, ssl_ca_data)
            logger.info('Connected with TLS on #{0}'.format(self.ID))
        elif scheme == 'ldapi':
            if not _have_unix_socket:
                raise LDAPError('Unix sockets are not supported on your platform, please choose a protocol other'
                                'than ldapi')
            self.sock_path = None
            self._sock = socket(AF_UNIX)
            self.host = 'localhost'

            if netloc == '/':
                for sockGlob in LDAPSocket.LDAPI_SOCKET_PATHS:
                    fn = glob(sockGlob)
                    if not fn:
                        continue
                    if len(fn) > 1:
                        logger.debug('Multiple results for glob {0}'.format(sockGlob))
                        continue
                    fn = fn[0]
                    try:
                        self._connect(fn)
                        self.sock_path = fn
                        break
                    except SocketError:
                        continue
                if self.sock_path is None:
                    raise LDAPConnectionError('Could not find any local LDAPI unix socket - full '
                                              'socket path must be supplied in URI')
            else:
                try:
                    self._connect(netloc)
                    self.sock_path = netloc
                except SocketError as e:
                    raise LDAPConnectionError('failed connect to unix socket {0} - {1} ({2})'.format(
                        netloc, e.strerror, e.errno
                    ))

            logger.debug('Connected to unix socket {0} on #{1}'.format(self.sock_path, self.ID))
        else:
            raise LDAPError('Unsupported scheme "{0}"'.format(scheme))

    def _connect(self, addr):
        self._sock.settimeout(self.connect_timeout)
        self._sock.connect(addr)
        self._sock.settimeout(None)

    def _inet_connect(self, netloc, default_port):
        ap = netloc.rsplit(':', 1)
        self.host = ap[0]
        if len(ap) == 1:
            port = default_port
        else:
            port = int(ap[1])
        try:
            self._sock = create_connection((self.host, port), self.connect_timeout)
            logger.debug('Connected to {0}:{1} on #{2}'.format(self.host, port, self.ID))
        except SocketError as e:
            raise LDAPConnectionError('failed connect to {0}:{1} - {2} ({3})'.format(
                                      self.host, port, e.strerror, e.errno))

    def start_tls(self, verify=True, ca_file=None, ca_path=None, ca_data=None):
        """Install TLS layer on this socket connection.

        :param bool verify: Validate the certificate and hostname on an SSL/TLS connection
        :param str ca_file: Path to PEM-formatted concatenated CA certficates file
        :param str ca_path: Path to directory with CA certs under hashed file names. See
                            https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_load_verify_locations.html for more
                            information about the format of this directory.
        :param ca_data: An ASCII string of one or more PEM-encoded certs or a bytes object containing DER-encoded
                        certificates.
        :type ca_data: str or bytes
        """
        if self.started_tls:
            raise LDAPError('TLS layer already installed')

        if verify:
            verify_mode = ssl.CERT_REQUIRED
        else:
            verify_mode = ssl.CERT_NONE

        try:
            proto = ssl.PROTOCOL_TLS
        except AttributeError:
            proto = ssl.PROTOCOL_SSLv23

        try:
            ctx = ssl.SSLContext(proto)
            ctx.verify_mode = verify_mode
            ctx.check_hostname = False  # we do this ourselves
            if verify:
                ctx.load_default_certs()
            if ca_file or ca_path or ca_data:
                ctx.load_verify_locations(cafile=ca_file, capath=ca_path, cadata=ca_data)
            self._sock = ctx.wrap_socket(self._sock)
        except AttributeError:
            # SSLContext wasn't added until 2.7.9
            if ca_path or ca_data:
                raise RuntimeError('python version >= 2.7.9 required for SSL ca_path/ca_data')

            self._sock = ssl.wrap_socket(self._sock, ca_certs=ca_file, cert_reqs=verify_mode, ssl_version=proto)

        if verify:
            cert = self._sock.getpeercert()
            cert_cn = dict([e[0] for e in cert['subject']])['commonName']
            self.check_hostname(cert_cn, cert)
        else:
            logger.debug('Skipping hostname validation')
        self.started_tls = True
        logger.debug('Installed TLS layer on #{0}'.format(self.ID))

    def check_hostname(self, cert_cn, cert):
        """SSL check_hostname according to RFC 4513 sec 3.1.3. Compares supplied values against ``self.host`` to
        determine the validity of the cert.

        :param str cert_cn: The common name of the cert
        :param dict cert: A dictionary representing the rest of the cert. Checks key subjectAltNames for a list of
                          (type, value) tuples, where type is 'DNS' or 'IP'. DNS supports leading wildcard.
        :rtype: None
        :raises LDAPConnectionError: if no supplied values match ``self.host``
        """
        if self.host == cert_cn:
            logger.debug('Matched server identity to cert commonName')
        else:
            valid = False
            tried = [cert_cn]
            for type, value in cert.get('subjectAltName', []):
                if type == 'DNS' and value.startswith('*.'):
                    valid = self.host.endswith(value[1:])
                else:
                    valid = (self.host == value)
                tried.append(value)
                if valid:
                    logger.debug('Matched server identity to cert {0} subjectAltName'.format(type))
                    break
            if not valid:
                raise LDAPConnectionError('Server identity "{0}" does not match any cert names: {1}'.format(
                    self.host, ', '.join(tried)))

    def sasl_init(self, mechs, **props):
        """Initialize a :class:`.puresasl.client.SASLClient`"""
        self._sasl_client = SASLClient(self.host, 'ldap', **props)
        self._sasl_client.choose_mechanism(mechs)

    def _has_sasl_client(self):
        return self._sasl_client is not None

    def _require_sasl_client(self):
        if not self._has_sasl_client():
            raise LDAPSASLError('SASL init not complete')

    @property
    def sasl_qop(self):
        """Obtain the chosen quality of protection"""
        self._require_sasl_client()
        return self._sasl_client.qop

    @property
    def sasl_mech(self):
        """Obtain the chosen mechanism"""
        self._require_sasl_client()
        mech = self._sasl_client.mechanism
        if mech is None:
            raise LDAPSASLError('SASL init not complete - no mech chosen')
        else:
            return mech

    def sasl_process_auth_challenge(self, challenge):
        """Process an auth challenge and return the correct response"""
        self._require_sasl_client()
        return self._sasl_client.process(challenge)

    def _prep_message(self, op, obj, controls=None):
        """Prepare a message for transmission"""
        mid = self._next_message_id
        self._next_message_id += 1
        lm = pack(mid, op, obj, controls)
        raw = ber_encode(lm)
        if self._has_sasl_client():
            raw = self._sasl_client.wrap(raw)
        return mid, raw

    def send_message(self, op, obj, controls=None):
        """Create and send an LDAPMessage given an operation name and a corresponding object.

        Operation names must be defined as component names in laurelin.ldap.rfc4511.ProtocolOp and
        the object must be of the corresponding type.

        :param str op: The protocol operation name
        :param object obj: The associated protocol object (see :class:`.rfc4511.ProtocolOp` for mapping.
        :param controls: Any request controls for the message
        :type controls: rfc4511.Controls or None
        :return: The message ID for this message
        :rtype: int
        """
        mid, raw = self._prep_message(op, obj, controls)
        self._sock.sendall(raw)
        return mid

    def recv_one(self, want_message_id):
        """Get the next message with ``want_message_id`` being sent by the server

        :param int want_message_id: The desired message ID.
        :return: The LDAP message
        :rtype: rfc4511.LDAPMessage
        """
        return next(self.recv_messages(want_message_id))

    def recv_messages(self, want_message_id):
        """Iterate all messages with ``want_message_id`` being sent by the server.

        :param int want_message_id: The desired message ID.
        :return: An iterator over :class:`.rfc4511.LDAPMessage`.
        """
        flush_queue = True
        raw = b''
        while True:
            if flush_queue:
                if want_message_id in self._message_queues:
                    q = self._message_queues[want_message_id]
                    while True:
                        if len(q) == 0:
                            break
                        obj = q.popleft()
                        if len(q) == 0:
                            del self._message_queues[want_message_id]
                        yield obj
            else:
                flush_queue = True
            if want_message_id in self.abandoned_mids:
                return
            try:
                newraw = self._sock.recv(LDAPSocket.RECV_BUFFER)
                if self._has_sasl_client():
                    newraw = self._sasl_client.unwrap(newraw)
                raw += newraw
                while len(raw) > 0:
                    response, raw = ber_decode(raw, asn1Spec=LDAPMessage())
                    have_message_id = response.getComponentByName('messageID')
                    if want_message_id == have_message_id:
                        yield response
                    elif have_message_id == 0:
                        msg = 'Received unsolicited message (default message - should never be seen)'
                        try:
                            mid, xr, ctrls = unpack('extendedResp', response)
                            res_code = xr.getComponentByName('resultCode')
                            xr_oid = six.text_type(xr.getComponentByName('responseName'))
                            if xr_oid == LDAPSocket.OID_DISCONNECTION_NOTICE:
                                mtype = 'Notice of Disconnection'
                            else:
                                mtype = 'Unhandled ({0})'.format(xr_oid)
                            diag = xr.getComponentByName('diagnosticMessage')
                            msg = 'Got unsolicited message: {0}: {1}: {2}'.format(mtype, res_code, diag)
                            if res_code == ResultCode('protocolError'):
                                msg += (' (This may indicate an incompatability between laurelin-ldap and your server '
                                        'distribution)')
                            elif res_code == ResultCode('strongerAuthRequired'):
                                # this is a direct quote from RFC 4511 sec 4.4.1
                                msg += (' (The server has detected that an established security association between the'
                                        ' client and server has unexpectedly failed or been compromised)')
                        except UnexpectedResponseType:
                            msg = 'Unhandled unsolicited message from server'
                        finally:
                            raise LDAPUnsolicitedMessage(response, msg)
                    else:
                        if have_message_id not in self._message_queues:
                            self._message_queues[have_message_id] = deque()
                        self._message_queues[have_message_id].append(response)
            except SubstrateUnderrunError:
                flush_queue = False
                continue

    def close(self):
        """Close the low-level socket connection."""
        return self._sock.close()
예제 #8
0
class LDAPSocket(object):
    """Holds a connection to an LDAP server.

    :param str host_uri: "scheme://netloc" to connect to
    :param int connect_timeout: Number of seconds to wait for connection to be accepted
    :param bool ssl_verify: Validate the certificate and hostname on an SSL/TLS connection
    :param str ssl_ca_file: Path to PEM-formatted concatenated CA certficates file
    :param str ssl_ca_path: Path to directory with CA certs under hashed file names. See
                            https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_load_verify_locations.html for more
                            information about the format of this directory.
    :param ssl_ca_data: An ASCII string of one or more PEM-encoded certs or a bytes object containing DER-encoded
                        certificates.
    :type ssl_ca_data: str or bytes
    """

    RECV_BUFFER = 4096

    # For ldapi:/// try to connect to these socket files in order
    # Globs must match exactly one result
    LDAPI_SOCKET_PATHS = [
        '/var/run/ldapi', '/var/run/slapd/ldapi', '/var/run/slapd-*.socket'
    ]

    # OIDs of unsolicited messages

    OID_DISCONNECTION_NOTICE = '1.3.6.1.4.1.1466.20036'  # RFC 4511 sec 4.4.1 Notice of Disconnection

    def __init__(self,
                 host_uri,
                 connect_timeout=5,
                 ssl_verify=True,
                 ssl_ca_file=None,
                 ssl_ca_path=None,
                 ssl_ca_data=None):

        self._prop_init(connect_timeout)
        self._uri_connect(host_uri, ssl_verify, ssl_ca_file, ssl_ca_path,
                          ssl_ca_data)

    def _prop_init(self, connect_timeout=5):
        # get socket ID number
        global _next_sock_id
        self.ID = _next_sock_id
        _next_sock_id += 1

        # misc init
        self._message_queues = {}
        self._next_message_id = 1
        self._sasl_client = None

        self.refcount = 0
        self.bound = False
        self.unbound = False
        self.abandoned_mids = []
        self.started_tls = False
        self.connect_timeout = connect_timeout

    def _parse_uri(self, host_uri):
        # parse host_uri
        parts = host_uri.split('://')
        if len(parts) == 1:
            netloc = unquote(parts[0])
            if netloc[0] == '/':
                scheme = 'ldapi'
            else:
                scheme = 'ldap'
        elif len(parts) == 2:
            scheme = parts[0]
            netloc = unquote(parts[1])
        else:
            raise LDAPError('Invalid host_uri')
        self.uri = '{0}://{1}'.format(scheme, netloc)
        return scheme, netloc

    def _uri_connect(self, host_uri, ssl_verify, ssl_ca_file, ssl_ca_path,
                     ssl_ca_data):
        # connect
        scheme, netloc = self._parse_uri(host_uri)
        logger.info('Connecting to {0} on #{1}'.format(self.uri, self.ID))
        if scheme == 'ldap':
            self._inet_connect(netloc, 389)
        elif scheme == 'ldaps':
            self._inet_connect(netloc, 636)
            self.start_tls(ssl_verify, ssl_ca_file, ssl_ca_path, ssl_ca_data)
            logger.info('Connected with TLS on #{0}'.format(self.ID))
        elif scheme == 'ldapi':
            if not _have_unix_socket:
                raise LDAPError(
                    'Unix sockets are not supported on your platform, please choose a protocol other'
                    'than ldapi')
            self.sock_path = None
            self._sock = socket(AF_UNIX)
            self.host = 'localhost'

            if netloc == '/':
                for sockGlob in LDAPSocket.LDAPI_SOCKET_PATHS:
                    fn = glob(sockGlob)
                    if not fn:
                        continue
                    if len(fn) > 1:
                        logger.debug(
                            'Multiple results for glob {0}'.format(sockGlob))
                        continue
                    fn = fn[0]
                    try:
                        self._connect(fn)
                        self.sock_path = fn
                        break
                    except SocketError:
                        continue
                if self.sock_path is None:
                    raise LDAPConnectionError(
                        'Could not find any local LDAPI unix socket - full '
                        'socket path must be supplied in URI')
            else:
                try:
                    self._connect(netloc)
                    self.sock_path = netloc
                except SocketError as e:
                    raise LDAPConnectionError(
                        'failed connect to unix socket {0} - {1} ({2})'.format(
                            netloc, e.strerror, e.errno))

            logger.debug('Connected to unix socket {0} on #{1}'.format(
                self.sock_path, self.ID))
        else:
            raise LDAPError('Unsupported scheme "{0}"'.format(scheme))

    def _connect(self, addr):
        self._sock.settimeout(self.connect_timeout)
        self._sock.connect(addr)
        self._sock.settimeout(None)

    def _inet_connect(self, netloc, default_port):
        ap = netloc.rsplit(':', 1)
        self.host = ap[0]
        if len(ap) == 1:
            port = default_port
        else:
            port = int(ap[1])
        try:
            self._sock = create_connection((self.host, port),
                                           self.connect_timeout)
            logger.debug('Connected to {0}:{1} on #{2}'.format(
                self.host, port, self.ID))
        except SocketError as e:
            raise LDAPConnectionError(
                'failed connect to {0}:{1} - {2} ({3})'.format(
                    self.host, port, e.strerror, e.errno))

    def start_tls(self, verify=True, ca_file=None, ca_path=None, ca_data=None):
        """Install TLS layer on this socket connection.

        :param bool verify: Validate the certificate and hostname on an SSL/TLS connection
        :param str ca_file: Path to PEM-formatted concatenated CA certficates file
        :param str ca_path: Path to directory with CA certs under hashed file names. See
                            https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_load_verify_locations.html for more
                            information about the format of this directory.
        :param ca_data: An ASCII string of one or more PEM-encoded certs or a bytes object containing DER-encoded
                        certificates.
        :type ca_data: str or bytes
        """
        if self.started_tls:
            raise LDAPError('TLS layer already installed')

        if verify:
            verify_mode = ssl.CERT_REQUIRED
        else:
            verify_mode = ssl.CERT_NONE

        try:
            proto = ssl.PROTOCOL_TLS
        except AttributeError:
            proto = ssl.PROTOCOL_SSLv23

        try:
            ctx = ssl.SSLContext(proto)
            ctx.verify_mode = verify_mode
            ctx.check_hostname = False  # we do this ourselves
            if verify:
                ctx.load_default_certs()
            if ca_file or ca_path or ca_data:
                ctx.load_verify_locations(cafile=ca_file,
                                          capath=ca_path,
                                          cadata=ca_data)
            self._sock = ctx.wrap_socket(self._sock)
        except AttributeError:
            # SSLContext wasn't added until 2.7.9
            if ca_path or ca_data:
                raise RuntimeError(
                    'python version >= 2.7.9 required for SSL ca_path/ca_data')

            self._sock = ssl.wrap_socket(self._sock,
                                         ca_certs=ca_file,
                                         cert_reqs=verify_mode,
                                         ssl_version=proto)

        if verify:
            cert = self._sock.getpeercert()
            cert_cn = dict([e[0] for e in cert['subject']])['commonName']
            self.check_hostname(cert_cn, cert)
        else:
            logger.debug('Skipping hostname validation')
        self.started_tls = True
        logger.debug('Installed TLS layer on #{0}'.format(self.ID))

    def check_hostname(self, cert_cn, cert):
        """SSL check_hostname according to RFC 4513 sec 3.1.3. Compares supplied values against ``self.host`` to
        determine the validity of the cert.

        :param str cert_cn: The common name of the cert
        :param dict cert: A dictionary representing the rest of the cert. Checks key subjectAltNames for a list of
                          (type, value) tuples, where type is 'DNS' or 'IP'. DNS supports leading wildcard.
        :rtype: None
        :raises LDAPConnectionError: if no supplied values match ``self.host``
        """
        if self.host == cert_cn:
            logger.debug('Matched server identity to cert commonName')
        else:
            valid = False
            tried = [cert_cn]
            for type, value in cert.get('subjectAltName', []):
                if type == 'DNS' and value.startswith('*.'):
                    valid = self.host.endswith(value[1:])
                else:
                    valid = (self.host == value)
                tried.append(value)
                if valid:
                    logger.debug(
                        'Matched server identity to cert {0} subjectAltName'.
                        format(type))
                    break
            if not valid:
                raise LDAPConnectionError(
                    'Server identity "{0}" does not match any cert names: {1}'.
                    format(self.host, ', '.join(tried)))

    def sasl_init(self, mechs, **props):
        """Initialize a :class:`.puresasl.client.SASLClient`"""
        self._sasl_client = SASLClient(self.host, 'ldap', **props)
        self._sasl_client.choose_mechanism(mechs)

    def _has_sasl_client(self):
        return self._sasl_client is not None

    def _require_sasl_client(self):
        if not self._has_sasl_client():
            raise LDAPSASLError('SASL init not complete')

    @property
    def sasl_qop(self):
        """Obtain the chosen quality of protection"""
        self._require_sasl_client()
        return self._sasl_client.qop

    @property
    def sasl_mech(self):
        """Obtain the chosen mechanism"""
        self._require_sasl_client()
        mech = self._sasl_client.mechanism
        if mech is None:
            raise LDAPSASLError('SASL init not complete - no mech chosen')
        else:
            return mech

    def sasl_process_auth_challenge(self, challenge):
        """Process an auth challenge and return the correct response"""
        self._require_sasl_client()
        return self._sasl_client.process(challenge)

    def _prep_message(self, op, obj, controls=None):
        """Prepare a message for transmission"""
        mid = self._next_message_id
        self._next_message_id += 1
        lm = pack(mid, op, obj, controls)
        raw = ber_encode(lm)
        if self._has_sasl_client():
            raw = self._sasl_client.wrap(raw)
        return mid, raw

    def send_message(self, op, obj, controls=None):
        """Create and send an LDAPMessage given an operation name and a corresponding object.

        Operation names must be defined as component names in laurelin.ldap.rfc4511.ProtocolOp and
        the object must be of the corresponding type.

        :param str op: The protocol operation name
        :param object obj: The associated protocol object (see :class:`.rfc4511.ProtocolOp` for mapping.
        :param controls: Any request controls for the message
        :type controls: rfc4511.Controls or None
        :return: The message ID for this message
        :rtype: int
        """
        mid, raw = self._prep_message(op, obj, controls)
        self._sock.sendall(raw)
        return mid

    def recv_one(self, want_message_id):
        """Get the next message with ``want_message_id`` being sent by the server

        :param int want_message_id: The desired message ID.
        :return: The LDAP message
        :rtype: rfc4511.LDAPMessage
        """
        return next(self.recv_messages(want_message_id))

    def recv_messages(self, want_message_id):
        """Iterate all messages with ``want_message_id`` being sent by the server.

        :param int want_message_id: The desired message ID.
        :return: An iterator over :class:`.rfc4511.LDAPMessage`.
        """
        flush_queue = True
        raw = b''
        while True:
            if flush_queue:
                if want_message_id in self._message_queues:
                    q = self._message_queues[want_message_id]
                    while True:
                        if len(q) == 0:
                            break
                        obj = q.popleft()
                        if len(q) == 0:
                            del self._message_queues[want_message_id]
                        yield obj
            else:
                flush_queue = True
            if want_message_id in self.abandoned_mids:
                raise StopIteration()
            try:
                newraw = self._sock.recv(LDAPSocket.RECV_BUFFER)
                if self._has_sasl_client():
                    newraw = self._sasl_client.unwrap(newraw)
                raw += newraw
                while len(raw) > 0:
                    response, raw = ber_decode(raw, asn1Spec=LDAPMessage())
                    have_message_id = response.getComponentByName('messageID')
                    if want_message_id == have_message_id:
                        yield response
                    elif have_message_id == 0:
                        msg = 'Received unsolicited message (default message - should never be seen)'
                        try:
                            mid, xr, ctrls = unpack('extendedResp', response)
                            res_code = xr.getComponentByName('resultCode')
                            xr_oid = six.text_type(
                                xr.getComponentByName('responseName'))
                            if xr_oid == LDAPSocket.OID_DISCONNECTION_NOTICE:
                                mtype = 'Notice of Disconnection'
                            else:
                                mtype = 'Unhandled ({0})'.format(xr_oid)
                            diag = xr.getComponentByName('diagnosticMessage')
                            msg = 'Got unsolicited message: {0}: {1}: {2}'.format(
                                mtype, res_code, diag)
                            if res_code == ResultCode('protocolError'):
                                msg += (
                                    ' (This may indicate an incompatability between laurelin-ldap and your server '
                                    'distribution)')
                            elif res_code == ResultCode(
                                    'strongerAuthRequired'):
                                # this is a direct quote from RFC 4511 sec 4.4.1
                                msg += (
                                    ' (The server has detected that an established security association between the'
                                    'client and server has unexpectedly failed or been compromised)'
                                )
                        except UnexpectedResponseType:
                            msg = 'Unhandled unsolicited message from server'
                        finally:
                            raise LDAPUnsolicitedMessage(response, msg)
                    else:
                        if have_message_id not in self._message_queues:
                            self._message_queues[have_message_id] = deque()
                        self._message_queues[have_message_id].append(response)
            except SubstrateUnderrunError:
                flush_queue = False
                continue

    def close(self):
        """Close the low-level socket connection."""
        return self._sock.close()