[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