Source code for aioxmpp.connector

########################################################################
# File name: connector.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.connector` --- Ways to establish XML streams
###########################################################

This module provides classes to establish XML streams. Currently, there are two
different ways to establish XML streams: normal TCP connection which is then
upgraded using STARTTLS, and directly using TLS.

.. versionadded:: 0.6

   The whole module was added in version 0.6.

Abstract base class
===================

The connectors share a common abstract base class, :class:`BaseConnector`:

.. autoclass:: BaseConnector

Specific connectors
===================

.. autoclass:: STARTTLSConnector

.. autoclass:: XMPPOverTLSConnector

"""

import abc
import asyncio
import logging

from datetime import timedelta

import aioxmpp.errors as errors
import aioxmpp.nonza as nonza
import aioxmpp.protocol as protocol
import aioxmpp.ssl_transport as ssl_transport


def to_ascii(s):
    return s.encode("idna").decode("ascii")


[docs]class BaseConnector(metaclass=abc.ABCMeta): """ This is the base class for connectors. It defines the public interface of all connectors. .. autoattribute:: tls_supported .. automethod:: connect Existing connectors: .. autosummary:: STARTTLSConnector XMPPOverTLSConnector """ @abc.abstractproperty def tls_supported(self): """ Boolean which indicates whether TLS is supported by this connector. """ @abc.abstractproperty def dane_supported(self): """ Boolean which indicates whether DANE is supported by this connector. """
[docs] @abc.abstractmethod async def connect(self, loop, metadata, domain, host, port, negotiation_timeout, base_logger=None): """ Establish a :class:`.protocol.XMLStream` for `domain` with the given `host` at the given TCP `port`. `metadata` must be a :class:`.security_layer.SecurityLayer` instance to use for the connection. `loop` must be a :class:`asyncio.BaseEventLoop` to use. `negotiation_timeout` must be the maximum time in seconds to wait for the server to reply in each negotiation step. The `negotiation_timeout` is used as value for :attr:`~aioxmpp.protocol.XMLStream.deadtime_hard_limit` in the returned stream. Return a triple consisting of the :class:`asyncio.Transport`, the :class:`.protocol.XMLStream` and the :class:`aioxmpp.nonza.StreamFeatures` of the stream. To detect the use of TLS on the stream, check whether :meth:`asyncio.Transport.get_extra_info` returns a non-:data:`None` value for ``"ssl_object"``. `base_logger` is passed to :class:`aioxmpp.protocol.XMLStream`. .. versionchanged:: 0.10 Assignment of :attr:`~aioxmpp.protocol.XMLStream.deadtime_hard_limit` was added. """
[docs]class STARTTLSConnector(BaseConnector): """ Establish an XML stream using STARTTLS. .. automethod:: connect """ @property def tls_supported(self): return True @property def dane_supported(self): return False
[docs] async def connect(self, loop, metadata, domain: str, host, port, negotiation_timeout, base_logger=None): """ .. seealso:: :meth:`BaseConnector.connect` For general information on the :meth:`connect` method. Connect to `host` at TCP port number `port`. The :class:`aioxmpp.security_layer.SecurityLayer` object `metadata` is used to determine the parameters of the TLS connection. First, a normal TCP connection is opened and the stream header is sent. The stream features are waited for, and then STARTTLS is negotiated if possible. :attr:`~.security_layer.SecurityLayer.tls_required` is honoured: if it is true and TLS negotiation fails, :class:`~.errors.TLSUnavailable` is raised. TLS negotiation is always attempted if :attr:`~.security_layer.SecurityLayer.tls_required` is true, even if the server does not advertise a STARTTLS stream feature. This might help to prevent trivial downgrade attacks, and we don’t have anything to lose at this point anymore anyways. :attr:`~.security_layer.SecurityLayer.ssl_context_factory` and :attr:`~.security_layer.SecurityLayer.certificate_verifier_factory` are used to configure the TLS connection. .. versionchanged:: 0.10 The `negotiation_timeout` is set as :attr:`~.XMLStream.deadtime_hard_limit` on the returned XML stream. """ features_future = asyncio.Future(loop=loop) stream = protocol.XMLStream( to=domain, features_future=features_future, base_logger=base_logger, ) if base_logger is not None: logger = base_logger.getChild(type(self).__name__) else: logger = logging.getLogger(".".join([ __name__, type(self).__qualname__, ])) try: transport, _ = await ssl_transport.create_starttls_connection( loop, lambda: stream, host=host, port=port, peer_hostname=host, server_hostname=to_ascii(domain), use_starttls=True, ) except: # NOQA stream.abort() raise stream.deadtime_hard_limit = timedelta(seconds=negotiation_timeout) features = await features_future try: features[nonza.StartTLSFeature] except KeyError: if not metadata.tls_required: return transport, stream, await features_future logger.debug( "attempting STARTTLS despite not announced since it is" " required") try: response = await protocol.send_and_wait_for( stream, [ nonza.StartTLS(), ], [ nonza.StartTLSFailure, nonza.StartTLSProceed, ] ) except errors.StreamError: raise errors.TLSUnavailable( "STARTTLS not supported by server, but required by client" ) if not isinstance(response, nonza.StartTLSProceed): if metadata.tls_required: message = ( "server failed to STARTTLS" ) protocol.send_stream_error_and_close( stream, condition=errors.StreamErrorCondition.POLICY_VIOLATION, text=message, ) raise errors.TLSUnavailable(message) return transport, stream, await features_future verifier = metadata.certificate_verifier_factory() await verifier.pre_handshake( domain, host, port, metadata, ) ssl_context = metadata.ssl_context_factory() verifier.setup_context(ssl_context, transport) await stream.starttls( ssl_context=ssl_context, post_handshake_callback=verifier.post_handshake, ) features = await protocol.reset_stream_and_get_features( stream, timeout=negotiation_timeout, ) return transport, stream, features
[docs]class XMPPOverTLSConnector(BaseConnector): """ Establish an XML stream using XMPP-over-TLS, as per :xep:`368`. .. automethod:: connect """ @property def dane_supported(self): return False @property def tls_supported(self): return True def _context_factory_factory(self, logger, metadata, verifier): def context_factory(transport): ssl_context = metadata.ssl_context_factory() if hasattr(ssl_context, "set_alpn_protos"): try: ssl_context.set_alpn_protos([b'xmpp-client']) except NotImplementedError: logger.warning( "the underlying OpenSSL library does not support ALPN" ) else: logger.warning( "OpenSSL.SSL.Context lacks set_alpn_protos - " "please update pyOpenSSL to a recent version" ) verifier.setup_context(ssl_context, transport) return ssl_context return context_factory
[docs] async def connect(self, loop, metadata, domain, host, port, negotiation_timeout, base_logger=None): """ .. seealso:: :meth:`BaseConnector.connect` For general information on the :meth:`connect` method. Connect to `host` at TCP port number `port`. The :class:`aioxmpp.security_layer.SecurityLayer` object `metadata` is used to determine the parameters of the TLS connection. The connector connects to the server by directly establishing TLS; no XML stream is started before TLS negotiation, in accordance to :xep:`368` and how legacy SSL was handled in the past. :attr:`~.security_layer.SecurityLayer.ssl_context_factory` and :attr:`~.security_layer.SecurityLayer.certificate_verifier_factory` are used to configure the TLS connection. .. versionchanged:: 0.10 The `negotiation_timeout` is set as :attr:`~.XMLStream.deadtime_hard_limit` on the returned XML stream. """ features_future = asyncio.Future(loop=loop) stream = protocol.XMLStream( to=domain, features_future=features_future, base_logger=base_logger, ) if base_logger is not None: logger = base_logger.getChild(type(self).__name__) else: logger = logging.getLogger(".".join([ __name__, type(self).__qualname__, ])) verifier = metadata.certificate_verifier_factory() await verifier.pre_handshake( domain, host, port, metadata, ) context_factory = self._context_factory_factory(logger, metadata, verifier) try: transport, _ = await ssl_transport.create_starttls_connection( loop, lambda: stream, host=host, port=port, peer_hostname=host, server_hostname=to_ascii(domain), post_handshake_callback=verifier.post_handshake, ssl_context_factory=context_factory, use_starttls=False, ) except: # NOQA stream.abort() raise stream.deadtime_hard_limit = timedelta(seconds=negotiation_timeout) return transport, stream, await features_future