From 19f1ee1be8b437ce327e63515119265f1ab60741 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 10 Apr 2016 20:24:58 -0700 Subject: [PATCH] p10: initial protocol stub (#87) This can connect and spawn the main PyLink client right now... It can't do anything else --- protocols/p10.py | 321 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 protocols/p10.py diff --git a/protocols/p10.py b/protocols/p10.py new file mode 100644 index 0000000..cb1c04e --- /dev/null +++ b/protocols/p10.py @@ -0,0 +1,321 @@ +""" +p10.py: P10 protocol module for PyLink, targetting Nefarious 2. +""" + +import sys +import os + +# Import hacks to access utils and classes... +curdir = os.path.dirname(__file__) +sys.path += [curdir, os.path.dirname(curdir)] + +import utils +from log import log +from classes import * + +class P10SIDGenerator(): + def __init__(self, irc): + self.irc = irc + try: + query = irc.serverdata["sidrange"] + except (KeyError, ValueError): + raise RuntimeError('(%s) "sidrange" is missing from your server configuration block!' % irc.name) + + try: + # Query is taken in the format MINNUM-MAXNUM, so we need + # to get the actual number values out of that. + self.minnum, self.maxnum = map(int, query.split('-', 1)) + except ValueError: + raise RuntimeError('(%s) Invalid sidrange %r' % (irc.name, query)) + else: + # Initialize a counter for the last numeric we've used. + self.currentnum = self.minnum + + @staticmethod + def encode(num, length=2): + """ + Encodes a given numeric using P10 Base64 numeric nicks, as documented at + https://github.com/evilnet/nefarious2/blob/a29b63144/doc/p10.txt#L69-L92 + """ + c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[]' + s = '' + # To accomplish this encoding, we divide the given value into a series of places. Much like + # a number can be divided into hundreds, tens, and digits (e.g. 128 is 1, 2, and 8), the + # places here add up to the value given. In the case of P10 Base64, each place can represent + # 0 to 63. divmod() is used to get the quotient and remainder of a division operation. When + # used on the input number and the length of our allowed characters list, the output becomes + # the values of (the next highest base, the current base). + places = divmod(num, len(c)) + print('places:', places) + while places[0] >= len(c): + # If the base one higher than ours is greater than the largest value each base can + # represent, repeat the divmod process on that value,also keeping track of the + # remaining values we've calculated already. + places = divmod(places[0], len(c)) + places[1:] + print('places:', places) + + # Expand the place values we've got to the characters list now. + chars = [c[place] for place in places] + s = ''.join(chars) + # Pad up to the required string length using the first character in our list (A). + return s.rjust(length, c[0]) + + def next_sid(self): + """ + Returns the next available SID. + """ + if self.currentnum > self.maxnum: + raise ProtocolError("Ran out of valid SIDs! Check your 'sidrange' setting and try again.") + sid = self.encodeSID(self.currentnum) + + self.currentnum += 1 + return sid + +class P10Protocol(Protocol): + + def __init__(self, irc): + super().__init__(irc) + + # Dictionary of UID generators (one for each server) that the protocol module will fill in. + self.uidgen = {} + + # SID generator for P10. + self.sidgen = P10SIDGenerator(irc) + + def _send(self, source, text): + self.irc.send("%s %s" % (source, text)) + + @staticmethod + def _getCommand(token): + """Returns the command name for the given token.""" + tokens = { + 'AC': 'ACCOUNT', + 'AD': 'ADMIN', + 'LL': 'ASLL', + 'A': 'AWAY', + 'B': 'BURST', + 'CAP': 'CAP', + 'CM': 'CLEARMODE', + 'CLOSE': 'CLOSE', + 'CN': 'CNOTICE', + 'CO': 'CONNECT', + 'CP': 'CPRIVMSG', + 'C': 'CREATE', + 'DE': 'DESTRUCT', + 'DS': 'DESYNCH', + 'DIE': 'DIE', + 'DNS': 'DNS', + 'EB': 'END_OF_BURST', + 'EA': 'EOB_ACK', + 'Y': 'ERROR', + 'GET': 'GET', + 'GL': 'GLINE', + 'HASH': 'HASH', + 'HELP': 'HELP', + 'F': 'INFO', + 'I': 'INVITE', + 'ISON': 'ISON', + 'J': 'JOIN', + 'JU': 'JUPE', + 'K': 'KICK', + 'D': 'KILL', + 'LI': 'LINKS', + 'LIST': 'LIST', + 'LU': 'LUSERS', + 'MAP': 'MAP', + 'M': 'MODE', + 'MO': 'MOTD', + 'E': 'NAMES', + 'N': 'NICK', + 'O': 'NOTICE', + 'OPER': 'OPER', + 'OM': 'OPMODE', + 'L': 'PART', + 'PA': 'PASS', + 'G': 'PING', + 'Z': 'PONG', + 'POST': 'POST', + 'P': 'PRIVMSG', + 'PRIVS': 'PRIVS', + 'PROTO': 'PROTO', + 'Q': 'QUIT', + 'REHASH': 'REHASH', + 'RESET': 'RESET', + 'RESTART': 'RESTART', + 'RI': 'RPING', + 'RO': 'RPONG', + 'S': 'SERVER', + 'SERVSET': 'SERVLIST', + 'SERVSET': 'SERVSET', + 'SET': 'SET', + 'SE': 'SETTIME', + 'U': 'SILENCE', + 'SQUERY': 'SQUERY', + 'SQ': 'SQUIT', + 'R': 'STATS', + 'TI': 'TIME', + 'T': 'TOPIC', + 'TR': 'TRACE', + 'UP': 'UPING', + 'USER': 'USER', + 'USERHOST': 'USERHOST', + 'USERIP': 'USERIP', + 'V': 'VERSION', + 'WC': 'WALLCHOPS', + 'WA': 'WALLOPS', + 'WU': 'WALLUSERS', + 'WV': 'WALLVOICES', + 'H': 'WHO', + 'W': 'WHOIS', + 'X': 'WHOWAS', + 'XQ': 'XQUERY', + 'XR': 'XREPLY', + 'SN': 'SVSNICK', + 'SJ': 'SVSJOIN', + 'SH': 'SETHOST' + } + # If the token isn't in the list, return it raw. + return tokens.get(token, token) + + def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', + manipulatable=False): + # {7N} *** NICK + # 1 + # 2 + # 3 + # 4 <-- a.k.a ident + # 5 + # 6 [<+modes>] + # 7+ [] + # -3 + # -2 + # -1 + + server = server or self.irc.sid + if not self.irc.isInternalServer(server): + raise ValueError('Server %r is not a PyLink server!' % server) + + # Create an UIDGenerator instance for every SID, so that each gets + # distinct values. + uid = self.uidgen.setdefault(server, utils.P10UIDGenerator(server)).next_uid() + + # Fill in all the values we need + ts = ts or int(time.time()) + realname = realname or self.irc.botdata['realname'] + realhost = realhost or host + raw_modes = utils.joinModes(modes) + + # Initialize an IrcUser instance + u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, + realhost=realhost, ip=ip, manipulatable=manipulatable, + opertype=opertype) + + # Fill in modes and add it to our users index + utils.applyModes(self.irc, uid, modes) + self.irc.servers[server].users.add(uid) + # TODO: send IPs + self._send(server, "N {nick} 1 {ts} {ident} {host} {modes} AAAAAA {uid} " + ":{realname}".format(ts=ts, host=host, nick=nick, ident=ident, uid=uid, + modes=raw_modes, ip=ip, realname=realname, + realhost=realhost)) + return u + + def join(self, client, channel): + pass + + def ping(*args): + pass + + def connect(self): + """Initializes a connection to a server.""" + ts = self.irc.start_ts + + self.irc.send("PASS :%s" % self.irc.serverdata["sendpass"]) + + # {7S} *** SERVER + + # 1 + # 2 + # 3 + # 4 + # 5 + # 6 + # 7 <-- Mark ourselves as a service with IPv6 support (+s & +6) -GLolol + # -1 + + name = self.irc.serverdata["hostname"] + + # HACK: Encode our SID everywhere, and replace it in the IrcServer index. + old_sid = self.irc.sid + self.irc.sid = sid = self.sidgen.encode(self.irc.serverdata["sid"]) + self.irc.servers[sid] = self.irc.servers[old_sid] + del self.irc.servers[old_sid] + + desc = self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'] + + self.irc.send('SERVER %s 1 %s %s J10 %s]]] +s6 :%s' % (name, ts, ts, sid, desc)) + self._send(sid, "EB") + self.irc.connected.set() + + def handle_events(self, data): + """ + Event handler for the P10 protocol. + + This passes most commands to the various handle_ABCD() functions defined elsewhere in the + protocol modules, coersing various sender prefixes from nicks and server names to P10 + "numeric nicks", whenever possible. + + Per the P10 specification, commands sent without an explicit sender prefix are treated + as originating from the uplink server if they are SQUIT or KILL messages. Otherwise, they + are silently ignored. + """ + data = data.split(" ") + args = self.parseArgs(data) + + sender = args[0] + if sender.startswith(':'): + # From https://github.com/evilnet/nefarious2/blob/a29b63144/doc/p10.txt#L140: + # if source begins with a colon, it (except for the colon) is the name. otherwise, it is + # a numeric. a P10 implementation must only send lines with a numeric source prefix. + sender = sender[1:] + + command_token = args[1].upper() + + # If the sender isn't in numeric format, try to convert it automatically. + sender_sid = self._getSid(sender) + sender_uid = self._getUid(sender) + if sender_sid in self.irc.servers: + # Sender is a server (converting from name to SID gave a valid result). + sender = sender_server + elif sender_uid in self.irc.users: + # Sender is a user (converting from name to UID gave a valid result). + sender = self._getUid(sender) + elif command_token in ('SQ', 'SQUIT', 'D',' KILL'): + # From https://github.com/evilnet/nefarious2/blob/a29b63144/doc/p10.txt#L142: + # if the source does not exist: if the command is SQUIT or KILL (or short token), the + # line must be parsed anyway, with the directly linked server from which the message + # came as the source. otherwise the line must be ignored. + sender = self.uplink + else: + log.debug("(%s) Ignoring message '%s' with bad sender '%s'.", self.irc.name, + args, args[0]) + return + + args = args[2:] + + try: + # Convert the token given into a regular command, if present. + command = self._getCommand(command_token) + + func = getattr(self, 'handle_'+command.lower()) + + except AttributeError: # Unhandled command, ignore + return + + else: # Send a hook with the hook arguments given by the handler function. + parsed_args = func(numeric, command, args) + if parsed_args is not None: + return [numeric, command, parsed_args] + +Class = P10Protocol