Port to new SASL interface in proton 0.10
Proton 0.10 completely changes the interface to the proton.SASL class. This patch adds new connection properties that will compensate for the changes to the SASL configuration interface. However, since pyngus never wrapped proton's SASL class, applications will have to be updated to use the new connection API to take advantage of this abstraction. Bumping version to 2.0.0
This commit is contained in:
@@ -228,11 +228,27 @@ case of TCP). Parameters:
|
||||
heartbeat generation by the peer, if supported.
|
||||
* "x-trace-protocol" - boolean, if True, enable debug dumps of the
|
||||
AMQP wire traffic.
|
||||
* "x-server" - boolean, set this to True to configure the
|
||||
connection as a server side connection. This should be set True
|
||||
if the connection was remotely initiated (e.g. accept on a
|
||||
listening socket). If the connection was locally initiated
|
||||
(e.g. by calling connect()), then this value should be set to
|
||||
False. This setting is used by authentication and encryption to
|
||||
configure the connection's role. The default value is False for
|
||||
client mode.
|
||||
* "x-username" - string, the client's username to use when
|
||||
authenticating with a server.
|
||||
* "x-password" - string, the client's password, used for
|
||||
authentication.
|
||||
* "x-require-auth" - boolean, reject remotely-initiated client
|
||||
connections that fail to provide valid credentials for
|
||||
authentication.
|
||||
* "x-sasl-mechs" - string, a space-separated list of mechanisms
|
||||
that are allowed for authentication. Defaults to "ANONYMOUS"
|
||||
* "x-ssl-ca-file" - string, path to a PEM file containing the
|
||||
certificates of the trusted Certificate Authorities that will be
|
||||
used to check the signature of the peer's certificate.
|
||||
* "x-ssl-server" - boolean, if True, the Connection acts as a SSL
|
||||
server (default False - use Client mode)
|
||||
* "x-ssl-server" - __DEPRECATED__ use x-server instead.
|
||||
* "x-ssl-identity" - tuple, contains self-identifying certificate
|
||||
information which will be presented to the peer. The first item
|
||||
in the tuple is the path to the certificate file (PEM format).
|
||||
@@ -259,9 +275,9 @@ case of TCP). Parameters:
|
||||
necessary. A DNS host name is required to authenticate peer's
|
||||
certificate (see x-ssl-verify-mode).
|
||||
* "x-ssl-allow-cleartext" - boolean, allows clients to connect
|
||||
without using SSL (eg, plain TCP). Used by a server that will
|
||||
accept clients requesting either trusted or untrusted
|
||||
connections.
|
||||
without using SSL (eg, plain TCP). Used by a server that will
|
||||
accept clients requesting either trusted or untrusted
|
||||
connections.
|
||||
|
||||
`Container.name()`
|
||||
|
||||
|
||||
@@ -63,7 +63,11 @@ def main(argv=None):
|
||||
# create AMQP Container, Connection, and SenderLink
|
||||
#
|
||||
container = pyngus.Container(uuid.uuid4().hex)
|
||||
conn_properties = {'hostname': host}
|
||||
conn_properties = {'hostname': host,
|
||||
'x-server': False,
|
||||
'x-username': 'guest',
|
||||
'x-password': 'guest',
|
||||
'x-sasl-mechs': "ANONYMOUS PLAIN"}
|
||||
if opts.trace:
|
||||
conn_properties["x-trace-protocol"] = True
|
||||
if opts.ca:
|
||||
@@ -74,8 +78,6 @@ def main(argv=None):
|
||||
connection = container.create_connection("receiver",
|
||||
None, # no events
|
||||
conn_properties)
|
||||
connection.pn_sasl.mechanisms("ANONYMOUS")
|
||||
connection.pn_sasl.client()
|
||||
connection.open()
|
||||
|
||||
class ReceiveCallback(pyngus.ReceiverEventHandler):
|
||||
|
||||
@@ -68,8 +68,6 @@ class MyConnection(pyngus.ConnectionEventHandler):
|
||||
self,
|
||||
self.properties)
|
||||
self.connection.user_context = self
|
||||
self.connection.sasl.mechanisms("ANONYMOUS")
|
||||
self.connection.sasl.client()
|
||||
self.connection.open()
|
||||
|
||||
def process(self):
|
||||
@@ -153,9 +151,6 @@ class MyConnection(pyngus.ConnectionEventHandler):
|
||||
# reject_sender to reject it.
|
||||
assert False, "Not expected"
|
||||
|
||||
def sasl_step(self, connection, pn_sasl):
|
||||
LOG.debug("sasl_step")
|
||||
|
||||
def sasl_done(self, connection, pn_sasl, result):
|
||||
LOG.debug("SASL done, result=%s", str(result))
|
||||
|
||||
|
||||
@@ -69,8 +69,6 @@ class SocketConnection(pyngus.ConnectionEventHandler):
|
||||
self.connection = container.create_connection(name, self,
|
||||
conn_properties)
|
||||
self.connection.user_context = self
|
||||
self.connection.sasl.mechanisms("ANONYMOUS")
|
||||
self.connection.sasl.server()
|
||||
self.connection.open()
|
||||
self.done = False
|
||||
|
||||
@@ -396,7 +394,7 @@ def main(argv=None):
|
||||
client_socket, client_address = my_socket.accept()
|
||||
name = uuid.uuid4().hex
|
||||
assert name not in socket_connections
|
||||
conn_properties = {}
|
||||
conn_properties = {'x-server': True}
|
||||
if opts.idle_timeout:
|
||||
conn_properties["idle-time-out"] = opts.idle_timeout
|
||||
if opts.trace:
|
||||
|
||||
@@ -68,7 +68,9 @@ def main(argv=None):
|
||||
# create AMQP Container, Connection, and SenderLink
|
||||
#
|
||||
container = pyngus.Container(uuid.uuid4().hex)
|
||||
conn_properties = {'hostname': host}
|
||||
conn_properties = {'hostname': host,
|
||||
'x-server': False,
|
||||
'x-sasl-mechs': "ANONYMOUS PLAIN"}
|
||||
if opts.trace:
|
||||
conn_properties["x-trace-protocol"] = True
|
||||
if opts.ca:
|
||||
@@ -79,8 +81,6 @@ def main(argv=None):
|
||||
connection = container.create_connection("sender",
|
||||
None, # no events
|
||||
conn_properties)
|
||||
connection.pn_sasl.mechanisms("ANONYMOUS")
|
||||
connection.pn_sasl.client()
|
||||
connection.open()
|
||||
|
||||
source_address = opts.source_addr or uuid.uuid4().hex
|
||||
|
||||
@@ -46,8 +46,6 @@ class SocketConnection(pyngus.ConnectionEventHandler):
|
||||
self, # handler
|
||||
properties)
|
||||
self.connection.user_context = self
|
||||
self.connection.pn_sasl.mechanisms("ANONYMOUS")
|
||||
self.connection.pn_sasl.server()
|
||||
self.connection.open()
|
||||
self.closed = False
|
||||
|
||||
@@ -109,8 +107,8 @@ class SocketConnection(pyngus.ConnectionEventHandler):
|
||||
|
||||
def connection_failed(self, connection, error):
|
||||
LOG.error("Connection: failed! error=%s", str(error))
|
||||
# No special recovery - just close it:
|
||||
self.connection.close()
|
||||
# No special recovery - just simulate close completed:
|
||||
self.connection_closed(connection)
|
||||
|
||||
def sender_requested(self, connection, link_handle,
|
||||
name, requested_source, properties):
|
||||
@@ -302,7 +300,9 @@ def main(argv=None):
|
||||
client_socket, client_address = my_socket.accept()
|
||||
# name = uuid.uuid4().hex
|
||||
name = str(client_address)
|
||||
conn_properties = {}
|
||||
conn_properties = {'x-server': True,
|
||||
'x-require-auth': False,
|
||||
'x-sasl-mechs': "ANONYMOUS"}
|
||||
if opts.idle_timeout:
|
||||
conn_properties["idle-time-out"] = opts.idle_timeout
|
||||
if opts.trace:
|
||||
|
||||
@@ -23,4 +23,4 @@ from pyngus.link import SenderLink, SenderEventHandler
|
||||
from pyngus.sockets import read_socket_input
|
||||
from pyngus.sockets import write_socket_output
|
||||
|
||||
VERSION = (1, 4, 0) # major, minor, fix
|
||||
VERSION = (2, 0, 0) # major, minor, fix
|
||||
|
||||
@@ -70,9 +70,9 @@ class ConnectionEventHandler(object):
|
||||
# reject_receiver to reject it.
|
||||
LOG.debug("receiver_requested (ignored)")
|
||||
|
||||
# TODO(kgiusti) cleaner sasl support, esp. server side
|
||||
# No longer supported by proton >= 0.10, so this method is deprecated
|
||||
def sasl_step(self, connection, pn_sasl):
|
||||
"""SASL exchange occurred."""
|
||||
"""DEPRECATED"""
|
||||
LOG.debug("sasl_step (ignored)")
|
||||
|
||||
def sasl_done(self, connection, pn_sasl, result):
|
||||
@@ -84,27 +84,46 @@ class Connection(Endpoint):
|
||||
"""A Connection to a peer."""
|
||||
EOS = -1 # indicates 'I/O stream closed'
|
||||
|
||||
# set of all SASL connection configuration properties
|
||||
_SASL_PROPS = set(['x-username', 'x-password', 'x-require-auth',
|
||||
'x-sasl-mechs'])
|
||||
|
||||
def __init__(self, container, name, event_handler=None, properties=None):
|
||||
"""Create a new connection from the Container
|
||||
|
||||
The following AMQP connection properties are supported:
|
||||
|
||||
hostname: string, target host name sent in Open frame.
|
||||
properties: map, properties of the new connection. The following keys
|
||||
and values are supported:
|
||||
|
||||
idle-time-out: float, time in seconds before an idle link will be
|
||||
closed.
|
||||
|
||||
properties: map, connection properties sent to the peer.
|
||||
hostname: string, the name of the host to which this connection is
|
||||
being made, sent in the Open frame.
|
||||
|
||||
max-frame-size: int, maximum acceptable frame size in bytes.
|
||||
|
||||
properties: map, proton connection properties sent to the peer.
|
||||
|
||||
The following custom connection properties are supported:
|
||||
|
||||
x-trace-protocol: boolean, if true, dump sent and received frames to
|
||||
stdout.
|
||||
x-server: boolean, set this to True to configure the connection as a
|
||||
server side connection. This should be set True if the connection was
|
||||
remotely initiated (e.g. accept on a listening socket). If the
|
||||
connection was locally initiated (e.g. by calling connect()), then this
|
||||
value should be set to False. This setting is used by authentication
|
||||
and encryption to configure the connection's role. The default value
|
||||
is False for client mode.
|
||||
|
||||
x-ssl-server: boolean, if True connection acts as a SSL server (default
|
||||
False - use Client mode)
|
||||
x-username: string, the client's username to use when authenticating
|
||||
with a server.
|
||||
|
||||
x-password: string, the client's password, used for authentication.
|
||||
|
||||
x-require-auth: boolean, reject remotely-initiated client connections
|
||||
that fail to provide valid credentials for authentication.
|
||||
|
||||
x-sasl-mechs" - string, a space-separated list of mechanisms
|
||||
that are allowed for authentication. Defaults to "ANONYMOUS"
|
||||
|
||||
x-ssl-identity: tuple, contains identifying certificate information
|
||||
which will be presented to the peer. The first item in the tuple is
|
||||
@@ -136,31 +155,43 @@ class Connection(Endpoint):
|
||||
x-ssl-allow-cleartext: boolean, Allows clients to connect without using
|
||||
SSL (eg, plain TCP). Used by a server that will accept clients
|
||||
requesting either trusted or untrusted connections.
|
||||
|
||||
x-trace-protocol: boolean, if true, dump sent and received frames to
|
||||
stdout.
|
||||
"""
|
||||
super(Connection, self).__init__(name)
|
||||
self._container = container
|
||||
self._handler = event_handler
|
||||
self._properties = properties or {}
|
||||
old_flag = self._properties.get('x-ssl-server', False)
|
||||
self._server = self._properties.get('x-server', old_flag)
|
||||
|
||||
self._pn_connection = proton.Connection()
|
||||
self._pn_connection.container = container.name
|
||||
self._pn_transport = proton.Transport()
|
||||
if (_PROTON_VERSION < (0, 9)):
|
||||
self._pn_transport = proton.Transport()
|
||||
else:
|
||||
if self._server:
|
||||
mode = proton.Transport.SERVER
|
||||
else:
|
||||
mode = proton.Transport.CLIENT
|
||||
self._pn_transport = proton.Transport(mode)
|
||||
self._pn_transport.bind(self._pn_connection)
|
||||
self._pn_collector = proton.Collector()
|
||||
self._pn_connection.collect(self._pn_collector)
|
||||
|
||||
if properties:
|
||||
if 'hostname' in properties:
|
||||
self._pn_connection.hostname = properties['hostname']
|
||||
secs = properties.get("idle-time-out")
|
||||
if secs:
|
||||
self._pn_transport.idle_timeout = secs
|
||||
max_frame = properties.get("max-frame-size")
|
||||
if max_frame:
|
||||
self._pn_transport.max_frame_size = max_frame
|
||||
if 'properties' in properties:
|
||||
self._pn_connection.properties = properties["properties"]
|
||||
if properties.get("x-trace-protocol"):
|
||||
self._pn_transport.trace(proton.Transport.TRACE_FRM)
|
||||
if 'hostname' in self._properties:
|
||||
self._pn_connection.hostname = self._properties['hostname']
|
||||
secs = self._properties.get("idle-time-out")
|
||||
if secs:
|
||||
self._pn_transport.idle_timeout = secs
|
||||
max_frame = self._properties.get("max-frame-size")
|
||||
if max_frame:
|
||||
self._pn_transport.max_frame_size = max_frame
|
||||
if 'properties' in self._properties:
|
||||
self._pn_connection.properties = self._properties["properties"]
|
||||
if self._properties.get("x-trace-protocol"):
|
||||
self._pn_transport.trace(proton.Transport.TRACE_FRM)
|
||||
|
||||
# indexed by link-name
|
||||
self._sender_links = {} # SenderLink
|
||||
@@ -176,10 +207,42 @@ class Connection(Endpoint):
|
||||
self._user_context = None
|
||||
self._in_process = False
|
||||
self._remote_session_id = 0
|
||||
# TODO(kgiusti) sasl configuration and handling
|
||||
|
||||
self._pn_sasl = None
|
||||
self._sasl_done = False
|
||||
|
||||
if (_PROTON_VERSION < (0, 10)):
|
||||
# best effort map of 0.10 sasl config to pre-0.10 sasl
|
||||
if self._SASL_PROPS.intersection(set(self._properties.keys())):
|
||||
# SASL config specified, need to enable SASL
|
||||
if self._server:
|
||||
self.pn_sasl.server()
|
||||
if 'x-require-auth' in self._properties:
|
||||
if not self._properties['x-require-auth']:
|
||||
if _PROTON_VERSION >= (0, 8):
|
||||
self.pn_sasl.allow_skip()
|
||||
else:
|
||||
if 'x-username' in self._properties:
|
||||
self.pn_sasl.plain(self._properties['x-username'],
|
||||
self._properties.get('x-password',
|
||||
''))
|
||||
else:
|
||||
self.pn_sasl.client()
|
||||
mechs = self._properties.get('x-sasl-mechs')
|
||||
if mechs:
|
||||
self.pn_sasl.mechanisms(mechs)
|
||||
else:
|
||||
# new SASL configuration
|
||||
if 'x-require-auth' in self._properties:
|
||||
ra = self._properties['x-require-auth']
|
||||
self._pn_transport.require_auth(ra)
|
||||
if 'x-username' in self._properties:
|
||||
self._pn_connection.user = self._properties['x-username']
|
||||
if 'x-password' in self._properties:
|
||||
self._pn_connection.password = self._properties['x-password']
|
||||
if 'x-sasl-mechs' in self._properties:
|
||||
self.pn_sasl.allowed_mechs(self._properties['x-sasl-mechs'])
|
||||
|
||||
# intercept any SSL failures and cleanup resources before propagating
|
||||
# the exception:
|
||||
try:
|
||||
@@ -227,19 +290,12 @@ class Connection(Endpoint):
|
||||
return self._pn_connection.remote_properties
|
||||
return None
|
||||
|
||||
# TODO(kgiusti) - think about server side use of sasl!
|
||||
@property
|
||||
def pn_sasl(self):
|
||||
if not self._pn_sasl:
|
||||
self._pn_sasl = self._pn_transport.sasl()
|
||||
return self._pn_sasl
|
||||
|
||||
@property
|
||||
def sasl(self):
|
||||
text = "sasl deprecated, use pn_sasl instead"
|
||||
warnings.warn(DeprecationWarning(text))
|
||||
return self.pn_sasl
|
||||
|
||||
def pn_ssl(self):
|
||||
"""Return the Proton SSL context for this Connection."""
|
||||
return self._pn_ssl
|
||||
@@ -319,21 +375,21 @@ class Connection(Endpoint):
|
||||
if self._pn_connection.state & proton.Endpoint.LOCAL_UNINIT:
|
||||
return 0
|
||||
|
||||
# wait until SASL has authenticated
|
||||
# TODO(kgiusti) Server-side SASL
|
||||
if self._pn_sasl:
|
||||
if self._pn_sasl.state not in (proton.SASL.STATE_PASS,
|
||||
proton.SASL.STATE_FAIL):
|
||||
LOG.debug("SASL in progress. State=%s",
|
||||
str(self._pn_sasl.state))
|
||||
if self._handler:
|
||||
self._handler.sasl_step(self, self._pn_sasl)
|
||||
return self._next_deadline
|
||||
if (_PROTON_VERSION < (0, 10)):
|
||||
# wait until SASL has authenticated
|
||||
if self._pn_sasl and not self._sasl_done:
|
||||
if self._pn_sasl.state not in (proton.SASL.STATE_PASS,
|
||||
proton.SASL.STATE_FAIL):
|
||||
LOG.debug("SASL in progress. State=%s",
|
||||
str(self._pn_sasl.state))
|
||||
if self._handler:
|
||||
self._handler.sasl_step(self, self._pn_sasl)
|
||||
return self._next_deadline
|
||||
|
||||
if self._handler:
|
||||
self._handler.sasl_done(self, self._pn_sasl,
|
||||
self._pn_sasl.outcome)
|
||||
self._pn_sasl = None
|
||||
self._sasl_done = True
|
||||
if self._handler:
|
||||
self._handler.sasl_done(self, self._pn_sasl,
|
||||
self._pn_sasl.outcome)
|
||||
|
||||
# process timer events:
|
||||
timer_deadline = self._expire_timers(now)
|
||||
@@ -651,7 +707,7 @@ class Connection(Endpoint):
|
||||
elif pn_event.type == proton.Event.CONNECTION_INIT:
|
||||
LOG.debug("Connection created: %s", pn_event.context)
|
||||
elif pn_event.type == proton.Event.CONNECTION_FINAL:
|
||||
LOG.error("Connection finalized: %s", pn_event.context)
|
||||
LOG.debug("Connection finalized: %s", pn_event.context)
|
||||
elif pn_event.type == proton.Event.TRANSPORT_ERROR:
|
||||
self._connection_failed(str(self._pn_transport.condition))
|
||||
else:
|
||||
@@ -676,6 +732,12 @@ class Connection(Endpoint):
|
||||
"""Both ends of the Endpoint have become active."""
|
||||
LOG.debug("Connection is up")
|
||||
if self._handler:
|
||||
if (_PROTON_VERSION >= (0, 10)):
|
||||
# simulate the old sasl_done callback
|
||||
if self._pn_sasl and not self._sasl_done:
|
||||
self._sasl_done = True
|
||||
self._handler.sasl_done(self, self._pn_sasl,
|
||||
self._pn_sasl.outcome)
|
||||
self._handler.connection_active(self)
|
||||
|
||||
def _ep_need_close(self):
|
||||
|
||||
@@ -230,34 +230,48 @@ class APITest(common.Test):
|
||||
c2.process(3)
|
||||
|
||||
def test_sasl_callbacks(self):
|
||||
"""Verify access to the connection's SASL state."""
|
||||
"""Verify sasl_done() callback is invoked"""
|
||||
if self.PROTON_VERSION >= (0, 10):
|
||||
server_props = {'x-server': True,
|
||||
'x-sasl-mechs': 'ANONYMOUS'}
|
||||
client_props = {'x-server': False,
|
||||
'x-username': 'user-foo',
|
||||
'x-password': 'pass-word',
|
||||
'x-sasl-mechs': 'ANONYMOUS PLAIN'}
|
||||
|
||||
else:
|
||||
server_props = {'x-server': True,
|
||||
'x-require-auth': True,
|
||||
'x-sasl-mechs': 'PLAIN'}
|
||||
client_props = {'x-server': False,
|
||||
'x-username': 'user-foo',
|
||||
'x-password': 'pass-word',
|
||||
'x-sasl-mechs': 'PLAIN'}
|
||||
|
||||
class SaslCallbackServer(common.ConnCallback):
|
||||
def sasl_step(self, connection, pn_sasl):
|
||||
self.sasl_step_ct += 1
|
||||
creds = pn_sasl.recv()
|
||||
if creds == "\x00user-foo\x00pass-word":
|
||||
pn_sasl.done(pn_sasl.OK)
|
||||
|
||||
c1_events = SaslCallbackServer()
|
||||
c1 = self.container1.create_connection("c1", c1_events)
|
||||
pn_sasl = c1.pn_sasl
|
||||
assert pn_sasl
|
||||
pn_sasl.mechanisms("PLAIN")
|
||||
pn_sasl.server()
|
||||
c1 = self.container1.create_connection("c1", c1_events,
|
||||
properties=server_props)
|
||||
|
||||
class SaslCallbackClient(common.ConnCallback):
|
||||
def sasl_done(self, connection, pn_sasl, result):
|
||||
assert result == pn_sasl.OK
|
||||
self.sasl_done_ct += 1
|
||||
|
||||
c2_events = SaslCallbackClient()
|
||||
c2 = self.container2.create_connection("c2", c2_events)
|
||||
pn_sasl = c2.pn_sasl
|
||||
assert pn_sasl
|
||||
pn_sasl.plain("user-foo", "pass-word")
|
||||
|
||||
c2 = self.container2.create_connection("c2", c2_events,
|
||||
properties=client_props)
|
||||
c1.open()
|
||||
c2.open()
|
||||
common.process_connections(c1, c2)
|
||||
assert c1.active and c2.active
|
||||
assert c2_events.sasl_done_ct == 1, c2_events.sasl_done_ct
|
||||
|
||||
def test_properties_idle_timeout(self):
|
||||
props = {"idle-time-out": 3}
|
||||
|
||||
Reference in New Issue
Block a user