Add IRCv3→SASL support for nickserv auth, added optional module whitelist, added server option for recording channel keys, increased flexibility for event raising in linehandler, probably other things too

This commit is contained in:
Evelyn 2017-07-12 10:00:27 +01:00
parent 81edacfba6
commit e232ad5dae
5 changed files with 140 additions and 42 deletions

View file

@ -8,32 +8,43 @@ RE_CHANTYPES = re.compile(r"\bCHANTYPES=(\W+)(?:\b|$)")
handlers = {} handlers = {}
descriptions = {} descriptions = {}
default_events = {}
current_description = None current_description = None
current_default_event = False
handle_lock = threading.Lock() handle_lock = threading.Lock()
line, line_split, bot, server = None, None, None, None line, line_split, bot, server = None, None, None, None
def handler(f=None, description=None): def handler(f=None, description=None, default_event=False):
global current_description global current_description, current_default_event
if description: if not f:
current_description = description current_description = description
current_default_event = default_event
return handler return handler
name = f.__name__.split("handle_")[1].upper() name = f.__name__.split("handle_")[1].upper()
handlers[name] = f handlers[name] = f
if current_description:
descriptions[name] = current_description descriptions[name] = current_description
current_description = None default_events[name] = current_default_event
current_description, current_default_event = None, False
def handle(_line, _line_split, _bot, _server): def handle(_line, _line_split, _bot, _server):
global line, line_split, bot, server global line, line_split, bot, server
handler_function = None handler_function = None
if len(_line_split) > 1: if len(_line_split) > 1:
name = _line_split[1]
if _line_split[0][0] == ":": if _line_split[0][0] == ":":
if _line_split[1] in handlers: if name in handlers:
handler_function = handlers[_line_split[1]] handler_function = handlers[name]
elif _line_split[1].isdigit(): if default_events.get(name, False) or not name in handlers:
if name.isdigit():
_bot.events.on("received").on("numeric").on( _bot.events.on("received").on("numeric").on(
_line_split[1]).call(line=_line, name).call(line=_line,
line_split=_line_split, server=_server, line_split=_line_split, server=_server,
number=_line_split[1]) number=name)
else:
_bot.events.on("received").on(name).call(
line=_line, line_split=_line_split, server=_server,
command=name)
elif _line_split[0] in handlers: elif _line_split[0] in handlers:
handler_function = handlers[_line_split[0]] handler_function = handlers[_line_split[0]]
if handler_function: if handler_function:
@ -48,12 +59,10 @@ def handle_PING():
server.send_pong(Utils.remove_colon(line_split[1])) server.send_pong(Utils.remove_colon(line_split[1]))
bot.events.on("received").on("ping").call(line=line, bot.events.on("received").on("ping").call(line=line,
line_split=line_split, server=server, nonce=nonce) line_split=line_split, server=server, nonce=nonce)
@handler(description="the first line sent to a registered client") @handler(description="the first line sent to a registered client", default_event=True)
def handle_001(): def handle_001():
server.set_own_nickname(line_split[2]) server.set_own_nickname(line_split[2])
server.send_whois(server.nickname) server.send_whois(server.nickname)
bot.events.on("received").on("numeric").on("001").call(
line=line, line_split=line_split, server=server)
@handler(description="the extra supported things line") @handler(description="the extra supported things line")
def handle_005(): def handle_005():
isupport_line = Utils.arbitrary(line_split, 3) isupport_line = Utils.arbitrary(line_split, 3)
@ -74,8 +83,8 @@ def handle_005():
server.channel_types = list(match.group(1)) server.channel_types = list(match.group(1))
bot.events.on("received").on("numeric").on("005").call( bot.events.on("received").on("numeric").on("005").call(
line=line, line_split=line_split, server=server, line=line, line_split=line_split, server=server,
isupport=isupport_line) isupport=isupport_line, number="005")
@handler(description="whois respose (nickname, username, realname, hostname)") @handler(description="whois respose (nickname, username, realname, hostname)", default_event=True)
def handle_311(): def handle_311():
nickname = line_split[3] nickname = line_split[3]
if server.is_own_nickname(nickname): if server.is_own_nickname(nickname):
@ -86,12 +95,12 @@ def handle_311():
hostname = line_split[5] hostname = line_split[5]
target.username = username target.username = username
target.hostname = hostname target.hostname = hostname
@handler(description="on-join channel topic line") @handler(description="on-join channel topic line", default_event=True)
def handle_332(): def handle_332():
channel = server.get_channel(line_split[3]) channel = server.get_channel(line_split[3])
topic = Utils.arbitrary(line_split, 4) topic = Utils.arbitrary(line_split, 4)
channel.set_topic(topic) channel.set_topic(topic)
@handler(description="on-join channel topic set by/at") @handler(description="on-join channel topic set by/at", default_event=True)
def handle_333(): def handle_333():
channel = server.get_channel(line_split[3]) channel = server.get_channel(line_split[3])
topic_setter_hostmask = line_split[4] topic_setter_hostmask = line_split[4]
@ -101,7 +110,7 @@ def handle_333():
) else None ) else None
channel.set_topic_setter(nickname, username, hostname) channel.set_topic_setter(nickname, username, hostname)
channel.set_topic_time(topic_time) channel.set_topic_time(topic_time)
@handler(description="on-join user list with status symbols") @handler(description="on-join user list with status symbols", default_event=True)
def handle_353(): def handle_353():
channel = server.get_channel(line_split[4]) channel = server.get_channel(line_split[4])
nicknames = line_split[5:] nicknames = line_split[5:]
@ -157,7 +166,7 @@ def handle_PART():
bot.events.on("self").on("part").call(line=line, bot.events.on("self").on("part").call(line=line,
line_split=line_split, server=server, channel=channel, line_split=line_split, server=server, channel=channel,
reason=reason) reason=reason)
@handler(description="unknown command sent by us, oops!") @handler(description="unknown command sent by us, oops!", default_event=True)
def handle_421(): def handle_421():
print("warning: unknown command '%s'." % line_split[3]) print("warning: unknown command '%s'." % line_split[3])
@handler(description="a user has disconnected!") @handler(description="a user has disconnected!")
@ -172,6 +181,31 @@ def handle_QUIT():
user=user) user=user)
else: else:
server.disconnect() server.disconnect()
@handler(description="The server is telling us about its capabilities!")
def handle_CAP():
_line = line
_line_split = line_split
if line.startswith(":"):
_line = " ".join(line_split[1:])
_line_split = _line.split()
capability_list = Utils.arbitrary(_line_split, 3).split()
bot.events.on("received").on("cap").call(line=line,
line_split=line_split, server=server,
nickname=_line_split[1], subcommand=_line_split[2],
capabilities=capability_list)
@handler(description="The server is asking for authentication")
def handle_AUTHENTICATE():
_line_split = line_split
if line.startswith(":"):
_line_split = line_split[1:]
bot.events.on("received").on("authenticate").call(line=line,
line_split=line_split, server=server,
message=Utils.arbitrary(_line_split, 1)
)
@handler(description="someone has changed their nickname") @handler(description="someone has changed their nickname")
def handle_NICK(): def handle_NICK():
nickname, username, hostname = Utils.seperate_hostmask(line_split[0]) nickname, username, hostname = Utils.seperate_hostmask(line_split[0])
@ -269,12 +303,12 @@ def handle_PRIVMSG():
user=user, message=message, message_split=message_split, user=user, message=message, message_split=message_split,
action=action) action=action)
user.log.add_line(user.nickname, message, action) user.log.add_line(user.nickname, message, action)
@handler(description="response to a WHO command for user information") @handler(description="response to a WHO command for user information", default_event=True)
def handle_352(): def handle_352():
user = server.get_user(line_split[7]) user = server.get_user(line_split[7])
user.username = line_split[4] user.username = line_split[4]
user.hostname = line_split[5] user.hostname = line_split[5]
@handler(description="response to an empty mode command") @handler(description="response to an empty mode command", default_event=True)
def handle_324(): def handle_324():
channel = server.get_channel(line_split[3]) channel = server.get_channel(line_split[3])
modes = line_split[4] modes = line_split[4]
@ -282,14 +316,14 @@ def handle_324():
for mode in modes[1:]: for mode in modes[1:]:
if mode in server.channel_modes: if mode in server.channel_modes:
channel.add_mode(mode) channel.add_mode(mode)
@handler(description="channel creation unix timestamp") @handler(description="channel creation unix timestamp", default_event=True)
def handle_329(): def handle_329():
channel = server.get_channel(line_split[3]) channel = server.get_channel(line_split[3])
channel.creation_timestamp = int(line_split[4]) channel.creation_timestamp = int(line_split[4])
@handler(description="nickname already in use") @handler(description="nickname already in use", default_event=True)
def handle_433(): def handle_433():
pass pass
@handler(description="we need a registered nickname for this channel") @handler(description="we need a registered nickname for this channel", default_event=True)
def handle_477(): def handle_477():
bot.add_timer("rejoin", 5, channel_name=line_split[3], bot.add_timer("rejoin", 5, channel_name=line_split[3],
key=server.attempted_join[line_split[3].lower()], key=server.attempted_join[line_split[3].lower()],

View file

@ -52,8 +52,14 @@ class Server(object):
return self.cached_fileno if fileno == -1 else fileno return self.cached_fileno if fileno == -1 else fileno
def connect(self): def connect(self):
self.socket.connect((self.target_hostname, self.port)) self.socket.connect((self.target_hostname, self.port))
if self.password: if self.password:
self.send_pass(self.password) self.send_pass(self.password)
# In principle, this belongs in the NS module. In reality, it's more practical to put this
# One-off case here for SASL
if "Nickserv" in self.bot.modules.modules and self.get_setting("nickserv-password"):
self.send_capability_request("sasl")
self.send_user(self.original_username, self.original_realname) self.send_user(self.original_username, self.original_realname)
self.send_nick(self.original_nickname) self.send_nick(self.original_nickname)
self.connected = True self.connected = True
@ -172,6 +178,14 @@ class Server(object):
self.send("USER %s - - :%s" % (username, realname)) self.send("USER %s - - :%s" % (username, realname))
def send_nick(self, nickname): def send_nick(self, nickname):
self.send("NICK %s" % nickname) self.send("NICK %s" % nickname)
def send_capability_request(self, capname):
self.send("CAP REQ :%s" % capname)
def send_capability_end(self):
self.send("CAP END")
def send_authenticate(self, text):
self.send("AUTHENTICATE %s" % text)
def send_pass(self, password): def send_pass(self, password):
self.send("PASS %s" % password) self.send("PASS %s" % password)
def send_ping(self, nonce="hello"): def send_ping(self, nonce="hello"):

View file

@ -14,6 +14,10 @@ class ModuleManager(object):
def _load_module(self, filename): def _load_module(self, filename):
name = self.module_name(filename) name = self.module_name(filename)
whitelist = self.bot.config.get("module_whitelist", [])
if whitelist and name not in whitelist: return
with open(filename) as module_file: with open(filename) as module_file:
while True: while True:
line = module_file.readline().strip() line = module_file.readline().strip()
@ -61,6 +65,7 @@ class ModuleManager(object):
if name in self.waiting_requirement: if name in self.waiting_requirement:
for filename in self.waiting_requirement: for filename in self.waiting_requirement:
self.load_module(filename) self.load_module(filename)
sys.stderr.write("module '%s' loaded.\n" % filename)
else: else:
sys.stderr.write("module '%s' not loaded.\n" % filename) sys.stderr.write("module '%s' not loaded.\n" % filename)
def load_modules(self): def load_modules(self):

View file

@ -2,17 +2,28 @@
class Module(object): class Module(object):
def __init__(self, bot): def __init__(self, bot):
bot.events.on("self").on("part").hook(self.on_self_part)
bot.events.on("self").on("join").hook(self.on_join) bot.events.on("self").on("join").hook(self.on_join)
bot.events.on("received").on("numeric").on("366").hook( bot.events.on("received").on("numeric").on("366").hook(
self.on_connect) self.on_identify_trigger)
bot.events.on("received").on("numeric").on("001").hook(
self.on_identify_trigger)
def on_self_part(self, event):
pass
def on_join(self, event): def on_join(self, event):
channels = set(event["server"].get_setting("autojoin", [])) channels = set(event["server"].get_setting("autojoin", []))
channels.add(event["channel"].name) channels.add(event["channel"].name)
event["server"].set_setting("autojoin", list(channels)) event["server"].set_setting("autojoin", list(channels))
def on_connect(self, event): def on_identify_trigger(self, event):
if event["line_split"][3].lower() == "#bitbot": if event["number"]=="001" and not event["server"].sasl_success: return
if event["line_split"][3].lower() == "#bitbot" or event["number"]=="001":
channels = event["server"].get_setting("autojoin", []) channels = event["server"].get_setting("autojoin", [])
chan_keys = event["server"].get_setting("channel_keys", {})
for channel in channels: for channel in channels:
if channel in chan_keys:
event["server"].send_join(channel, key=chan_keys[channel])
else:
event["server"].send_join(channel) event["server"].send_join(channel)

View file

@ -1,18 +1,28 @@
import base64
class Module(object): class Module(object):
def __init__(self, bot): def __init__(self, bot):
bot.events.on("new").on("server").hook(self.on_new_server)
bot.events.on("received").on("numeric").on("001" bot.events.on("received").on("numeric").on("001"
).hook(self.on_connect) ).hook(self.on_connect)
bot.events.on("received").on("command").on("setnickserv" bot.events.on("received").on("command").on("setnickserv"
).hook(self.set_nickserv, min_args=1, permission="setnickserv", ).hook(self.set_nickserv, min_args=1, permission="setnickserv",
help="Set bot's nickserv password", usage="<password>", help="Set bot's nickserv password", usage="<password>",
private_only=True) private_only=True)
bot.events.on("received").on("cap").hook(self.on_cap)
bot.events.on("received").on("authenticate").hook(self.on_authenticate)
for code in ["902", "903", "904", "905", "906", "907", "908"]:
bot.events.on("received").on("numeric").on(code).hook(self.on_90x)
def on_new_server(self, event):
event["server"].attempted_auth = False
event["server"].sasl_success = False
def on_connect(self, event): def on_connect(self, event):
nickserv_password = event["server"].get_setting( nickserv_password = event["server"].get_setting(
"nickserv-password") "nickserv-password")
if nickserv_password: if nickserv_password and not event["server"].sasl_success:
event["server"].attempted_auth = True
event["server"].send_message("nickserv", event["server"].send_message("nickserv",
"identify %s" % nickserv_password) "identify %s" % nickserv_password)
@ -20,3 +30,27 @@ class Module(object):
nickserv_password = event["args"] nickserv_password = event["args"]
event["server"].set_setting("nickserv-password", nickserv_password) event["server"].set_setting("nickserv-password", nickserv_password)
event["stdout"].write("Nickserv password saved") event["stdout"].write("Nickserv password saved")
def on_cap(self, event):
if event["subcommand"] == "NAK":
event["server"].send_capability_end()
elif event["subcommand"] == "ACK":
event["server"].send_authenticate("PLAIN")
else:
pass
def on_authenticate(self, event):
if event["message"] != "+":
event["server"].send_authenticate("*")
else:
nick = event["server"].original_nickname
password = event["server"].get_setting("nickserv-password")
event["server"].attempted_auth = True
event["server"].send_authenticate(
base64.b64encode(("%s\0%s\0%s" % (nick, nick, password)).encode("utf8")).decode("utf8")
)
def on_90x(self, event):
if event["number"]=="903":
event["server"].sasl_success = True
event["server"].send_capability_end()