Add basic pre-registration SASL authentication.

Unfinished, currently breaks nickserv auto-identify when switching handles.
This commit is contained in:
Dpeta 2023-02-25 04:46:59 +01:00
parent 1ec2d42e31
commit f8c3dd3b35
No known key found for this signature in database
GPG key ID: 51227517CEA0030C
3 changed files with 73 additions and 15 deletions

55
irc.py
View file

@ -115,6 +115,8 @@ class PesterIRC(QtCore.QThread):
"768": self._keynotset, "768": self._keynotset,
"769": self._keynopermission, "769": self._keynopermission,
"770": self._metadatasubok, "770": self._metadatasubok,
"903": self._saslsuccess, # We did a SASL!! woo yeah!! (RPL_SASLSUCCESS)
"904": self._saslfail, # oh no,,, cringe,,, (ERR_SASLFAIL)
"error": self._error, "error": self._error,
"join": self._join, "join": self._join,
"kick": self._kick, "kick": self._kick,
@ -129,6 +131,7 @@ class PesterIRC(QtCore.QThread):
"metadata": self._metadata, # Metadata specification "metadata": self._metadata, # Metadata specification
"tagmsg": self._tagmsg, # IRCv3 message tags extension "tagmsg": self._tagmsg, # IRCv3 message tags extension
"cap": self._cap, # IRCv3 Client Capability Negotiation "cap": self._cap, # IRCv3 Client Capability Negotiation
"authenticate": self._authenticate, # IRCv3 SASL authentication
} }
def run(self): def run(self):
@ -190,7 +193,28 @@ class PesterIRC(QtCore.QThread):
if self.password: if self.password:
self._send_irc.pass_(self.password) self._send_irc.pass_(self.password)
self._send_irc.nick(self.mainwindow.profile().handle)
# Negotiate capabilities
self._send_irc.cap("REQ", "message-tags")
self._send_irc.cap(
"REQ",
"draft/metadata-notify-2", # <--- Not required for the unreal5 module.
)
self._send_irc.cap("REQ", "pesterchum-tag") # <--- Currently not using this
self._send_irc.cap("REQ", "twitch.tv/membership") # Twitch silly
# This should not be here.
profile = self.mainwindow.profile()
# Do SASL!!
if self.mainwindow.userprofile.getAutoIdentify():
self._send_irc.cap("REQ", "sasl")
self._send_irc.authenticate("PLAIN")
else:
# Without SASL, end caps here.
self._send_irc.cap("END")
# Send NICK & USER :3
self._send_irc.nick(profile.handle)
self._send_irc.user("pcc31", "pcc31") self._send_irc.user("pcc31", "pcc31")
def _conn_generator(self): def _conn_generator(self):
@ -827,13 +851,6 @@ class PesterIRC(QtCore.QThread):
) )
self.connected.emit() # Alert main thread that we've connected. self.connected.emit() # Alert main thread that we've connected.
profile = self.mainwindow.profile() profile = self.mainwindow.profile()
# Negotiate capabilities
self._send_irc.cap("REQ", "message-tags")
self._send_irc.cap(
"REQ", "draft/metadata-notify-2"
) # <--- Not required in the unreal5 module implementation
self._send_irc.cap("REQ", "pesterchum-tag") # <--- Currently not using this
self._send_irc.cap("REQ", "twitch.tv/membership") # Twitch silly
# Get mood # Get mood
mood = profile.mood.value_str() mood = profile.mood.value_str()
# Moods via metadata # Moods via metadata
@ -867,8 +884,9 @@ class PesterIRC(QtCore.QThread):
See: https://ircv3.net/specs/extensions/capability-negotiation See: https://ircv3.net/specs/extensions/capability-negotiation
""" """
PchumLog.info("CAP %s %s %s %s", server, nick, subcommand, tag) PchumLog.info("CAP %s %s %s %s", server, nick, subcommand, tag)
# if tag == "message-tags": if subcommand.casefold() == "nak" and tag.casefold() == "sasl":
# if subcommand == "ACK": # SASL isn't supported, end CAP negotation.
self._send_irc.cap("END")
def _umodeis(self, _server, _handle, modes): def _umodeis(self, _server, _handle, modes):
"""Numeric reply 221 RPL_UMODEIS, shows us our user modes.""" """Numeric reply 221 RPL_UMODEIS, shows us our user modes."""
@ -1013,6 +1031,23 @@ class PesterIRC(QtCore.QThread):
""" "METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key.""" """ "METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key."""
PchumLog.info("_metadatasubok: %s", params) PchumLog.info("_metadatasubok: %s", params)
def _authenticate(self, _, token):
"""Handle IRCv3 SASL authneticate command from server."""
if token == "+":
# Try to send password now
self._send_irc.authenticate(
nick=self.mainwindow.profile().handle,
password=self.mainwindow.userprofile.getNickServPass(),
)
def _saslfail(self, *_msg):
"""Handle 'RPL_SASLSUCCESS' reply from server, SASL authentication succeeded! woo yeah!!"""
self._send_irc.cap("END")
def _saslsuccess(self, *_msg):
"""Handle 'ERR_SASLFAIL' reply from server, SASL failed somehow."""
self._send_irc.cap("END")
moodUpdated = QtCore.pyqtSignal(str, Mood) moodUpdated = QtCore.pyqtSignal(str, Mood)
colorUpdated = QtCore.pyqtSignal(str, QtGui.QColor) colorUpdated = QtCore.pyqtSignal(str, QtGui.QColor)
messageReceived = QtCore.pyqtSignal(str, str) messageReceived = QtCore.pyqtSignal(str, str)

View file

@ -2545,10 +2545,11 @@ class PesterWindow(MovingWindow):
self.waitingMessages.answerMessage() self.waitingMessages.answerMessage()
def doAutoIdentify(self): def doAutoIdentify(self):
if self.userprofile.getAutoIdentify(): pass
self.sendMessage.emit( # if self.userprofile.getAutoIdentify():
"identify " + self.userprofile.getNickServPass(), "NickServ" # self.sendMessage.emit(
) # "identify " + self.userprofile.getNickServPass(), "NickServ"
# )
def doAutoJoins(self): def doAutoJoins(self):
if not self.autoJoinDone: if not self.autoJoinDone:

View file

@ -1,5 +1,6 @@
"""IRC-related functions and classes to be imported by irc.py""" """IRC-related functions and classes to be imported by irc.py"""
import logging import logging
import base64
PchumLog = logging.getLogger("pchumLogger") PchumLog = logging.getLogger("pchumLogger")
@ -41,7 +42,7 @@ class SendIRC:
try: try:
PchumLog.debug("Sending: %s", command) PchumLog.debug("Sending: %s", command)
self.socket.sendall(outgoing_bytes) self.socket.send(outgoing_bytes)
except OSError: except OSError:
PchumLog.exception("Error while sending: '%s'", command.strip()) PchumLog.exception("Error while sending: '%s'", command.strip())
self.socket.close() self.socket.close()
@ -157,6 +158,27 @@ class SendIRC:
"""Send QUIT to terminate connection.""" """Send QUIT to terminate connection."""
self._send("QUIT", text=reason) self._send("QUIT", text=reason)
def authenticate(self, token=None, nick=None, password=None):
"""Authenticate command for SASL.
Send either a token like 'PLAIN' or authenticate with nick and password.
Reference: https://ircv3.net/irc/#account-authentication-and-registration
"""
if token:
self._send("AUTHENTICATE", text=token)
return
if nick and password:
# Authentication identity 'nick', authorization identity 'nick' and password 'password'.
sasl_string = f"{nick}\x00{nick}\x00{password}"
# Encode to use base64, then decode since 'text' only takes str.
sasl_string_bytes = sasl_string.encode(encoding="utf-8", errors="replace")
sasl_string_base64 = base64.b64encode(sasl_string_bytes).decode(
encoding="utf-8"
)
# Woo yeah woo yeah
self._send("AUTHENTICATE", text=sasl_string_base64)
def parse_irc_line(line: str): def parse_irc_line(line: str):
"""Retrieves tags, prefix, command, and arguments from an unparsed IRC line.""" """Retrieves tags, prefix, command, and arguments from an unparsed IRC line."""