Merge pull request #124 from Dpeta/sasl
Use SASL when auto-identifying to NickServ on connect.
This commit is contained in:
commit
f09c6eeafd
3 changed files with 119 additions and 16 deletions
81
irc.py
81
irc.py
|
@ -95,6 +95,7 @@ class PesterIRC(QtCore.QThread):
|
|||
self.channel_list = []
|
||||
self.channel_field = None
|
||||
|
||||
# Dict for connection server commands/replies to handling functions.
|
||||
self.commands = {
|
||||
"001": self._welcome,
|
||||
"005": self._featurelist,
|
||||
|
@ -115,6 +116,10 @@ class PesterIRC(QtCore.QThread):
|
|||
"768": self._keynotset,
|
||||
"769": self._keynopermission,
|
||||
"770": self._metadatasubok,
|
||||
"902": self._sasl_skill_issue, # ERR_NICKLOCKED, account is not available...
|
||||
"903": self._saslsuccess, # We did a SASL!! woo yeah!! (RPL_SASLSUCCESS)
|
||||
"904": self._sasl_skill_issue, # oh no,,, cringe,,, (ERR_SASLFAIL)
|
||||
"905": self._sasl_skill_issue, # ERR_SASLTOOLONG, we don't split so end.
|
||||
"error": self._error,
|
||||
"join": self._join,
|
||||
"kick": self._kick,
|
||||
|
@ -129,6 +134,7 @@ class PesterIRC(QtCore.QThread):
|
|||
"metadata": self._metadata, # Metadata specification
|
||||
"tagmsg": self._tagmsg, # IRCv3 message tags extension
|
||||
"cap": self._cap, # IRCv3 Client Capability Negotiation
|
||||
"authenticate": self._authenticate, # IRCv3 SASL authentication
|
||||
}
|
||||
|
||||
def run(self):
|
||||
|
@ -190,7 +196,31 @@ class PesterIRC(QtCore.QThread):
|
|||
|
||||
if 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!!
|
||||
self._send_irc.cap("REQ", "sasl")
|
||||
if self.mainwindow.userprofile.getAutoIdentify():
|
||||
# Send plain, send end later when 903 or 904 is received.
|
||||
self._send_irc.authenticate("PLAIN")
|
||||
# Always call CAP END after 5 seconds.
|
||||
self.cap_negotation_started.emit()
|
||||
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")
|
||||
|
||||
def _conn_generator(self):
|
||||
|
@ -289,6 +319,15 @@ class PesterIRC(QtCore.QThread):
|
|||
PchumLog.critical("set_connection_broken() got called, disconnecting.")
|
||||
self.disconnectIRC()
|
||||
|
||||
def end_cap_negotiation(self):
|
||||
"""Send CAP END to end capability negotation.
|
||||
|
||||
Called from SASL-related functions here,
|
||||
but also from a timer on the main thread that always triggers after 5 seconds.
|
||||
"""
|
||||
if not self.registered_irc:
|
||||
self._send_irc.cap("END")
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def update_irc(self):
|
||||
"""Get a silly scrunkler from the generator!!"""
|
||||
|
@ -299,7 +338,7 @@ class PesterIRC(QtCore.QThread):
|
|||
return True
|
||||
raise socket_exception
|
||||
except StopIteration:
|
||||
self._conn = self.conn_generator()
|
||||
self._conn = self._conn_generator()
|
||||
return True
|
||||
else:
|
||||
return res
|
||||
|
@ -515,6 +554,11 @@ class PesterIRC(QtCore.QThread):
|
|||
def send_nick(self, nick: str):
|
||||
self._send_irc.nick(nick)
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def send_authenticate(self, msg):
|
||||
"""Called from main thread via signal, send requirements."""
|
||||
self._send_irc.authenticate(msg)
|
||||
|
||||
def _notice(self, nick, chan, msg):
|
||||
"""Standard IRC 'NOTICE' message, primarily used for automated replies from services."""
|
||||
handle = nick[0 : nick.find("!")]
|
||||
|
@ -827,13 +871,6 @@ class PesterIRC(QtCore.QThread):
|
|||
)
|
||||
self.connected.emit() # Alert main thread that we've connected.
|
||||
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
|
||||
mood = profile.mood.value_str()
|
||||
# Moods via metadata
|
||||
|
@ -867,8 +904,9 @@ class PesterIRC(QtCore.QThread):
|
|||
See: https://ircv3.net/specs/extensions/capability-negotiation
|
||||
"""
|
||||
PchumLog.info("CAP %s %s %s %s", server, nick, subcommand, tag)
|
||||
# if tag == "message-tags":
|
||||
# if subcommand == "ACK":
|
||||
if subcommand.casefold() == "nak" and tag.casefold() == "sasl":
|
||||
# SASL isn't supported, end CAP negotation.
|
||||
self._send_irc.cap("END")
|
||||
|
||||
def _umodeis(self, _server, _handle, modes):
|
||||
"""Numeric reply 221 RPL_UMODEIS, shows us our user modes."""
|
||||
|
@ -1013,6 +1051,26 @@ class PesterIRC(QtCore.QThread):
|
|||
""" "METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key."""
|
||||
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 _sasl_skill_issue(self, *_msg):
|
||||
"""Handles all responses from server that indicate SASL authentication failed.
|
||||
|
||||
Replies that indicate we can't authenticate include: 902, 904, 905.
|
||||
Aborts SASL by sending CAP END, ending capability negotiation."""
|
||||
self.end_cap_negotiation()
|
||||
|
||||
def _saslsuccess(self, *_msg):
|
||||
"""Handle 'RPL_SASLSUCCESS' reply from server, SASL authentication succeeded! woo yeah!!"""
|
||||
self.end_cap_negotiation()
|
||||
|
||||
moodUpdated = QtCore.pyqtSignal(str, Mood)
|
||||
colorUpdated = QtCore.pyqtSignal(str, QtGui.QColor)
|
||||
messageReceived = QtCore.pyqtSignal(str, str)
|
||||
|
@ -1032,3 +1090,4 @@ class PesterIRC(QtCore.QThread):
|
|||
userPresentUpdate = QtCore.pyqtSignal(str, str, str)
|
||||
cannotSendToChan = QtCore.pyqtSignal(str, str)
|
||||
signal_forbiddenchannel = QtCore.pyqtSignal(str, str)
|
||||
cap_negotation_started = QtCore.pyqtSignal()
|
||||
|
|
|
@ -1633,6 +1633,10 @@ class PesterWindow(MovingWindow):
|
|||
|
||||
self.waitingMessages = waitingMessageHolder(self)
|
||||
|
||||
# Create timer for IRC cap negotiation timeout, started in capStarted().
|
||||
self.cap_negotiation_timeout = QtCore.QTimer()
|
||||
self.cap_negotiation_timeout.singleShot = True
|
||||
|
||||
self.idler = {
|
||||
# autoidle
|
||||
"auto": False,
|
||||
|
@ -2545,9 +2549,17 @@ class PesterWindow(MovingWindow):
|
|||
self.waitingMessages.answerMessage()
|
||||
|
||||
def doAutoIdentify(self):
|
||||
"""Identify to NickServ after we've already connected and are switching handle.
|
||||
|
||||
It'd be better to do this with only the AUTHENTICATE command even after connecting,
|
||||
but UnrealIRCd doens't seem to support it yet? https://bugs.unrealircd.org/view.php?id=6084
|
||||
The protocol allows it though, so hopefully it'll be a thing in the future.
|
||||
For now it's better to just msg too for backwards compatibility.
|
||||
"""
|
||||
if self.userprofile.getAutoIdentify():
|
||||
# self.sendAuthenticate.emit("PLAIN")
|
||||
self.sendMessage.emit(
|
||||
"identify " + self.userprofile.getNickServPass(), "NickServ"
|
||||
f"identify {self.userprofile.getNickServPass()}", "NickServ"
|
||||
)
|
||||
|
||||
def doAutoJoins(self):
|
||||
|
@ -2562,7 +2574,6 @@ class PesterWindow(MovingWindow):
|
|||
self.loadingscreen.done(QtWidgets.QDialog.DialogCode.Accepted)
|
||||
self.loadingscreen = None
|
||||
|
||||
self.doAutoIdentify()
|
||||
self.doAutoJoins()
|
||||
|
||||
# Start client --> server pings
|
||||
|
@ -3804,6 +3815,11 @@ class PesterWindow(MovingWindow):
|
|||
self.parent.trayicon.hide()
|
||||
self.app.quit()
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def capNegotationStarted(self):
|
||||
"""IRC thread started capabilities negotiation, end it if it takes longer than 5 seconds."""
|
||||
self.cap_negotiation_timeout.start(5000)
|
||||
|
||||
def updateServerJson(self):
|
||||
PchumLog.info("'%s' chosen.", self.customServerPrompt_qline.text())
|
||||
server_and_port = self.customServerPrompt_qline.text().split(":")
|
||||
|
@ -4244,6 +4260,7 @@ class PesterWindow(MovingWindow):
|
|||
pingServer = QtCore.pyqtSignal()
|
||||
setAway = QtCore.pyqtSignal(bool)
|
||||
killSomeQuirks = QtCore.pyqtSignal(str, str)
|
||||
sendAuthenticate = QtCore.pyqtSignal(str)
|
||||
|
||||
|
||||
class PesterTray(QtWidgets.QSystemTrayIcon):
|
||||
|
@ -4402,8 +4419,9 @@ class MainProgram(QtCore.QObject):
|
|||
self.widget.config.set("traymsg", False)
|
||||
|
||||
def ircQtConnections(self, irc, widget):
|
||||
# IRC --> Main window
|
||||
return (
|
||||
# Connect widget signal to IRC slot/function. (IRC --> Widget)
|
||||
# IRC runs on a different thread.
|
||||
(widget.sendMessage, irc.send_message),
|
||||
(widget.sendNotice, irc.send_notice),
|
||||
(widget.sendCTCP, irc.send_ctcp),
|
||||
|
@ -4428,7 +4446,9 @@ class MainProgram(QtCore.QObject):
|
|||
(widget.killSomeQuirks, irc.kill_some_quirks),
|
||||
(widget.disconnectIRC, irc.disconnect_irc),
|
||||
(widget.changeNick, irc.send_nick),
|
||||
# Main window --> IRC
|
||||
(widget.sendAuthenticate, irc.send_authenticate),
|
||||
(widget.cap_negotiation_timeout.timeout, irc.end_cap_negotiation),
|
||||
# Connect IRC signal to widget slot/function. (IRC --> Widget)
|
||||
(irc.connected, widget.connected),
|
||||
(irc.askToConnect, widget.connectAnyway),
|
||||
(irc.moodUpdated, widget.updateMoodSlot),
|
||||
|
@ -4448,6 +4468,7 @@ class MainProgram(QtCore.QObject):
|
|||
(irc.modesUpdated, widget.modesUpdated),
|
||||
(irc.cannotSendToChan, widget.cannotSendToChan),
|
||||
(irc.signal_forbiddenchannel, widget.forbiddenchannel),
|
||||
(irc.cap_negotation_started, widget.capNegotationStarted),
|
||||
)
|
||||
|
||||
def connectWidgets(self, irc, widget):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""IRC-related functions and classes to be imported by irc.py"""
|
||||
import logging
|
||||
import base64
|
||||
|
||||
PchumLog = logging.getLogger("pchumLogger")
|
||||
|
||||
|
@ -41,7 +42,7 @@ class SendIRC:
|
|||
|
||||
try:
|
||||
PchumLog.debug("Sending: %s", command)
|
||||
self.socket.sendall(outgoing_bytes)
|
||||
self.socket.send(outgoing_bytes)
|
||||
except OSError:
|
||||
PchumLog.exception("Error while sending: '%s'", command.strip())
|
||||
self.socket.close()
|
||||
|
@ -157,6 +158,28 @@ class SendIRC:
|
|||
"""Send QUIT to terminate connection."""
|
||||
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"
|
||||
)
|
||||
# SASL string needs to be under 400 bytes,
|
||||
# splitting over multiple messages is in the protocol but not implemented here.
|
||||
self._send("AUTHENTICATE", text=sasl_string_base64)
|
||||
|
||||
|
||||
def parse_irc_line(line: str):
|
||||
"""Retrieves tags, prefix, command, and arguments from an unparsed IRC line."""
|
||||
|
|
Loading…
Reference in a new issue