3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-12-24 19:52:53 +01:00

p10: initial protocol stub (#87)

This can connect and spawn the main PyLink client right now... It can't do anything else
This commit is contained in:
James Lu 2016-04-10 20:24:58 -07:00
parent 3a00e46b48
commit 19f1ee1be8

321
protocols/p10.py Normal file
View File

@ -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 <nickname>
# 2 <hops>
# 3 <TS>
# 4 <userid> <-- a.k.a ident
# 5 <host>
# 6 [<+modes>]
# 7+ [<mode parameters>]
# -3 <base64 IP>
# -2 <numeric>
# -1 <fullname>
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 <name of new server>
# 2 <hops>
# 3 <boot TS>
# 4 <link TS>
# 5 <protocol>
# 6 <numeric of new server><max client numeric>
# 7 <flags> <-- Mark ourselves as a service with IPv6 support (+s & +6) -GLolol
# -1 <description of new server>
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