Add SASL EXTERNAL support and SASL mechanism choice.

This commit is contained in:
Shiz 2016-10-20 06:05:21 +02:00
parent 8dbe732dda
commit 0ccb96a9cb
3 changed files with 52 additions and 20 deletions

View File

@ -165,10 +165,12 @@ to the `pydle.Client` constructor:
* ``sasl_username``: The SASL username.
* ``sasl_password``: The SASL password.
* ``sasl_identity``: The identity to use. Default, and most common, is ``''``.
* ``sasl_mechanism``: The SASL mechanism to force. Default involves auto-selection from server-supported mechanism, or a `PLAIN`` fallback.
These arguments are also set as attributes.
Currently, pydle's SASL support requires on the Python `pure-sasl`_ package and is limited to support for the `PLAIN` mechanism.
Currently, pydle's SASL support requires on the Python `pure-sasl`_ package and is thus limited to the mechanisms it supports.
The ``EXTERNAL`` mechanism is also supported without, however.
.. _`SASL`: https://tools.ietf.org/html/rfc4422
.. _`pure-sasl`: https://github.com/thobbs/pure-sasl

View File

@ -24,11 +24,12 @@ class SASLSupport(cap.CapabilityNegotiationSupport):
## Internal overrides.
def __init__(self, *args, sasl_identity='', sasl_username=None, sasl_password=None, **kwargs):
def __init__(self, *args, sasl_identity='', sasl_username=None, sasl_password=None, sasl_mechanism=None, **kwargs):
super().__init__(*args, **kwargs)
self.sasl_identity = sasl_identity
self.sasl_username = sasl_username
self.sasl_password = sasl_password
self.sasl_mechanism = sasl_mechanism
def _reset_attributes(self):
super()._reset_attributes()
@ -41,10 +42,10 @@ class SASLSupport(cap.CapabilityNegotiationSupport):
## SASL functionality.
@async.coroutine
def _sasl_start(self):
def _sasl_start(self, mechanism):
""" Initiate SASL authentication. """
# The rest will be handled in on_raw_authenticate()/_sasl_respond().
yield from self.rawmsg('AUTHENTICATE', self._sasl_client.mechanism.upper())
yield from self.rawmsg('AUTHENTICATE', mechanism)
self._sasl_timer = self.eventloop.schedule_async_in(self.SASL_TIMEOUT, self._sasl_abort(timeout=True))
@async.coroutine
@ -55,6 +56,10 @@ class SASLSupport(cap.CapabilityNegotiationSupport):
else:
self.logger.error('SASL authentication aborted.')
if self._sasl_timer:
self.eventloop.unschedule(self._sasl_timer)
self._sasl_timer = None
# We're done here.
yield from self.rawmsg('AUTHENTICATE', ABORT_MESSAGE)
yield from self._capability_negotiated('sasl')
@ -62,6 +67,9 @@ class SASLSupport(cap.CapabilityNegotiationSupport):
@async.coroutine
def _sasl_end(self):
""" Finalize SASL authentication. """
if self._sasl_timer:
self.eventloop.unschedule(self._sasl_timer)
self._sasl_timer = None
yield from self._capability_negotiated('sasl')
@async.coroutine
@ -100,10 +108,10 @@ class SASLSupport(cap.CapabilityNegotiationSupport):
if value:
self._sasl_mechanisms = value.upper().split(',')
else:
self._sasl_mechanisms = ['PLAIN']
self._sasl_mechanisms = None
if self.sasl_username and self.sasl_password:
if puresasl:
if self.sasl_mechanism == 'EXTERNAL' or (self.sasl_username and self.sasl_password):
if self.sasl_mechanism == 'EXTERNAL' or puresasl:
return True
self.logger.warning('SASL credentials set but puresasl module not found: not initiating SASL authentication.')
return False
@ -111,19 +119,32 @@ class SASLSupport(cap.CapabilityNegotiationSupport):
@async.coroutine
def on_capability_sasl_enabled(self):
""" Start SASL authentication. """
self._sasl_client = puresasl.client.SASLClient(self.connection.hostname, 'irc',
username=self.sasl_username,
password=self.sasl_password,
identity=self.sasl_identity
)
try:
self._sasl_client.choose_mechanism(self._sasl_mechanisms, allow_anonymous=False)
except puresasl.SASLError:
self.logger.exception('SASL mechanism choice failed: aborting SASL authentication.')
return cap.FAILED
if self.sasl_mechanism:
if self._sasl_mechanisms and self.sasl_mechanism not in self._sasl_mechanisms:
self.logger.warning('Requested SASL mechanism is not in server mechanism list: aborting SASL authentication.')
return cap.failed
mechanisms = [self.sasl_mechanism]
else:
mechanisms = self._sasl_mechanisms or ['PLAIN']
if mechanisms == ['EXTERNAL']:
mechanism = 'EXTERNAL'
else:
self._sasl_client = puresasl.client.SASLClient(self.connection.hostname, 'irc',
username=self.sasl_username,
password=self.sasl_password,
identity=self.sasl_identity
)
try:
self._sasl_client.choose_mechanism(mechanisms, allow_anonymous=False)
except puresasl.SASLError:
self.logger.exception('SASL mechanism choice failed: aborting SASL authentication.')
return cap.FAILED
mechanism = self._sasl_client.mechanism.upper()
# Initialize SASL.
yield from self._sasl_start()
yield from self._sasl_start(mechanism)
# Tell caller we need more time, and to not end capability negotiation just yet.
return cap.NEGOTIATING
@ -133,8 +154,16 @@ class SASLSupport(cap.CapabilityNegotiationSupport):
@async.coroutine
def on_raw_authenticate(self, message):
""" Received part of the authentication challenge. """
if self.sasl_mechanism == 'EXTERNAL':
# We don't know what to do here. Call it a day.
self.logger.warning('Received SASL challenge with EXTERNAL mechanism: aborting SASL authentication.')
yield from self._sasl_abort()
return
# Cancel timeout timer.
self.eventloop.unschedule(self._sasl_timer)
if self._sasl_timer:
self.eventloop.unschedule(self._sasl_timer)
self._sasl_timer = None
# Add response data.
response = ' '.join(message.params)

View File

@ -34,6 +34,7 @@ def client_from_args(name, description, default_nick='Bot', cls=pydle.Client):
auth.add_argument('--sasl-identity', help='Identity to use for SASL authentication. (default: <empty>)', default='', metavar='SASLIDENT')
auth.add_argument('--sasl-username', help='Username to use for SASL authentication.', metavar='SASLUSER')
auth.add_argument('--sasl-password', help='Password to use for SASL authentication.', metavar='SASLPASS')
auth.add_argument('--sasl-mechanism', help='Mechanism to use for SASL authentication.', metavar='SASLMECH')
auth.add_argument('--tls-client-cert', help='TLS client certificate to use.', metavar='CERT')
auth.add_argument('--tls-client-cert-keyfile', help='Keyfile to use for TLS client cert.', metavar='KEYFILE')
@ -57,7 +58,7 @@ def client_from_args(name, description, default_nick='Bot', cls=pydle.Client):
# Setup client and connect.
client = cls(nickname=nick, fallback_nicknames=fallback, username=args.username, realname=args.realname,
sasl_identity=args.sasl_identity, sasl_username=args.sasl_username, sasl_password=args.sasl_password,
sasl_identity=args.sasl_identity, sasl_username=args.sasl_username, sasl_password=args.sasl_password, sasl_mechanism=args.sasl_mechanism,
tls_client_cert=args.tls_client_cert, tls_client_cert_key=args.tls_client_cert_keyfile)
connect = functools.partial(client.connect,