diff --git a/docs/features/overview.rst b/docs/features/overview.rst index 4cd3c0e..e1183ad 100644 --- a/docs/features/overview.rst +++ b/docs/features/overview.rst @@ -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 diff --git a/pydle/features/ircv3/sasl.py b/pydle/features/ircv3/sasl.py index 02507a1..9bbf1fb 100644 --- a/pydle/features/ircv3/sasl.py +++ b/pydle/features/ircv3/sasl.py @@ -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) diff --git a/pydle/utils/_args.py b/pydle/utils/_args.py index 5772414..0b51242 100644 --- a/pydle/utils/_args.py +++ b/pydle/utils/_args.py @@ -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: )', 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,