""" nefarious.py: Nefarious IRCu protocol module for PyLink. """ 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) ### COMMANDS 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 ### HANDLERS 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. Commands sent without an explicit sender prefix are treated as originating from the uplink server. """ 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:] # 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_sid elif sender_uid in self.irc.users: # Sender is a user (converting from name to UID gave a valid result). sender = sender_uid else: # No sender prefix; treat as coming from uplink IRCd. sender = self.irc.uplink args.insert(0, sender) command_token = args[1].upper() args = args[2:] log.debug('(%s) Found message sender as %s', self.irc.name, sender) try: # Convert the token given into a regular command, if present. command = self._getCommand(command_token) log.debug('(%s) Translating token %s to command %s', self.irc.name, command_token, command) 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(sender, command, args) if parsed_args is not None: return [sender, command, parsed_args] def handle_server(self, source, command, args): """Handles incoming server introductions.""" # <- SERVER nefarious.midnight.vpn 1 1460673022 1460673239 J10 ABP]] +h6 :Nefarious2 test server servername = args[0].lower() sid = args[5][:2] sdesc = args[-1] self.irc.servers[sid] = IrcServer(source, servername, desc=sdesc) if self.irc.uplink is None: # If we haven't already found our uplink, this is probably it. self.irc.uplink = sid return {'name': servername, 'sid': sid, 'text': sdesc} def handle_nick(self, source, command, args): """Handles the NICK command, used for user introductions and nick changes.""" if len(args) > 2: # <- AB N GL 1 1460673049 ~gl nefarious.midnight.vpn +iw B]AAAB ABAAA :realname nick = args[0] ts, ident, host = args[2:5] # TODO: fill this in realhost = None ip = '0.0.0.0' uid = args[-2] realname = args[-1] log.debug('(%s) handle_nick got args: nick=%s ts=%s uid=%s ident=%s ' 'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts, uid, ident, host, realname, realhost, ip) self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) self.irc.servers[source].users.add(uid) # https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L708 # Mode list is optional, and can be detected if the 6th argument starts with a +. # This list can last until the 3rd LAST argument in the line, should there be mode # parameters attached. if args[5].startswith('+'): modes = args[5:-3] parsedmodes = utils.parseModes(self.irc, uid, modes) utils.applyModes(self.irc, uid, parsedmodes) # Call the OPERED UP hook if +o is being added to the mode list. if ('+o', None) in parsedmodes: self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) # Set the accountname if present #if accountname != "*": # self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) Class = P10Protocol