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 = {}
descriptions = {}
default_events = {}
current_description = None
current_default_event = False
handle_lock = threading.Lock()
line, line_split, bot, server = None, None, None, None
def handler(f=None, description=None):
global current_description
if description:
def handler(f=None, description=None, default_event=False):
global current_description, current_default_event
if not f:
current_description = description
current_default_event = default_event
return handler
name = f.__name__.split("handle_")[1].upper()
handlers[name] = f
if current_description:
descriptions[name] = current_description
current_description = None
descriptions[name] = current_description
default_events[name] = current_default_event
current_description, current_default_event = None, False
def handle(_line, _line_split, _bot, _server):
global line, line_split, bot, server
handler_function = None
if len(_line_split) > 1:
name = _line_split[1]
if _line_split[0][0] == ":":
if _line_split[1] in handlers:
handler_function = handlers[_line_split[1]]
elif _line_split[1].isdigit():
_bot.events.on("received").on("numeric").on(
_line_split[1]).call(line=_line,
line_split=_line_split, server=_server,
number=_line_split[1])
if name in handlers:
handler_function = handlers[name]
if default_events.get(name, False) or not name in handlers:
if name.isdigit():
_bot.events.on("received").on("numeric").on(
name).call(line=_line,
line_split=_line_split, server=_server,
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:
handler_function = handlers[_line_split[0]]
if handler_function:
@ -48,12 +59,10 @@ def handle_PING():
server.send_pong(Utils.remove_colon(line_split[1]))
bot.events.on("received").on("ping").call(line=line,
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():
server.set_own_nickname(line_split[2])
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")
def handle_005():
isupport_line = Utils.arbitrary(line_split, 3)
@ -74,8 +83,8 @@ def handle_005():
server.channel_types = list(match.group(1))
bot.events.on("received").on("numeric").on("005").call(
line=line, line_split=line_split, server=server,
isupport=isupport_line)
@handler(description="whois respose (nickname, username, realname, hostname)")
isupport=isupport_line, number="005")
@handler(description="whois respose (nickname, username, realname, hostname)", default_event=True)
def handle_311():
nickname = line_split[3]
if server.is_own_nickname(nickname):
@ -86,12 +95,12 @@ def handle_311():
hostname = line_split[5]
target.username = username
target.hostname = hostname
@handler(description="on-join channel topic line")
@handler(description="on-join channel topic line", default_event=True)
def handle_332():
channel = server.get_channel(line_split[3])
topic = Utils.arbitrary(line_split, 4)
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():
channel = server.get_channel(line_split[3])
topic_setter_hostmask = line_split[4]
@ -101,7 +110,7 @@ def handle_333():
) else None
channel.set_topic_setter(nickname, username, hostname)
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():
channel = server.get_channel(line_split[4])
nicknames = line_split[5:]
@ -157,7 +166,7 @@ def handle_PART():
bot.events.on("self").on("part").call(line=line,
line_split=line_split, server=server, channel=channel,
reason=reason)
@handler(description="unknown command sent by us, oops!")
@handler(description="unknown command sent by us, oops!", default_event=True)
def handle_421():
print("warning: unknown command '%s'." % line_split[3])
@handler(description="a user has disconnected!")
@ -172,6 +181,31 @@ def handle_QUIT():
user=user)
else:
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")
def handle_NICK():
nickname, username, hostname = Utils.seperate_hostmask(line_split[0])
@ -269,12 +303,12 @@ def handle_PRIVMSG():
user=user, message=message, message_split=message_split,
action=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():
user = server.get_user(line_split[7])
user.username = line_split[4]
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():
channel = server.get_channel(line_split[3])
modes = line_split[4]
@ -282,14 +316,14 @@ def handle_324():
for mode in modes[1:]:
if mode in server.channel_modes:
channel.add_mode(mode)
@handler(description="channel creation unix timestamp")
@handler(description="channel creation unix timestamp", default_event=True)
def handle_329():
channel = server.get_channel(line_split[3])
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():
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():
bot.add_timer("rejoin", 5, channel_name=line_split[3],
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
def connect(self):
self.socket.connect((self.target_hostname, self.port))
if 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_nick(self.original_nickname)
self.connected = True
@ -172,6 +178,14 @@ class Server(object):
self.send("USER %s - - :%s" % (username, realname))
def send_nick(self, 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):
self.send("PASS %s" % password)
def send_ping(self, nonce="hello"):

View file

@ -14,6 +14,10 @@ class ModuleManager(object):
def _load_module(self, 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:
while True:
line = module_file.readline().strip()
@ -61,6 +65,7 @@ class ModuleManager(object):
if name in self.waiting_requirement:
for filename in self.waiting_requirement:
self.load_module(filename)
sys.stderr.write("module '%s' loaded.\n" % filename)
else:
sys.stderr.write("module '%s' not loaded.\n" % filename)
def load_modules(self):

View file

@ -1,18 +1,29 @@
class Module(object):
def __init__(self, bot):
bot.events.on("self").on("join").hook(self.on_join)
bot.events.on("received").on("numeric").on("366").hook(
self.on_connect)
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("received").on("numeric").on("366").hook(
self.on_identify_trigger)
bot.events.on("received").on("numeric").on("001").hook(
self.on_identify_trigger)
def on_join(self, event):
channels = set(event["server"].get_setting("autojoin", []))
channels.add(event["channel"].name)
event["server"].set_setting("autojoin", list(channels))
def on_self_part(self, event):
pass
def on_connect(self, event):
if event["line_split"][3].lower() == "#bitbot":
channels = event["server"].get_setting("autojoin", [])
for channel in channels:
event["server"].send_join(channel)
def on_join(self, event):
channels = set(event["server"].get_setting("autojoin", []))
channels.add(event["channel"].name)
event["server"].set_setting("autojoin", list(channels))
def on_identify_trigger(self, event):
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", [])
chan_keys = event["server"].get_setting("channel_keys", {})
for channel in channels:
if channel in chan_keys:
event["server"].send_join(channel, key=chan_keys[channel])
else:
event["server"].send_join(channel)

View file

@ -1,18 +1,28 @@
import base64
class Module(object):
def __init__(self, bot):
bot.events.on("new").on("server").hook(self.on_new_server)
bot.events.on("received").on("numeric").on("001"
).hook(self.on_connect)
bot.events.on("received").on("command").on("setnickserv"
).hook(self.set_nickserv, min_args=1, permission="setnickserv",
help="Set bot's nickserv password", usage="<password>",
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):
nickserv_password = event["server"].get_setting(
"nickserv-password")
if nickserv_password:
if nickserv_password and not event["server"].sasl_success:
event["server"].attempted_auth = True
event["server"].send_message("nickserv",
"identify %s" % nickserv_password)
@ -20,3 +30,27 @@ class Module(object):
nickserv_password = event["args"]
event["server"].set_setting("nickserv-password", nickserv_password)
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()