680 lines
22 KiB
Python
680 lines
22 KiB
Python
"""Transport implementation."""
|
||
# Copyright (C) 2009 Barry Pederson <bp@barryp.org>
|
||
|
||
import errno
|
||
import os
|
||
import re
|
||
import socket
|
||
import ssl
|
||
from contextlib import contextmanager
|
||
from ssl import SSLError
|
||
from struct import pack, unpack
|
||
|
||
from .exceptions import UnexpectedFrame
|
||
from .platform import KNOWN_TCP_OPTS, SOL_TCP
|
||
from .utils import set_cloexec
|
||
|
||
_UNAVAIL = {errno.EAGAIN, errno.EINTR, errno.ENOENT, errno.EWOULDBLOCK}
|
||
|
||
AMQP_PORT = 5672
|
||
|
||
EMPTY_BUFFER = bytes()
|
||
|
||
SIGNED_INT_MAX = 0x7FFFFFFF
|
||
|
||
# Yes, Advanced Message Queuing Protocol Protocol is redundant
|
||
AMQP_PROTOCOL_HEADER = b'AMQP\x00\x00\x09\x01'
|
||
|
||
# Match things like: [fe80::1]:5432, from RFC 2732
|
||
IPV6_LITERAL = re.compile(r'\[([\.0-9a-f:]+)\](?::(\d+))?')
|
||
|
||
DEFAULT_SOCKET_SETTINGS = {
|
||
'TCP_NODELAY': 1,
|
||
'TCP_USER_TIMEOUT': 1000,
|
||
'TCP_KEEPIDLE': 60,
|
||
'TCP_KEEPINTVL': 10,
|
||
'TCP_KEEPCNT': 9,
|
||
}
|
||
|
||
|
||
def to_host_port(host, default=AMQP_PORT):
|
||
"""Convert hostname:port string to host, port tuple."""
|
||
port = default
|
||
m = IPV6_LITERAL.match(host)
|
||
if m:
|
||
host = m.group(1)
|
||
if m.group(2):
|
||
port = int(m.group(2))
|
||
else:
|
||
if ':' in host:
|
||
host, port = host.rsplit(':', 1)
|
||
port = int(port)
|
||
return host, port
|
||
|
||
|
||
class _AbstractTransport:
|
||
"""Common superclass for TCP and SSL transports.
|
||
|
||
PARAMETERS:
|
||
host: str
|
||
|
||
Broker address in format ``HOSTNAME:PORT``.
|
||
|
||
connect_timeout: int
|
||
|
||
Timeout of creating new connection.
|
||
|
||
read_timeout: int
|
||
|
||
sets ``SO_RCVTIMEO`` parameter of socket.
|
||
|
||
write_timeout: int
|
||
|
||
sets ``SO_SNDTIMEO`` parameter of socket.
|
||
|
||
socket_settings: dict
|
||
|
||
dictionary containing `optname` and ``optval`` passed to
|
||
``setsockopt(2)``.
|
||
|
||
raise_on_initial_eintr: bool
|
||
|
||
when True, ``socket.timeout`` is raised
|
||
when exception is received during first read. See ``_read()`` for
|
||
details.
|
||
"""
|
||
|
||
def __init__(self, host, connect_timeout=None,
|
||
read_timeout=None, write_timeout=None,
|
||
socket_settings=None, raise_on_initial_eintr=True, **kwargs):
|
||
self.connected = False
|
||
self.sock = None
|
||
self.raise_on_initial_eintr = raise_on_initial_eintr
|
||
self._read_buffer = EMPTY_BUFFER
|
||
self.host, self.port = to_host_port(host)
|
||
self.connect_timeout = connect_timeout
|
||
self.read_timeout = read_timeout
|
||
self.write_timeout = write_timeout
|
||
self.socket_settings = socket_settings
|
||
|
||
__slots__ = (
|
||
"connection",
|
||
"sock",
|
||
"raise_on_initial_eintr",
|
||
"_read_buffer",
|
||
"host",
|
||
"port",
|
||
"connect_timeout",
|
||
"read_timeout",
|
||
"write_timeout",
|
||
"socket_settings",
|
||
# adding '__dict__' to get dynamic assignment
|
||
"__dict__",
|
||
"__weakref__",
|
||
)
|
||
|
||
def __repr__(self):
|
||
if self.sock:
|
||
src = f'{self.sock.getsockname()[0]}:{self.sock.getsockname()[1]}'
|
||
try:
|
||
dst = f'{self.sock.getpeername()[0]}:{self.sock.getpeername()[1]}'
|
||
except (socket.error) as e:
|
||
dst = f'ERROR: {e}'
|
||
return f'<{type(self).__name__}: {src} -> {dst} at {id(self):#x}>'
|
||
else:
|
||
return f'<{type(self).__name__}: (disconnected) at {id(self):#x}>'
|
||
|
||
def connect(self):
|
||
try:
|
||
# are we already connected?
|
||
if self.connected:
|
||
return
|
||
self._connect(self.host, self.port, self.connect_timeout)
|
||
self._init_socket(
|
||
self.socket_settings, self.read_timeout, self.write_timeout,
|
||
)
|
||
# we've sent the banner; signal connect
|
||
# EINTR, EAGAIN, EWOULDBLOCK would signal that the banner
|
||
# has _not_ been sent
|
||
self.connected = True
|
||
except (OSError, SSLError):
|
||
# if not fully connected, close socket, and reraise error
|
||
if self.sock and not self.connected:
|
||
self.sock.close()
|
||
self.sock = None
|
||
raise
|
||
|
||
@contextmanager
|
||
def having_timeout(self, timeout):
|
||
if timeout is None:
|
||
yield self.sock
|
||
else:
|
||
sock = self.sock
|
||
prev = sock.gettimeout()
|
||
if prev != timeout:
|
||
sock.settimeout(timeout)
|
||
try:
|
||
yield self.sock
|
||
except SSLError as exc:
|
||
if 'timed out' in str(exc):
|
||
# http://bugs.python.org/issue10272
|
||
raise socket.timeout()
|
||
elif 'The operation did not complete' in str(exc):
|
||
# Non-blocking SSL sockets can throw SSLError
|
||
raise socket.timeout()
|
||
raise
|
||
except OSError as exc:
|
||
if exc.errno == errno.EWOULDBLOCK:
|
||
raise socket.timeout()
|
||
raise
|
||
finally:
|
||
if timeout != prev:
|
||
sock.settimeout(prev)
|
||
|
||
def _connect(self, host, port, timeout):
|
||
entries = socket.getaddrinfo(
|
||
host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, SOL_TCP,
|
||
)
|
||
for i, res in enumerate(entries):
|
||
af, socktype, proto, canonname, sa = res
|
||
try:
|
||
self.sock = socket.socket(af, socktype, proto)
|
||
try:
|
||
set_cloexec(self.sock, True)
|
||
except NotImplementedError:
|
||
pass
|
||
self.sock.settimeout(timeout)
|
||
self.sock.connect(sa)
|
||
except socket.error:
|
||
if self.sock:
|
||
self.sock.close()
|
||
self.sock = None
|
||
if i + 1 >= len(entries):
|
||
raise
|
||
else:
|
||
break
|
||
|
||
def _init_socket(self, socket_settings, read_timeout, write_timeout):
|
||
self.sock.settimeout(None) # set socket back to blocking mode
|
||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||
self._set_socket_options(socket_settings)
|
||
|
||
# set socket timeouts
|
||
for timeout, interval in ((socket.SO_SNDTIMEO, write_timeout),
|
||
(socket.SO_RCVTIMEO, read_timeout)):
|
||
if interval is not None:
|
||
sec = int(interval)
|
||
usec = int((interval - sec) * 1000000)
|
||
self.sock.setsockopt(
|
||
socket.SOL_SOCKET, timeout,
|
||
pack('ll', sec, usec),
|
||
)
|
||
self._setup_transport()
|
||
|
||
self._write(AMQP_PROTOCOL_HEADER)
|
||
|
||
def _get_tcp_socket_defaults(self, sock):
|
||
tcp_opts = {}
|
||
for opt in KNOWN_TCP_OPTS:
|
||
enum = None
|
||
if opt == 'TCP_USER_TIMEOUT':
|
||
try:
|
||
from socket import TCP_USER_TIMEOUT as enum
|
||
except ImportError:
|
||
# should be in Python 3.6+ on Linux.
|
||
enum = 18
|
||
elif hasattr(socket, opt):
|
||
enum = getattr(socket, opt)
|
||
|
||
if enum:
|
||
if opt in DEFAULT_SOCKET_SETTINGS:
|
||
tcp_opts[enum] = DEFAULT_SOCKET_SETTINGS[opt]
|
||
elif hasattr(socket, opt):
|
||
tcp_opts[enum] = sock.getsockopt(
|
||
SOL_TCP, getattr(socket, opt))
|
||
return tcp_opts
|
||
|
||
def _set_socket_options(self, socket_settings):
|
||
tcp_opts = self._get_tcp_socket_defaults(self.sock)
|
||
if socket_settings:
|
||
tcp_opts.update(socket_settings)
|
||
for opt, val in tcp_opts.items():
|
||
self.sock.setsockopt(SOL_TCP, opt, val)
|
||
|
||
def _read(self, n, initial=False):
|
||
"""Read exactly n bytes from the peer."""
|
||
raise NotImplementedError('Must be overridden in subclass')
|
||
|
||
def _setup_transport(self):
|
||
"""Do any additional initialization of the class."""
|
||
pass
|
||
|
||
def _shutdown_transport(self):
|
||
"""Do any preliminary work in shutting down the connection."""
|
||
pass
|
||
|
||
def _write(self, s):
|
||
"""Completely write a string to the peer."""
|
||
raise NotImplementedError('Must be overridden in subclass')
|
||
|
||
def close(self):
|
||
if self.sock is not None:
|
||
try:
|
||
self._shutdown_transport()
|
||
except OSError:
|
||
pass
|
||
|
||
# Call shutdown first to make sure that pending messages
|
||
# reach the AMQP broker if the program exits after
|
||
# calling this method.
|
||
try:
|
||
self.sock.shutdown(socket.SHUT_RDWR)
|
||
except OSError:
|
||
pass
|
||
|
||
try:
|
||
self.sock.close()
|
||
except OSError:
|
||
pass
|
||
self.sock = None
|
||
self.connected = False
|
||
|
||
def read_frame(self, unpack=unpack):
|
||
"""Parse AMQP frame.
|
||
|
||
Frame has following format::
|
||
|
||
0 1 3 7 size+7 size+8
|
||
+------+---------+---------+ +-------------+ +-----------+
|
||
| type | channel | size | | payload | | frame-end |
|
||
+------+---------+---------+ +-------------+ +-----------+
|
||
octet short long 'size' octets octet
|
||
|
||
"""
|
||
read = self._read
|
||
read_frame_buffer = EMPTY_BUFFER
|
||
try:
|
||
frame_header = read(7, True)
|
||
read_frame_buffer += frame_header
|
||
frame_type, channel, size = unpack('>BHI', frame_header)
|
||
# >I is an unsigned int, but the argument to sock.recv is signed,
|
||
# so we know the size can be at most 2 * SIGNED_INT_MAX
|
||
if size > SIGNED_INT_MAX:
|
||
part1 = read(SIGNED_INT_MAX)
|
||
|
||
try:
|
||
part2 = read(size - SIGNED_INT_MAX)
|
||
except (socket.timeout, OSError, SSLError):
|
||
# In case this read times out, we need to make sure to not
|
||
# lose part1 when we retry the read
|
||
read_frame_buffer += part1
|
||
raise
|
||
|
||
payload = b''.join([part1, part2])
|
||
else:
|
||
payload = read(size)
|
||
read_frame_buffer += payload
|
||
frame_end = ord(read(1))
|
||
except socket.timeout:
|
||
self._read_buffer = read_frame_buffer + self._read_buffer
|
||
raise
|
||
except (OSError, SSLError) as exc:
|
||
if (
|
||
isinstance(exc, socket.error) and os.name == 'nt'
|
||
and exc.errno == errno.EWOULDBLOCK # noqa
|
||
):
|
||
# On windows we can get a read timeout with a winsock error
|
||
# code instead of a proper socket.timeout() error, see
|
||
# https://github.com/celery/py-amqp/issues/320
|
||
self._read_buffer = read_frame_buffer + self._read_buffer
|
||
raise socket.timeout()
|
||
|
||
if isinstance(exc, SSLError) and 'timed out' in str(exc):
|
||
# Don't disconnect for ssl read time outs
|
||
# http://bugs.python.org/issue10272
|
||
self._read_buffer = read_frame_buffer + self._read_buffer
|
||
raise socket.timeout()
|
||
|
||
if exc.errno not in _UNAVAIL:
|
||
self.connected = False
|
||
raise
|
||
# frame-end octet must contain '\xce' value
|
||
if frame_end == 206:
|
||
return frame_type, channel, payload
|
||
else:
|
||
raise UnexpectedFrame(
|
||
f'Received frame_end {frame_end:#04x} while expecting 0xce')
|
||
|
||
def write(self, s):
|
||
try:
|
||
self._write(s)
|
||
except socket.timeout:
|
||
raise
|
||
except OSError as exc:
|
||
if exc.errno not in _UNAVAIL:
|
||
self.connected = False
|
||
raise
|
||
|
||
|
||
class SSLTransport(_AbstractTransport):
|
||
"""Transport that works over SSL.
|
||
|
||
PARAMETERS:
|
||
host: str
|
||
|
||
Broker address in format ``HOSTNAME:PORT``.
|
||
|
||
connect_timeout: int
|
||
|
||
Timeout of creating new connection.
|
||
|
||
ssl: bool|dict
|
||
|
||
parameters of TLS subsystem.
|
||
- when ``ssl`` is not dictionary, defaults of TLS are used
|
||
- otherwise:
|
||
- if ``ssl`` dictionary contains ``context`` key,
|
||
:attr:`~SSLTransport._wrap_context` is used for wrapping
|
||
socket. ``context`` is a dictionary passed to
|
||
:attr:`~SSLTransport._wrap_context` as context parameter.
|
||
All others items from ``ssl`` argument are passed as
|
||
``sslopts``.
|
||
- if ``ssl`` dictionary does not contain ``context`` key,
|
||
:attr:`~SSLTransport._wrap_socket_sni` is used for
|
||
wrapping socket. All items in ``ssl`` argument are
|
||
passed to :attr:`~SSLTransport._wrap_socket_sni` as
|
||
parameters.
|
||
|
||
kwargs:
|
||
|
||
additional arguments of
|
||
:class:`~amqp.transport._AbstractTransport` class
|
||
"""
|
||
|
||
def __init__(self, host, connect_timeout=None, ssl=None, **kwargs):
|
||
self.sslopts = ssl if isinstance(ssl, dict) else {}
|
||
self._read_buffer = EMPTY_BUFFER
|
||
super().__init__(
|
||
host, connect_timeout=connect_timeout, **kwargs)
|
||
|
||
__slots__ = (
|
||
"sslopts",
|
||
)
|
||
|
||
def _setup_transport(self):
|
||
"""Wrap the socket in an SSL object."""
|
||
self.sock = self._wrap_socket(self.sock, **self.sslopts)
|
||
# Explicitly set a timeout here to stop any hangs on handshake.
|
||
self.sock.settimeout(self.connect_timeout)
|
||
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 self._wrap_socket_sni(sock, **sslopts)
|
||
|
||
def _wrap_context(self, sock, sslopts, check_hostname=None, **ctx_options):
|
||
"""Wrap socket without SNI headers.
|
||
|
||
PARAMETERS:
|
||
sock: socket.socket
|
||
|
||
Socket to be wrapped.
|
||
|
||
sslopts: dict
|
||
|
||
Parameters of :attr:`ssl.SSLContext.wrap_socket`.
|
||
|
||
check_hostname
|
||
|
||
Whether to match the peer cert’s hostname. See
|
||
:attr:`ssl.SSLContext.check_hostname` for details.
|
||
|
||
ctx_options
|
||
|
||
Parameters of :attr:`ssl.create_default_context`.
|
||
"""
|
||
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=None,
|
||
ca_certs=None, do_handshake_on_connect=False,
|
||
suppress_ragged_eofs=True, server_hostname=None,
|
||
ciphers=None, ssl_version=None):
|
||
"""Socket wrap with SNI headers.
|
||
|
||
stdlib :attr:`ssl.SSLContext.wrap_socket` method augmented with support
|
||
for setting the server_hostname field required for SNI hostname header.
|
||
|
||
PARAMETERS:
|
||
sock: socket.socket
|
||
|
||
Socket to be wrapped.
|
||
|
||
keyfile: str
|
||
|
||
Path to the private key
|
||
|
||
certfile: str
|
||
|
||
Path to the certificate
|
||
|
||
server_side: bool
|
||
|
||
Identifies whether server-side or client-side
|
||
behavior is desired from this socket. See
|
||
:attr:`~ssl.SSLContext.wrap_socket` for details.
|
||
|
||
cert_reqs: ssl.VerifyMode
|
||
|
||
When set to other than :attr:`ssl.CERT_NONE`, peers certificate
|
||
is checked. Possible values are :attr:`ssl.CERT_NONE`,
|
||
:attr:`ssl.CERT_OPTIONAL` and :attr:`ssl.CERT_REQUIRED`.
|
||
|
||
ca_certs: str
|
||
|
||
Path to “certification authority” (CA) certificates
|
||
used to validate other peers’ certificates when ``cert_reqs``
|
||
is other than :attr:`ssl.CERT_NONE`.
|
||
|
||
do_handshake_on_connect: bool
|
||
|
||
Specifies whether to do the SSL
|
||
handshake automatically. See
|
||
:attr:`~ssl.SSLContext.wrap_socket` for details.
|
||
|
||
suppress_ragged_eofs (bool):
|
||
|
||
See :attr:`~ssl.SSLContext.wrap_socket` for details.
|
||
|
||
server_hostname: str
|
||
|
||
Specifies the hostname of the service which
|
||
we are connecting to. See :attr:`~ssl.SSLContext.wrap_socket`
|
||
for details.
|
||
|
||
ciphers: str
|
||
|
||
Available ciphers for sockets created with this
|
||
context. See :attr:`ssl.SSLContext.set_ciphers`
|
||
|
||
ssl_version:
|
||
|
||
Protocol of the SSL Context. The value is one of
|
||
``ssl.PROTOCOL_*`` constants.
|
||
"""
|
||
opts = {
|
||
'sock': sock,
|
||
'server_side': server_side,
|
||
'do_handshake_on_connect': do_handshake_on_connect,
|
||
'suppress_ragged_eofs': suppress_ragged_eofs,
|
||
'server_hostname': server_hostname,
|
||
}
|
||
|
||
if ssl_version is None:
|
||
ssl_version = (
|
||
ssl.PROTOCOL_TLS_SERVER
|
||
if server_side
|
||
else ssl.PROTOCOL_TLS_CLIENT
|
||
)
|
||
|
||
context = ssl.SSLContext(ssl_version)
|
||
|
||
if certfile is not None:
|
||
context.load_cert_chain(certfile, keyfile)
|
||
if ca_certs is not None:
|
||
context.load_verify_locations(ca_certs)
|
||
if ciphers is not None:
|
||
context.set_ciphers(ciphers)
|
||
# Set SNI headers if supported.
|
||
# Must set context.check_hostname before setting context.verify_mode
|
||
# to avoid setting context.verify_mode=ssl.CERT_NONE while
|
||
# context.check_hostname is still True (the default value in context
|
||
# if client-side) which results in the following exception:
|
||
# ValueError: Cannot set verify_mode to CERT_NONE when check_hostname
|
||
# is enabled.
|
||
try:
|
||
context.check_hostname = (
|
||
ssl.HAS_SNI and server_hostname is not None
|
||
)
|
||
except AttributeError:
|
||
pass # ask forgiveness not permission
|
||
|
||
# See note above re: ordering for context.check_hostname and
|
||
# context.verify_mode assignments.
|
||
if cert_reqs is not None:
|
||
context.verify_mode = cert_reqs
|
||
|
||
if ca_certs is None and context.verify_mode != ssl.CERT_NONE:
|
||
purpose = (
|
||
ssl.Purpose.CLIENT_AUTH
|
||
if server_side
|
||
else ssl.Purpose.SERVER_AUTH
|
||
)
|
||
context.load_default_certs(purpose)
|
||
|
||
sock = context.wrap_socket(**opts)
|
||
return sock
|
||
|
||
def _shutdown_transport(self):
|
||
"""Unwrap a SSL socket, so we can call shutdown()."""
|
||
if self.sock is not None:
|
||
self.sock = self.sock.unwrap()
|
||
|
||
def _read(self, n, initial=False,
|
||
_errnos=(errno.ENOENT, errno.EAGAIN, errno.EINTR)):
|
||
# According to SSL_read(3), it can at most return 16kb of data.
|
||
# Thus, we use an internal read buffer like TCPTransport._read
|
||
# to get the exact number of bytes wanted.
|
||
recv = self._quick_recv
|
||
rbuf = self._read_buffer
|
||
try:
|
||
while len(rbuf) < n:
|
||
try:
|
||
s = recv(n - len(rbuf)) # see note above
|
||
except OSError as exc:
|
||
# ssl.sock.read may cause ENOENT if the
|
||
# operation couldn't be performed (Issue celery#1414).
|
||
if exc.errno in _errnos:
|
||
if initial and self.raise_on_initial_eintr:
|
||
raise socket.timeout()
|
||
continue
|
||
raise
|
||
if not s:
|
||
raise OSError('Server unexpectedly closed connection')
|
||
rbuf += s
|
||
except: # noqa
|
||
self._read_buffer = rbuf
|
||
raise
|
||
result, self._read_buffer = rbuf[:n], rbuf[n:]
|
||
return result
|
||
|
||
def _write(self, s):
|
||
"""Write a string out to the SSL socket fully."""
|
||
write = self.sock.write
|
||
while s:
|
||
try:
|
||
n = write(s)
|
||
except ValueError:
|
||
# AG: sock._sslobj might become null in the meantime if the
|
||
# remote connection has hung up.
|
||
# In python 3.4, a ValueError is raised is self._sslobj is
|
||
# None.
|
||
n = 0
|
||
if not n:
|
||
raise OSError('Socket closed')
|
||
s = s[n:]
|
||
|
||
|
||
class TCPTransport(_AbstractTransport):
|
||
"""Transport that deals directly with TCP socket.
|
||
|
||
All parameters are :class:`~amqp.transport._AbstractTransport` class.
|
||
"""
|
||
|
||
def _setup_transport(self):
|
||
# Setup to _write() directly to the socket, and
|
||
# do our own buffered reads.
|
||
self._write = self.sock.sendall
|
||
self._read_buffer = EMPTY_BUFFER
|
||
self._quick_recv = self.sock.recv
|
||
|
||
def _read(self, n, initial=False, _errnos=(errno.EAGAIN, errno.EINTR)):
|
||
"""Read exactly n bytes from the socket."""
|
||
recv = self._quick_recv
|
||
rbuf = self._read_buffer
|
||
try:
|
||
while len(rbuf) < n:
|
||
try:
|
||
s = recv(n - len(rbuf))
|
||
except OSError as exc:
|
||
if exc.errno in _errnos:
|
||
if initial and self.raise_on_initial_eintr:
|
||
raise socket.timeout()
|
||
continue
|
||
raise
|
||
if not s:
|
||
raise OSError('Server unexpectedly closed connection')
|
||
rbuf += s
|
||
except: # noqa
|
||
self._read_buffer = rbuf
|
||
raise
|
||
|
||
result, self._read_buffer = rbuf[:n], rbuf[n:]
|
||
return result
|
||
|
||
|
||
def Transport(host, connect_timeout=None, ssl=False, **kwargs):
|
||
"""Create transport.
|
||
|
||
Given a few parameters from the Connection constructor,
|
||
select and create a subclass of
|
||
:class:`~amqp.transport._AbstractTransport`.
|
||
|
||
PARAMETERS:
|
||
|
||
host: str
|
||
|
||
Broker address in format ``HOSTNAME:PORT``.
|
||
|
||
connect_timeout: int
|
||
|
||
Timeout of creating new connection.
|
||
|
||
ssl: bool|dict
|
||
|
||
If set, :class:`~amqp.transport.SSLTransport` is used
|
||
and ``ssl`` parameter is passed to it. Otherwise
|
||
:class:`~amqp.transport.TCPTransport` is used.
|
||
|
||
kwargs:
|
||
|
||
additional arguments of :class:`~amqp.transport._AbstractTransport`
|
||
class
|
||
"""
|
||
transport = SSLTransport if ssl else TCPTransport
|
||
return transport(host, connect_timeout=connect_timeout, ssl=ssl, **kwargs)
|