Skip to content

Commit

Permalink
Merge pull request #199 from anisse/main
Browse files Browse the repository at this point in the history
Add support for SASL PLAIN authentication
  • Loading branch information
jaraco committed Aug 13, 2023
2 parents a1de8d6 + c1b6cee commit 6bee9b0
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 0 deletions.
70 changes: 70 additions & 0 deletions irc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
.. [IRC specifications] http://www.irchelp.org/irchelp/rfc/
"""

import base64
import bisect
import re
import select
Expand Down Expand Up @@ -144,6 +145,7 @@ def connect(
username=None,
ircname=None,
connect_factory=connection.Factory(),
sasl_login=None,
):
"""Connect/reconnect to a server.
Expand All @@ -158,6 +160,9 @@ def connect(
* server_address - The remote host/port of the server
* connect_factory - A callable that takes the server address and
returns a connection (with a socket interface)
* sasl_login - A string used to toggle sasl plain login.
Password needs to be set as well, and will be used for SASL,
not PASS login.
This function can be called to reconnect a closed connection.
Expand All @@ -182,6 +187,7 @@ def connect(
self.ircname = ircname or nickname
self.password = password
self.connect_factory = connect_factory
self.sasl_login = sasl_login
try:
self.socket = self.connect_factory(self.server_address)
except socket.error as ex:
Expand All @@ -190,12 +196,76 @@ def connect(
self.reactor._on_connect(self.socket)

# Log on...
if self.sasl_login and self.password:
self._sasl_step = None
for i in ["cap", "authenticate", "saslsuccess", "saslfail"]:
self.add_global_handler(i, self._sasl_state_machine, -42)
self.cap("LS")
self.nick(self.nickname)
self.user(self.username, self.ircname)
self._sasl_step = self._sasl_cap_ls
return self
if self.password:
self.pass_(self.password)
self.nick(self.nickname)
self.user(self.username, self.ircname)
return self

def _sasl_state_machine(self, connection, event):
if self._sasl_step:
self._sasl_step(event)

def _sasl_cap_ls(self, event):
if (
event.type == "cap"
and len(event.arguments) > 1
and event.arguments[0] == "LS"
):
if 'sasl' in event.arguments[1].split():
self.cap("REQ", "sasl")
self._sasl_step = self._sasl_cap_req
else:
event = Event(
"login_failed", event.target, ["server does not support sasl"]
)
self._handle_event(event)
self._sasl_end()

def _sasl_cap_req(self, event):
if event.type == "cap" and len(event.arguments) > 1:
if event.arguments[0] == "ACK" and 'sasl' in event.arguments:
self.send_items('AUTHENTICATE', 'PLAIN')
self._sasl_step = self._sasl_auth_plain
elif event.arguments[0] == "NAK":
event = Event(
"login_failed", event.target, ["server refused sasl protocol"]
)
self._handle_event(event)
self._sasl_end()

def _sasl_auth_plain(self, event):
if event.type == "authenticate" and event.target == "+":
auth_string = base64.b64encode(
self.encode("\x00%s\x00%s" % (self.sasl_login, self.password))
).decode()
self.send_items('AUTHENTICATE', auth_string)
self._sasl_step = self._sasl_auth_sent

def _sasl_auth_sent(self, event):
if event.type == "saslsuccess":
self._sasl_end()
elif event.type == "saslfail":
event = Event("login_failed", event.target, event.arguments)
self._handle_event(event)
self._sasl_end()

def _sasl_end(self):
self._sasl_step = None
self.cap("END")
# SASL done, de-register handlers
for i in ["cap", "authenticate", "saslsuccess", "saslfail"]:
self.remove_global_handler(i, self._sasl_state_machine)

def reconnect(self):
"""
Reconnect with the last arguments passed to self.connect()
Expand Down
11 changes: 11 additions & 0 deletions irc/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@
"492": "noservicehost",
"501": "umodeunknownflag",
"502": "usersdontmatch",
# IRCv3.1 SASL https://ircv3.net/specs/extensions/sasl-3.1
"900": "loggedin",
"901": "loggedout",
"902": "nicklocked",
"903": "saslsuccess",
"904": "saslfail",
"905": "sasltoolong",
"906": "saslaborted",
"907": "saslalready",
"908": "saslmechs",
}

codes = dict((v, k) for k, v in numeric.items())
Expand All @@ -177,6 +187,7 @@
"disconnect",
"ctcp",
"ctcpreply",
"login_failed",
]

protocol = [
Expand Down
1 change: 1 addition & 0 deletions newsfragments/195.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for SASL login.

0 comments on commit 6bee9b0

Please sign in to comment.