From: Christian Heller Date: Mon, 4 Aug 2025 19:48:38 +0000 (+0200) Subject: Add very rudimentary SASL PLAIN authentication mechanism. X-Git-Url: https://plomlompom.com/repos/booking/static/%7B%7Bprefix%7D%7D/%7B%7Bdb.prefix%7D%7D/conditions?a=commitdiff_plain;ds=inline;p=ircplom Add very rudimentary SASL PLAIN authentication mechanism. --- diff --git a/ircplom/client.py b/ircplom/client.py index 9c679c6..e9f367a 100644 --- a/ircplom/client.py +++ b/ircplom/client.py @@ -1,6 +1,7 @@ 'High-level IRC protocol / server connection management.' # built-ins from abc import ABC, abstractmethod +from base64 import b64encode from dataclasses import dataclass from getpass import getuser from threading import Thread @@ -90,7 +91,7 @@ class _CapsManager: self.clear() self.challenge('LS', '302') return [] - if self._challenge_met('END'): + if self._challenged('END'): return [f'ignoring post-END CAP message not NEW, DEL: {params}'] match params[0]: case 'LS' | 'LIST': @@ -101,7 +102,6 @@ class _CapsManager: self._dict[cap_name].enabled = params[0] == 'ACK' if self._challenge_met('LIST'): self.challenge('END') - self._challenge_set('END', done=True) return (['server capabilities (enabled: "+"):'] + [cap.str_for_log(cap_name) for cap_name, cap in self._dict.items()]) @@ -121,6 +121,13 @@ class _CapsManager: self._send(IrcMessage(verb='CAP', params=params)) self._challenge_set(challenge_key) + @property + def could_sasl_plain(self) -> bool: + 'Whether opportunity for some SASL AUTHENTICATE PLAIN attempt.' + return (self._challenged('END') + and 'sasl' in self._dict + and 'PLAIN' in self._dict['sasl'].data.split(',')) + def _challenge_met(self, step: str) -> bool: return self._challenges.get(step, False) @@ -158,6 +165,7 @@ class IrcConnSetup: hostname: str nickname: str realname: str + password: str class Client(ABC, ClientQueueMixin): @@ -260,6 +268,19 @@ class Client(ABC, ClientQueueMixin): case 'CAP': for to_log in self._caps.process_msg(msg.params[1:]): self.log.add(to_log) + if self._caps.could_sasl_plain and self.conn_setup.password: + # NB: spec recommends to AUTHENTICATE during, rather than + # after, CAPS negotation; opting for simplicity now instead + self.send(IrcMessage('AUTHENTICATE', ('PLAIN',))) + case 'AUTHENTICATE': + if msg.params == ('+',): + auth = b64encode((self.conn_setup.nickname + '\0' + + self.conn_setup.nickname + '\0' + + self.conn_setup.password + ).encode('utf-8')).decode('utf-8') + self.send(IrcMessage('AUTHENTICATE', (auth,))) + case '904': + self.log.alert('SASL authentication failed') @dataclass diff --git a/ircplom/client_tui.py b/ircplom/client_tui.py index ced6129..ab24508 100644 --- a/ircplom/client_tui.py +++ b/ircplom/client_tui.py @@ -99,14 +99,16 @@ class ClientTui(BaseTui): client_wins[0].prompt.prefix_copy_to(win.prompt) return win - def cmd__connect(self, hostname: str, nickname: str, realname: str + def cmd__connect(self, hostname: str, nickname_and_pw: str, realname: str ) -> None: 'Create Client and pass it via NewClientEvent.' + split = nickname_and_pw.split(':', maxsplit=1) + nickname = split[0] + pw = split[1] if len(split) > 1 else '' self._put(NewClientEvent( _ClientKnowingTui( q_out=self.q_out, - conn_setup=IrcConnSetup(hostname=hostname, nickname=nickname, - realname=realname)))) + conn_setup=IrcConnSetup(hostname, nickname, realname, pw)))) @dataclass