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:
Kenneth Giusti
2015-05-12 13:24:34 -04:00
parent 1fde7230a0
commit a6843e822f
9 changed files with 170 additions and 83 deletions

View File

@@ -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()`

View File

@@ -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):

View File

@@ -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))

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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):

View File

@@ -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}