[Python-modules-commits] [python-amqp] 02/06: New upstream version 2.2.1

Michael Fladischer fladi at moszumanska.debian.org
Sun Jul 30 21:42:57 UTC 2017


This is an automated email from the git hooks/post-receive script.

fladi pushed a commit to branch debian/experimental
in repository python-amqp.

commit 410ffa7e9f2f37dddade78431915c9d71298c9c0
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Sun Jul 30 23:35:35 2017 +0200

    New upstream version 2.2.1
---
 Changelog                      |  52 ++++++++++++++
 PKG-INFO                       |   6 +-
 README.rst                     |   2 +-
 amqp.egg-info/PKG-INFO         |   6 +-
 amqp.egg-info/SOURCES.txt      |   3 +
 amqp/__init__.py               |   2 +-
 amqp/basic_message.py          |   2 +-
 amqp/channel.py                |   2 +-
 amqp/connection.py             |  68 ++++++++++++------
 amqp/method_framing.py         |  20 +++---
 amqp/platform.py               |   9 ++-
 amqp/sasl.py                   | 159 +++++++++++++++++++++++++++++++++++++++++
 amqp/transport.py              |  50 +++++++++++--
 docs/conf.py                   |   4 +-
 docs/includes/introduction.txt |   2 +-
 docs/reference/amqp.sasl.rst   |  11 +++
 docs/reference/index.rst       |   1 +
 requirements/pkgutils.txt      |   2 +-
 setup.cfg                      |   1 -
 setup.py                       |   2 +-
 t/unit/test_connection.py      | 104 ++++++++++++++++++++++++---
 t/unit/test_platform.py        |   5 ++
 t/unit/test_sasl.py            | 149 ++++++++++++++++++++++++++++++++++++++
 t/unit/test_transport.py       |   6 +-
 24 files changed, 600 insertions(+), 68 deletions(-)

diff --git a/Changelog b/Changelog
index 7dd0f0c..aa7d3ea 100644
--- a/Changelog
+++ b/Changelog
@@ -5,6 +5,58 @@ py-amqp is fork of amqplib used by Kombu containing additional features and impr
 The previous amqplib changelog is here:
 http://code.google.com/p/py-amqplib/source/browse/CHANGES
 
+.. _version-2.2.1:
+
+2.2.1
+=====
+:release-date: 2017-07-14 09:00 A.M UTC+2
+:release-by: Omer Katz
+
+- Fix implicit conversion from bytes to string on the connection object. (Issue #155)
+
+  This issue has caused Celery to crash on connection to RabbitMQ.
+
+  Fix contributed by **Omer Katz**
+
+.. _version-2.2.0:
+
+2.2.0
+=====
+:release-date: 2017-07-12 10:00 A.M UTC+2
+:release-by: Ask Solem
+
+- Fix random delays in task execution.
+
+  This is a bug that caused performance issues due to polling timeouts that occur when receiving incomplete AMQP frames. (Issues #3978 #3737 #3814)
+
+  Fix contributed by **Robert Kopaczewski**
+
+- Calling ``conn.collect()`` multiple times will no longer raise an ``AttributeError`` when no channels exist.
+
+  Fix contributed by **Gord Chung**
+
+- Fix compatibility code for Python 2.7.6.
+
+  Fix contributed by **Jonathan Schuff**
+
+- When running in Windows, py-amqp will no longer use the unsupported TCP option TCP_MAXSEG.
+
+  Fix contributed by **Tony Breeds**
+
+- Added support for setting the SNI hostname header.
+
+  The SSL protocol version is now set to SSLv23
+
+  Contributed by **Dhananjay Sathe**
+
+- Authentication mechanisms were refactored to be more modular. GSSAPI authentication is now supported.
+
+  Contributed by **Alexander Dutton**
+
+- Do not reconnect on collect.
+
+  Fix contributed by **Gord Chung**
+
 .. _version-2.1.4:
 
 2.1.4
diff --git a/PKG-INFO b/PKG-INFO
index a33b36a..16cdb4b 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: amqp
-Version: 2.1.4
+Version: 2.2.1
 Summary: Low-level AMQP client for Python (fork of amqplib).
 Home-page: http://github.com/celery/py-amqp
 Author: Ask Solem
@@ -12,7 +12,7 @@ Description: ===================================================================
         
         |build-status| |coverage| |license| |wheel| |pyversion| |pyimp|
         
-        :Version: 2.1.4
+        :Version: 2.2.1
         :Web: https://amqp.readthedocs.io/
         :Download: http://pypi.python.org/pypi/amqp/
         :Source: http://github.com/celery/py-amqp/
@@ -144,9 +144,9 @@ Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
 Classifier: License :: OSI Approved :: BSD License
 Classifier: Intended Audience :: Developers
 Classifier: Operating System :: OS Independent
diff --git a/README.rst b/README.rst
index 087550f..be83c65 100644
--- a/README.rst
+++ b/README.rst
@@ -4,7 +4,7 @@
 
 |build-status| |coverage| |license| |wheel| |pyversion| |pyimp|
 
-:Version: 2.1.4
+:Version: 2.2.1
 :Web: https://amqp.readthedocs.io/
 :Download: http://pypi.python.org/pypi/amqp/
 :Source: http://github.com/celery/py-amqp/
diff --git a/amqp.egg-info/PKG-INFO b/amqp.egg-info/PKG-INFO
index a33b36a..16cdb4b 100644
--- a/amqp.egg-info/PKG-INFO
+++ b/amqp.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: amqp
-Version: 2.1.4
+Version: 2.2.1
 Summary: Low-level AMQP client for Python (fork of amqplib).
 Home-page: http://github.com/celery/py-amqp
 Author: Ask Solem
@@ -12,7 +12,7 @@ Description: ===================================================================
         
         |build-status| |coverage| |license| |wheel| |pyversion| |pyimp|
         
-        :Version: 2.1.4
+        :Version: 2.2.1
         :Web: https://amqp.readthedocs.io/
         :Download: http://pypi.python.org/pypi/amqp/
         :Source: http://github.com/celery/py-amqp/
@@ -144,9 +144,9 @@ Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
 Classifier: License :: OSI Approved :: BSD License
 Classifier: Intended Audience :: Developers
 Classifier: Operating System :: OS Independent
diff --git a/amqp.egg-info/SOURCES.txt b/amqp.egg-info/SOURCES.txt
index 1a6fa4a..33c6b61 100644
--- a/amqp.egg-info/SOURCES.txt
+++ b/amqp.egg-info/SOURCES.txt
@@ -14,6 +14,7 @@ amqp/five.py
 amqp/method_framing.py
 amqp/platform.py
 amqp/protocol.py
+amqp/sasl.py
 amqp/serialization.py
 amqp/spec.py
 amqp/transport.py
@@ -43,6 +44,7 @@ docs/reference/amqp.five.rst
 docs/reference/amqp.method_framing.rst
 docs/reference/amqp.platform.rst
 docs/reference/amqp.protocol.rst
+docs/reference/amqp.sasl.rst
 docs/reference/amqp.serialization.rst
 docs/reference/amqp.spec.rst
 docs/reference/amqp.transport.rst
@@ -64,6 +66,7 @@ t/unit/test_connection.py
 t/unit/test_exceptions.py
 t/unit/test_method_framing.py
 t/unit/test_platform.py
+t/unit/test_sasl.py
 t/unit/test_serialization.py
 t/unit/test_transport.py
 t/unit/test_utils.py
\ No newline at end of file
diff --git a/amqp/__init__.py b/amqp/__init__.py
index 8054450..70bc07c 100644
--- a/amqp/__init__.py
+++ b/amqp/__init__.py
@@ -20,7 +20,7 @@ import re
 
 from collections import namedtuple
 
-__version__ = '2.1.4'
+__version__ = '2.2.1'
 __author__ = 'Barry Pederson'
 __maintainer__ = 'Ask Solem'
 __contact__ = 'pyamqp at celeryproject.org'
diff --git a/amqp/basic_message.py b/amqp/basic_message.py
index cd8f96f..7f80df2 100644
--- a/amqp/basic_message.py
+++ b/amqp/basic_message.py
@@ -32,7 +32,7 @@ __all__ = ['Message']
 
 
 class Message(GenericContent):
-    """A Message for use with the Channnel.basic_* methods.
+    """A Message for use with the Channel.basic_* methods.
 
     Expected arg types
 
diff --git a/amqp/channel.py b/amqp/channel.py
index 289b903..f939e45 100644
--- a/amqp/channel.py
+++ b/amqp/channel.py
@@ -1604,7 +1604,7 @@ class Channel(AbstractChannel):
         try:
             fun = self.callbacks[consumer_tag]
         except KeyError:
-            AMQP_LOGGER.warn(
+            AMQP_LOGGER.warning(
                 REJECTED_MESSAGE_WITHOUT_CALLBACK,
                 delivery_tag, consumer_tag, exchange, routing_key,
             )
diff --git a/amqp/connection.py b/amqp/connection.py
index d9c2abb..2ed89fb 100644
--- a/amqp/connection.py
+++ b/amqp/connection.py
@@ -21,11 +21,10 @@ import socket
 import uuid
 import warnings
 
-from io import BytesIO
-
 from vine import ensure_promise
 
 from . import __version__
+from . import sasl
 from . import spec
 from .abstract_channel import AbstractChannel
 from .channel import Channel
@@ -34,9 +33,8 @@ from .exceptions import (
     ConnectionForced, ConnectionError, error_for_code,
     RecoverableConnectionError, RecoverableChannelError,
 )
-from .five import array, items, monotonic, range, values
+from .five import array, items, monotonic, range, values, string
 from .method_framing import frame_handler, frame_writer
-from .serialization import _write_table
 from .transport import Transport
 
 try:
@@ -100,8 +98,9 @@ class Connection(AbstractChannel):
     (defaults to 'localhost', if a port is not specified then
     5672 is used)
 
-    If login_response is not specified, one is built up for you from
-    userid and password if they are present.
+    Authentication can be controlled by passing one or more
+    `amqp.sasl.SASL` instances as the `authentication` parameter, or
+    by using the userid and password parameters (for AMQPLAIN and PLAIN).
 
     The 'ssl' parameter may be simply True/False, or for Python >= 2.6
     a dictionary of options to pass to ssl.wrap_socket() such as
@@ -188,7 +187,8 @@ class Connection(AbstractChannel):
     )
 
     def __init__(self, host='localhost:5672', userid='guest', password='guest',
-                 login_method='AMQPLAIN', login_response=None,
+                 login_method=None, login_response=None,
+                 authentication=(),
                  virtual_host='/', locale='en_US', client_properties=None,
                  ssl=False, connect_timeout=None, channel_max=None,
                  frame_max=None, heartbeat=0, on_open=None, on_blocked=None,
@@ -199,20 +199,22 @@ class Connection(AbstractChannel):
         self._connection_id = uuid.uuid4().hex
         channel_max = channel_max or 65535
         frame_max = frame_max or 131072
-        if (login_response is None) \
-                and (userid is not None) \
-                and (password is not None):
-            login_response = BytesIO()
-            _write_table({'LOGIN': userid, 'PASSWORD': password},
-                         login_response.write, [])
-            # Skip the length at the beginning
-            login_response = login_response.getvalue()[4:]
+        if authentication:
+            if isinstance(authentication, sasl.SASL):
+                authentication = (authentication,)
+            self.authentication = authentication
+        elif login_method is not None and login_response is not None:
+            self.authentication = (sasl.RAW(login_method, login_response),)
+        elif userid is not None and password is not None:
+            self.authentication = (sasl.GSSAPI(userid, fail_soft=True),
+                                   sasl.AMQPLAIN(userid, password),
+                                   sasl.PLAIN(userid, password))
+        else:
+            raise ValueError("Must supply authentication or userid/password")
 
         self.client_properties = dict(
             self.library_properties, **client_properties or {}
         )
-        self.login_method = login_method
-        self.login_response = login_response
         self.locale = locale
         self.host = host
         self.virtual_host = virtual_host
@@ -342,7 +344,9 @@ class Connection(AbstractChannel):
         self.version_major = version_major
         self.version_minor = version_minor
         self.server_properties = server_properties
-        self.mechanisms = mechanisms.split(' ')
+        if isinstance(mechanisms, string):
+            mechanisms = mechanisms.encode('utf-8')
+        self.mechanisms = mechanisms.split(b' ')
         self.locales = locales.split(' ')
         AMQP_LOGGER.debug(
             START_DEBUG_FMT,
@@ -363,10 +367,24 @@ class Connection(AbstractChannel):
             # this key present in client_properties, so we remove it.
             client_properties.pop('capabilities', None)
 
+        for authentication in self.authentication:
+            if authentication.mechanism in self.mechanisms:
+                login_response = authentication.start(self)
+                if login_response is not NotImplemented:
+                    break
+        else:
+            raise ConnectionError(
+                "Couldn't find appropriate auth mechanism "
+                "(can offer: {0}; available: {1})".format(
+                    b", ".join(m.mechanism
+                               for m in self.authentication
+                               if m.mechanism).decode(),
+                    b", ".join(self.mechanisms).decode()))
+
         self.send_method(
             spec.Connection.StartOk, argsig,
-            (client_properties, self.login_method,
-             self.login_response, self.locale),
+            (client_properties, authentication.mechanism,
+             login_response, self.locale),
         )
 
     def _on_secure(self, challenge):
@@ -418,9 +436,11 @@ class Connection(AbstractChannel):
 
     def collect(self):
         try:
-            self.transport.close()
+            if self._transport:
+                self._transport.close()
 
-            temp_list = [x for x in values(self.channels) if x is not self]
+            temp_list = [x for x in values(self.channels or {})
+                         if x is not self]
             for ch in temp_list:
                 ch.collect()
         except socket.error:
@@ -461,7 +481,9 @@ class Connection(AbstractChannel):
         raise NotImplementedError('Use AMQP heartbeats')
 
     def drain_events(self, timeout=None):
-        return self.blocking_read(timeout)
+        # read until message is ready
+        while not self.blocking_read(timeout):
+            pass
 
     def blocking_read(self, timeout=None):
         with self.transport.having_timeout(timeout):
diff --git a/amqp/method_framing.py b/amqp/method_framing.py
index 1d5d799..5ea46f9 100644
--- a/amqp/method_framing.py
+++ b/amqp/method_framing.py
@@ -64,21 +64,24 @@ def frame_handler(connection, callback,
                     frame_method=method_sig, frame_args=buf,
                 )
                 expected_types[channel] = 2
-            else:
-                callback(channel, method_sig, buf, None)
+                return False
+
+            callback(channel, method_sig, buf, None)
 
         elif frame_type == 2:
             msg = partial_messages[channel]
             msg.inbound_header(buf)
 
-            if msg.ready:
-                # bodyless message, we're done
-                expected_types[channel] = 1
-                partial_messages.pop(channel, None)
-                callback(channel, msg.frame_method, msg.frame_args, msg)
-            else:
+            if not msg.ready:
                 # wait for the content-body
                 expected_types[channel] = 3
+                return False
+
+            # bodyless message, we're done
+            expected_types[channel] = 1
+            partial_messages.pop(channel, None)
+            callback(channel, msg.frame_method, msg.frame_args, msg)
+
         elif frame_type == 3:
             msg = partial_messages[channel]
             msg.inbound_body(buf)
@@ -89,6 +92,7 @@ def frame_handler(connection, callback,
         elif frame_type == 8:
             # bytes_recv already updated
             pass
+        return True
 
     return on_frame
 
diff --git a/amqp/platform.py b/amqp/platform.py
index 2f77c04..5f2b65f 100644
--- a/amqp/platform.py
+++ b/amqp/platform.py
@@ -41,8 +41,14 @@ except ImportError:  # pragma: no cover
     TCP_USER_TIMEOUT = 18
     HAS_TCP_USER_TIMEOUT = LINUX_VERSION and LINUX_VERSION >= (2, 6, 37)
 
+HAS_TCP_MAXSEG = True
+# According to MSDN Windows platforms support getsockopt(TCP_MAXSSEG) but not
+# setsockopt(TCP_MAXSEG) on IPPROTO_TCP sockets.
+if sys.platform.startswith('win'):
+    HAS_TCP_MAXSEG = False
 
-if sys.version_info < (2, 7, 6):
+
+if sys.version_info < (2, 7, 7):
     import functools
 
     def _to_bytes_arg(fun):
@@ -66,6 +72,7 @@ __all__ = [
     'SOL_TCP',
     'TCP_USER_TIMEOUT',
     'HAS_TCP_USER_TIMEOUT',
+    'HAS_TCP_MAXSEG',
     'pack',
     'pack_into',
     'unpack',
diff --git a/amqp/sasl.py b/amqp/sasl.py
new file mode 100644
index 0000000..8283e5f
--- /dev/null
+++ b/amqp/sasl.py
@@ -0,0 +1,159 @@
+"""SASL mechanisms for AMQP authentication."""
+from __future__ import absolute_import, unicode_literals
+
+from io import BytesIO
+import socket
+
+import warnings
+
+from amqp.serialization import _write_table
+
+
+class SASL(object):
+    """The base class for all amqp SASL authentication mechanisms.
+
+    You should sub-class this if you're implementing your own authentication.
+    """
+
+    @property
+    def mechanism(self):
+        """Return a bytes containing the SASL mechanism name."""
+        raise NotImplementedError
+
+    def start(self, connection):
+        """Return the first response to a SASL challenge as a bytes object."""
+        raise NotImplementedError
+
+
+class PLAIN(SASL):
+    """PLAIN SASL authentication mechanism.
+
+    See https://tools.ietf.org/html/rfc4616 for details
+    """
+
+    mechanism = b'PLAIN'
+
+    def __init__(self, username, password):
+        self.username, self.password = username, password
+
+    def start(self, connection):
+        login_response = BytesIO()
+        login_response.write(b'\0')
+        login_response.write(self.username.encode('utf-8'))
+        login_response.write(b'\0')
+        login_response.write(self.password.encode('utf-8'))
+        return login_response.getvalue()
+
+
+class AMQPLAIN(SASL):
+    """AMQPLAIN SASL authentication mechanism.
+
+    This is a non-standard mechanism used by AMQP servers.
+    """
+
+    mechanism = b'AMQPLAIN'
+
+    def __init__(self, username, password):
+        self.username, self.password = username, password
+
+    def start(self, connection):
+        login_response = BytesIO()
+        _write_table({b'LOGIN': self.username, b'PASSWORD': self.password},
+                     login_response.write, [])
+        # Skip the length at the beginning
+        return login_response.getvalue()[4:]
+
+
+def _get_gssapi_mechanism():
+    try:
+        import gssapi
+    except ImportError:
+        class FakeGSSAPI(SASL):
+            """A no-op SASL mechanism for when gssapi isn't available."""
+
+            mechanism = None
+
+            def __init__(self, client_name=None, service=b'amqp',
+                         rdns=False, fail_soft=False):
+                if not fail_soft:
+                    raise NotImplementedError(
+                        "You need to install the `gssapi` module for GSSAPI "
+                        "SASL support")
+
+            def start(self):  # pragma: no cover
+                return NotImplemented
+        return FakeGSSAPI
+    else:
+        import gssapi.raw.misc
+
+        class GSSAPI(SASL):
+            """GSSAPI SASL authentication mechanism.
+
+            See https://tools.ietf.org/html/rfc4752 for details
+            """
+
+            mechanism = b'GSSAPI'
+
+            def __init__(self, client_name=None, service=b'amqp',
+                         rdns=False, fail_soft=False):
+                if client_name and not isinstance(client_name, bytes):
+                    client_name = client_name.encode('ascii')
+                self.client_name = client_name
+                self.fail_soft = fail_soft
+                self.service = service
+                self.rdns = rdns
+
+            def get_hostname(self, connection):
+                sock = connection.transport.sock
+                if self.rdns and sock.family in (socket.AF_INET,
+                                                 socket.AF_INET6):
+                    peer = sock.getpeername()
+                    hostname, _, _ = socket.gethostbyaddr(peer[0])
+                else:
+                    hostname = connection.transport.host
+                if not isinstance(hostname, bytes):
+                    hostname = hostname.encode('ascii')
+                return hostname
+
+            def start(self, connection):
+                try:
+                    if self.client_name:
+                        creds = gssapi.Credentials(
+                            name=gssapi.Name(self.client_name))
+                    else:
+                        creds = None
+                    hostname = self.get_hostname(connection)
+                    name = gssapi.Name(b'@'.join([self.service, hostname]),
+                                       gssapi.NameType.hostbased_service)
+                    context = gssapi.SecurityContext(name=name, creds=creds)
+                    return context.step(None)
+                except gssapi.raw.misc.GSSError:
+                    if self.fail_soft:
+                        return NotImplemented
+                    else:
+                        raise
+        return GSSAPI
+
+
+GSSAPI = _get_gssapi_mechanism()
+
+
+class RAW(SASL):
+    """A generic custom SASL mechanism.
+
+    This mechanism takes a mechanism name and response to send to the server,
+    so can be used for simple custom authentication schemes.
+    """
+
+    mechanism = None
+
+    def __init__(self, mechanism, response):
+        assert isinstance(mechanism, bytes)
+        assert isinstance(response, bytes)
+        self.mechanism, self.response = mechanism, response
+        warnings.warn("Passing login_method and login_response to Connection "
+                      "is deprecated. Please implement a SASL subclass "
+                      "instead.", DeprecationWarning)
+
+    def start(self, connection):
+        return self.response
diff --git a/amqp/transport.py b/amqp/transport.py
index 5787165..981c8b6 100644
--- a/amqp/transport.py
+++ b/amqp/transport.py
@@ -26,7 +26,7 @@ from contextlib import contextmanager
 from .exceptions import UnexpectedFrame
 from .five import items
 from .platform import (
-    SOL_TCP, TCP_USER_TIMEOUT, HAS_TCP_USER_TIMEOUT,
+    SOL_TCP, TCP_USER_TIMEOUT, HAS_TCP_USER_TIMEOUT, HAS_TCP_MAXSEG,
     pack, unpack,
 )
 from .utils import get_errno, set_cloexec
@@ -55,9 +55,13 @@ IPV6_LITERAL = re.compile(r'\[([\.0-9a-f:]+)\](?::(\d+))?')
 KNOWN_TCP_OPTS = (
     'TCP_CORK', 'TCP_DEFER_ACCEPT', 'TCP_KEEPCNT',
     'TCP_KEEPIDLE', 'TCP_KEEPINTVL', 'TCP_LINGER2',
-    'TCP_MAXSEG', 'TCP_NODELAY', 'TCP_QUICKACK',
+    'TCP_NODELAY', 'TCP_QUICKACK',
     'TCP_SYNCNT', 'TCP_WINDOW_CLAMP',
 )
+
+if HAS_TCP_MAXSEG:
+    KNOWN_TCP_OPTS += ('TCP_MAXSEG',)
+
 TCP_OPTS = {
     getattr(socket, opt) for opt in KNOWN_TCP_OPTS if hasattr(socket, opt)
 }
@@ -70,9 +74,8 @@ if HAS_TCP_USER_TIMEOUT:
     TCP_OPTS.add(TCP_USER_TIMEOUT)
     DEFAULT_SOCKET_SETTINGS[TCP_USER_TIMEOUT] = 1000
 
-
 try:
-    from socket import TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT # noqa
+    from socket import TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT  # noqa
 except ImportError:
     pass
 else:
@@ -286,20 +289,55 @@ class SSLTransport(_AbstractTransport):
 
     def _setup_transport(self):
         """Wrap the socket in an SSL object."""
-        self.sock = self._wrap_socket(self.sock, **self.sslopts or {})
+        self.sock = self._wrap_socket(self.sock, **self.sslopts)
         self.sock.do_handshake()
         self._quick_recv = self.sock.read
 
     def _wrap_socket(self, sock, context=None, **sslopts):
         if context:
             return self._wrap_context(sock, sslopts, **context)
-        return ssl.wrap_socket(sock, **sslopts)
+        return self._wrap_socket_sni(sock, **sslopts)
 
     def _wrap_context(self, sock, sslopts, check_hostname=None, **ctx_options):
         ctx = ssl.create_default_context(**ctx_options)
         ctx.check_hostname = check_hostname
         return ctx.wrap_socket(sock, **sslopts)
 
+    def _wrap_socket_sni(self, sock, keyfile=None, certfile=None,
+                         server_side=False, cert_reqs=ssl.CERT_NONE,
+                         ca_certs=None, do_handshake_on_connect=True,
+                         suppress_ragged_eofs=True, server_hostname=None,
+                         ciphers=None, ssl_version=None):
+        """Socket wrap with SNI headers.
+
+        Default `ssl.wrap_socket` method augmented with support for
+        setting the server_hostname field required for SNI hostname header
+        """
+        opts = dict(sock=sock, keyfile=keyfile, certfile=certfile,
+                    server_side=server_side, cert_reqs=cert_reqs,
+                    ca_certs=ca_certs,
+                    do_handshake_on_connect=do_handshake_on_connect,
+                    suppress_ragged_eofs=suppress_ragged_eofs,
+                    ciphers=ciphers)
+        # Setup the right SSL version; default to optimal versions across
+        # ssl implementations
+        if ssl_version is not None:
+            opts['ssl_version'] = ssl_version
+        else:
+            # older versions of python 2.7 and python 2.6 do not have the
+            # ssl.PROTOCOL_TLS defined the equivalent is ssl.PROTOCOL_SSLv23
+            # we default to PROTOCOL_TLS and fallback to PROTOCOL_SSLv23
+            if hasattr(ssl, 'PROTOCOL_TLS'):
+                opts['ssl_version'] = ssl.PROTOCOL_TLS
+            else:
+                opts['ssl_version'] = ssl.PROTOCOL_SSLv23
+        # Set SNI headers if supported
+        if (server_hostname is not None) and (
+                hasattr(ssl, 'HAS_SNI') and ssl.HAS_SNI):
+            opts['server_hostname'] = server_hostname
+        sock = ssl.SSLSocket(**opts)
+        return sock
+
     def _shutdown_transport(self):
         """Unwrap a Python 2.6 SSL socket, so we can call shutdown()."""
         if self.sock is not None:
diff --git a/docs/conf.py b/docs/conf.py
index d1e0178..c550e42 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -7,8 +7,8 @@ globals().update(conf.build_config(
     'amqp', __file__,
     project='py-amqp',
     description='Python Promises',
-    version_dev='2.1',
-    version_stable='2.0',
+    version_dev='2.3',
+    version_stable='2.2',
     canonical_url='https://amqp.readthedocs.io',
     webdomain='celeryproject.org',
     github_project='celery/py-amqp',
diff --git a/docs/includes/introduction.txt b/docs/includes/introduction.txt
index 2404e62..d88fce6 100644
--- a/docs/includes/introduction.txt
+++ b/docs/includes/introduction.txt
@@ -1,4 +1,4 @@
-:Version: 2.1.4
+:Version: 2.2.1
 :Web: https://amqp.readthedocs.io/
 :Download: http://pypi.python.org/pypi/amqp/
 :Source: http://github.com/celery/py-amqp/
diff --git a/docs/reference/amqp.sasl.rst b/docs/reference/amqp.sasl.rst
new file mode 100644
index 0000000..2c062a9
--- /dev/null
+++ b/docs/reference/amqp.sasl.rst
@@ -0,0 +1,11 @@
+=====================================================
+ amqp.spec
+=====================================================
+
+.. contents::
+    :local:
+.. currentmodule:: amqp.sasl
+
+.. automodule:: amqp.sasl
+    :members:
+    :undoc-members:
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index 43e1ac7..a214a84 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -19,6 +19,7 @@
     amqp.method_framing
     amqp.platform
     amqp.protocol
+    amqp.sasl
     amqp.serialization
     amqp.spec
     amqp.utils
diff --git a/requirements/pkgutils.txt b/requirements/pkgutils.txt
index c08306b..e39d1d8 100644
--- a/requirements/pkgutils.txt
+++ b/requirements/pkgutils.txt
@@ -5,4 +5,4 @@ flakeplus>=1.1
 tox>=2.3.1
 sphinx2rst>=1.0
 bumpversion
-pydocstyle
+pydocstyle==1.1.1
diff --git a/setup.cfg b/setup.cfg
index 68a0505..efda315 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -17,5 +17,4 @@ universal = 1
 [egg_info]
 tag_build = 
 tag_date = 0
-tag_svn_revision = 0
 
diff --git a/setup.py b/setup.py
index 03c987e..2899f1a 100644
--- a/setup.py
+++ b/setup.py
@@ -22,9 +22,9 @@ classes = """
     Programming Language :: Python :: 2
     Programming Language :: Python :: 2.7
     Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.3
     Programming Language :: Python :: 3.4
     Programming Language :: Python :: 3.5
+    Programming Language :: Python :: 3.6
     License :: OSI Approved :: BSD License
     Intended Audience :: Developers
     Operating System :: OS Independent
diff --git a/t/unit/test_connection.py b/t/unit/test_connection.py
index b24042e..8dc26b6 100644
--- a/t/unit/test_connection.py
+++ b/t/unit/test_connection.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
 import pytest
 import socket
 
+import warnings
 from case import ContextMock, Mock, call
 
 from amqp import Connection
@@ -10,6 +11,7 @@ from amqp import spec
 from amqp.connection import SSLError
 from amqp.exceptions import ConnectionError, NotFound, ResourceError
 from amqp.five import items
+from amqp.sasl import SASL, AMQPLAIN, PLAIN
 from amqp.transport import TCPTransport
 
 
@@ -22,6 +24,7 @@ class test_Connection:
         self.conn = Connection(
             frame_handler=self.frame_handler,
             frame_writer=self.frame_writer,
+            authentication=AMQPLAIN('foo', 'bar'),
         )
         self.conn.Channel = Mock(name='Channel')
         self.conn.Transport = Mock(name='Transport')
@@ -29,9 +32,27 @@ class test_Connection:
         self.conn.send_method = Mock(name='send_method')
         self.conn.frame_writer = Mock(name='frame_writer')
 
-    def test_login_response(self):
-        self.conn = Connection(login_response='foo')
-        assert self.conn.login_response == 'foo'
+    def test_sasl_authentication(self):
+        authentication = SASL()
+        self.conn = Connection(authentication=authentication)
+        assert self.conn.authentication == (authentication,)
+
+    def test_sasl_authentication_iterable(self):
+        authentication = SASL()
+        self.conn = Connection(authentication=(authentication,))
+        assert self.conn.authentication == (authentication,)
+
+    def test_amqplain(self):
+        self.conn = Connection(userid='foo', password='bar')
+        assert isinstance(self.conn.authentication[1], AMQPLAIN)
+        assert self.conn.authentication[1].username == 'foo'
+        assert self.conn.authentication[1].password == 'bar'
+
+    def test_plain(self):
+        self.conn = Connection(userid='foo', password='bar')
+        assert isinstance(self.conn.authentication[2], PLAIN)
+        assert self.conn.authentication[2].username == 'foo'
+        assert self.conn.authentication[2].password == 'bar'
 
     def test_enter_exit(self):
         self.conn.connect = Mock(name='connect')
@@ -68,23 +89,72 @@ class test_Connection:
         callback.assert_called_with()
 
     def test_on_start(self):
-        self.conn._on_start(3, 4, {'foo': 'bar'}, 'x y z', 'en_US en_GB')
+        self.conn._on_start(3, 4, {'foo': 'bar'}, b'x y z AMQPLAIN PLAIN',
+                            'en_US en_GB')
+        assert self.conn.version_major == 3
+        assert self.conn.version_minor == 4
+        assert self.conn.server_properties == {'foo': 'bar'}
+        assert self.conn.mechanisms == [b'x', b'y', b'z',
+                                        b'AMQPLAIN', b'PLAIN']
+        assert self.conn.locales == ['en_US', 'en_GB']
+        self.conn.send_method.assert_called_with(
+            spec.Connection.StartOk, 'FsSs', (
+                self.conn.client_properties, b'AMQPLAIN',
+                self.conn.authentication[0].start(self.conn), self.conn.locale,
+            ),
+        )
+
+    def test_on_start_string_mechanisms(self):
+        self.conn._on_start(3, 4, {'foo': 'bar'}, 'x y z AMQPLAIN PLAIN',
+                            'en_US en_GB')
         assert self.conn.version_major == 3
         assert self.conn.version_minor == 4
         assert self.conn.server_properties == {'foo': 'bar'}
-        assert self.conn.mechanisms == ['x', 'y', 'z']
+        assert self.conn.mechanisms == [b'x', b'y', b'z',
+                                        b'AMQPLAIN', b'PLAIN']
         assert self.conn.locales == ['en_US', 'en_GB']
         self.conn.send_method.assert_called_with(
             spec.Connection.StartOk, 'FsSs', (
-                self.conn.client_properties, self.conn.login_method,
-                self.conn.login_response, self.conn.locale,
+                self.conn.client_properties, b'AMQPLAIN',
+                self.conn.authentication[0].start(self.conn), self.conn.locale,
+            ),
+        )
+
+    def test_missing_credentials(self):
+        with pytest.raises(ValueError):
+            self.conn = Connection(userid=None, password=None)
+        with pytest.raises(ValueError):
+            self.conn = Connection(password=None)
+
+    def test_mechanism_mismatch(self):
+        with pytest.raises(ConnectionError):
+            self.conn._on_start(3, 4, {'foo': 'bar'}, b'x y z',
+                                'en_US en_GB')
+
+    def test_login_method_response(self):
+        # An old way of doing things.:
+        login_method, login_response = b'foo', b'bar'
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            self.conn = Connection(login_method=login_method,
+                                   login_response=login_response)
+            self.conn.send_method = Mock(name='send_method')
+            self.conn._on_start(3, 4, {'foo': 'bar'}, login_method,
+                                'en_US en_GB')
+            assert len(w) == 1
+            assert issubclass(w[0].category, DeprecationWarning)
+
+        self.conn.send_method.assert_called_with(
+            spec.Connection.StartOk, 'FsSs', (
+                self.conn.client_properties, login_method,
+                login_response, self.conn.locale,
             ),
         )
 
     def test_on_start__consumer_cancel_notify(self):
         self.conn._on_start(
             3, 4, {'capabilities': {'consumer_cancel_notify': 1}},
-            '', '',
+            b'AMQPLAIN', '',
         )
         cap = self.conn.client_properties['capabilities']
         assert cap['consumer_cancel_notify']
@@ -92,7 +162,7 @@ class test_Connection:
     def test_on_start__connection_blocked(self):
         self.conn._on_start(
             3, 4, {'capabilities': {'connection.blocked': 1}},
-            '', '',
+            b'AMQPLAIN', '',
         )
         cap = self.conn.client_properties['capabilities']
         assert cap['connection.blocked']
@@ -100,7 +170,7 @@ class test_Connection:
     def test_on_start__authentication_failure_close(self):
         self.conn._on_start(
             3, 4, {'capabilities': {'authentication_failure_close': 1}},
-            '', '',
+            b'AMQPLAIN', '',
         )
         cap = self.conn.client_properties['capabilities']
         assert cap['authentication_failure_close']
@@ -108,7 +178,7 @@ class test_Connection:
     def test_on_start__authentication_failure_close__disabled(self):
         self.conn._on_start(
             3, 4, {'capabilities': {}},
-            '', '',
+            b'AMQPLAIN', '',
         )
         assert 'capabilities' not in self.conn.client_properties
 
@@ -171,6 +241,18 @@ class test_Connection:
         self.conn.channels[1].collect.side_effect = socket.error()
         self.conn.collect()
 
+    def test_collect_no_transport(self):
+        self.conn = Connection()
+        self.conn.connect = Mock(name='connect')
+        assert not self.conn.connected
+        self.conn.collect()
+        assert not self.conn.connect.called
+
+    def test_collect_again(self):
+        self.conn = Connection()
+        self.conn.collect()
+        self.conn.collect()
+
     def test_get_free_channel_id__raises_IndexError(self):
         self.conn._avail_channel_ids = []
         with pytest.raises(ResourceError):
diff --git a/t/unit/test_platform.py b/t/unit/test_platform.py
index 1d5e43e..96bc3ac 100644
--- a/t/unit/test_platform.py
+++ b/t/unit/test_platform.py
@@ -3,6 +3,11 @@ import pytest
 from amqp.platform import _linux_version_to_tuple
 
... 184 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-amqp.git



More information about the Python-modules-commits mailing list