3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 01:09:22 +01:00
PyLink/protocols/unreal.py
2015-10-09 22:38:28 -07:00

376 lines
19 KiB
Python

import time
import sys
import os
import time
import ipaddress
import re
curdir = os.path.dirname(__file__)
sys.path += [curdir, os.path.dirname(curdir)]
import utils
from log import log
from classes import *
from ts6_common import TS6BaseProtocol
class UnrealProtocol(TS6BaseProtocol):
def __init__(self, irc):
super(UnrealProtocol, self).__init__(irc)
# Set our case mapping (rfc1459 maps "\" and "|" together, for example".
self.casemapping = 'ascii'
self.proto_ver = 2351
self.hook_map = {'UMODE2': 'MODE'}
self.uidgen = {}
self.caps = {}
self._unrealCmodes = {'l': 'limit', 'c': 'blockcolor', 'G': 'censor',
'D': 'delayjoin', 'n': 'noextmsg', 's': 'secret',
'T': 'nonotice', 'z': 'sslonly', 'b': 'ban', 'V': 'noinvite',
'Z': 'issecure', 'r': 'registered', 'N': 'nonick',
'e': 'banexception', 'R': 'regonly', 'M': 'regmoderated',
'p': 'private', 'Q': 'nokick', 'P': 'permanent', 'k': 'key',
'C': 'noctcp', 'O': 'operonly', 'S': 'stripcolor',
'm': 'moderated', 'K': 'noknock', 'o': 'op', 'v': 'voice',
'I': 'invex', 't': 'topiclock', 'f': 'flood_unreal'}
self._neededCaps = ["VL", "SID", "CHANMODES", "NOQUIT", "SJ3"]
### OUTGOING COMMAND FUNCTIONS
def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(),
server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None,
manipulatable=True):
"""Spawns a client with nick <nick> on the given IRC connection.
Note: No nick collision / valid nickname checks are done here; it is
up to plugins to make sure they don't introduce anything invalid."""
server = server or self.irc.sid
if not utils.isInternalServer(self.irc, server):
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
# Unreal 3.4 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's
# do, but we can do that fine...
uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid()
ts = ts or int(time.time())
realname = realname or self.irc.botdata['realname']
realhost = realhost or host
raw_modes = utils.joinModes(modes)
u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname,
realhost=realhost, ip=ip)
utils.applyModes(self.irc, uid, modes)
self.irc.servers[server].users.add(uid)
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
self._send(server, "UID {nick} 0 {ts} {ident} {realhost} {uid} 0 {modes} "
"* {host} * :{realname}".format(ts=ts, host=host,
nick=nick, ident=ident, uid=uid,
modes=raw_modes, realname=realname,
realhost=realhost))
return u
def joinClient(self, client, channel):
pass
def pingServer(self, source=None, target=None):
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink
automatically by the Irc() internals; plugins shouldn't have to use this."""
source = source or self.irc.sid
target = target or self.irc.uplink
if not (target is None or source is None):
self._send(source, 'PING %s %s' % (self.irc.servers[source].name, self.irc.servers[target].name))
### HANDLERS
def connect(self):
"""Initializes a connection to a server."""
ts = self.irc.start_ts
self.irc.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'}
### XXX: fill out self.irc.umodes
f = self.irc.send
host = self.irc.serverdata["hostname"]
f('PASS :%s' % self.irc.serverdata["sendpass"])
# https://github.com/unrealself.ircd/unrealself.ircd/blob/2f8cb55e/doc/technical/protoctl.txt
# We support the following protocol features:
# SJ3 - extended SJOIN
# NOQUIT - QUIT messages aren't sent for all users in a netsplit
# NICKv2 - Extended NICK command, sending MODE and CHGHOST info with it
# SID - Use UIDs and SIDs (unreal 3.4)
# VL - Sends version string in below SERVER message
# UMODE2 - used for users setting modes on themselves (one less argument needed)
# EAUTH - Early auth? (Unreal 3.4 linking protocol)
# ~~NICKIP - sends the IP in the NICK/UID command~~ Doesn't work with SID/UID support
f('PROTOCTL SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL EAUTH=%s SID=%s' % (self.irc.serverdata["hostname"], self.irc.sid))
sdesc = self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc']
f('SERVER %s 1 U%s-h6e-%s :%s' % (host, self.proto_ver, self.irc.sid, sdesc))
# Now, we wait until remote sends its NETINFO command (handle_netinfo),
# so we can find and use a matching netname, preventing netname mismatch
# errors.
def handle_netinfo(self, numeric, command, args):
# <- NETINFO maxglobal currenttime protocolversion cloakhash 0 0 0 :networkname
# "maxglobal" is the amount of maximum global users we've seen so far.
# We'll just set it to 1 (the PyLink client), since this is completely
# arbitrary.
self.irc.send('NETINFO 1 %s %s * 0 0 0 :%s' % (self.irc.start_ts, self.proto_ver, args[-1]))
self._send(self.irc.sid, 'EOS')
def handle_uid(self, numeric, command, args):
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
# <- :001 UID GL| 0 1441389007 gl 10.120.0.6 001ZO8F03 0 +iwx * 391A9CB9.26A16454.D9847B69.IP CngABg== :realname
# arguments: nick, number???, ts, ident, real-host, UID, number???, modes,
# star???, hidden host, some base64 thing???, and realname
# TODO: find out what all the "???" fields mean.
nick = args[0]
ts, ident, realhost, uid = args[2:6]
modestring = args[7]
host = args[9]
try:
ip = ipaddress.ip_address(host)
except ValueError: # Invalid for IP
# XXX: find a way of getting the real IP of the user (protocol-wise)
# without looking up every hostname ourselves (that's expensive!)
# NICKIP doesn't seem to work for the UID command...
ip = "0.0.0.0"
realname = args[-1]
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
parsedmodes = utils.parseModes(self.irc, uid, [modestring])
utils.applyModes(self.irc, uid, parsedmodes)
self.irc.servers[numeric].users.add(uid)
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
def handle_pass(self, numeric, command, args):
# <- PASS :abcdefg
if args[0] != self.irc.serverdata['recvpass']:
raise ProtocolError("Error: RECVPASS from uplink does not match configuration!")
def handle_ping(self, numeric, command, args):
if numeric == self.irc.uplink:
self.irc.send('PONG %s :%s' % (self.irc.serverdata['hostname'], args[-1]))
def handle_pong(self, source, command, args):
log.debug('(%s) Ping received from %s for %s.', self.irc.name, source, args[-1])
if source in (self.irc.uplink, self.irc.servers[self.irc.uplink].name) and args[-1] == self.irc.serverdata['hostname']:
log.debug('(%s) Set self.irc.lastping.', self.irc.name)
self.irc.lastping = time.time()
def handle_server(self, numeric, command, args):
# <- SERVER unreal.midnight.vpn 1 :U2351-Fhin6OoEM UnrealIRCd test server
sname = args[0]
if numeric == self.irc.uplink:
for cap in self._neededCaps:
if cap not in self.caps:
raise ProtocolError("Not all required capabilities were met "
"by the remote server. Your version of UnrealIRCd "
"is probably too old! (Got: %s, needed: %s)" %
(sorted(self.caps.keys()),
sorted(_neededCaps)))
sdesc = args[-1].split(" ")
# Get our protocol version :)
vline = sdesc[0].split('-', 1)
try:
protover = int(vline[0].strip('U'))
except ValueError:
raise ProtocolError("Protocol version too old! (needs at least 2351 "
"(Unreal 3.4-beta1/2), got something invalid; "
"is VL being sent?)")
sdesc = args[-1][1:]
if protover < 2351:
raise ProtocolError("Protocol version too old! (needs at least 2351 "
"(Unreal 3.4-beta1/2), got %s)" % protover)
self.irc.servers[numeric] = IrcServer(None, sname)
else:
# Legacy servers can still be introduced this way (without a SID).
raise NotImplementedError("FIXME: we don't handle legacy server "
"introduction yet (using SERVER instead of SID).")
def handle_sid(self, numeric, command, args):
"""Handles the SID command, used for introducing remote servers by our uplink."""
# <- SID services.int 2 00A :Shaltúre IRC Services
sname = args[0].lower()
sid = args[2]
sdesc = args[-1]
self.irc.servers[sid] = IrcServer(numeric, sname, desc=sdesc)
return {'name': sname, 'sid': sid, 'text': sdesc}
def handle_squit(self, numeric, command, args):
"""Handles the SQUIT command."""
# <- SQUIT services.int :Read error
# Convert the server name to a SID...
args[0] = self._sidToServer(args[0])
return super(UnrealProtocol, self).handle_squit(numeric, 'SQUIT', args)
def handle_protoctl(self, numeric, command, args):
# <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID
# <- PROTOCTL CHANMODES=beI,k,l,psmntirzMQNRTOVKDdGPZSCc NICKCHARS= SID=001 MLOCK TS=1441314501 EXTSWHOIS
for cap in args:
if cap.startswith('SID'):
self.irc.uplink = cap.split('=', 1)[1]
self.caps['SID'] = True
elif cap.startswith('CHANMODES'):
cmodes = cap.split('=', 1)[1]
self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] = cmodes.split(',')
for m in cmodes:
if m in self._unrealCmodes:
self.irc.cmodes[self._unrealCmodes[m]] = m
self.caps['CHANMODES'] = True
self.irc.cmodes['*B'] += 'f' # Add +f to the list too, dunno why it isn't there.
# Because more than one PROTOCTL line is sent, we have to delay the
# check to see whether our needed capabilities are all there...
# That's done by handle_server(), which comes right after PROTOCTL.
elif cap == 'VL':
self.caps['VL'] = True
elif cap == 'NOQUIT':
self.caps['NOQUIT'] = True
elif cap == 'SJ3':
self.caps['SJ3'] = True
def _sidToServer(self, sname):
"""Returns the SID of a server with the given name, if present."""
nick = sname.lower()
for k, v in self.irc.servers.items():
if v.name.lower() == nick:
return k
def _convertNick(self, target):
"""Converts a nick argument to its matching UID."""
target = utils.nickToUid(self.irc, target) or target
if target not in self.irc.users:
log.warning("(%s) Possible desync? Got command target %s, who "
"isn't in our user list!", self.irc.name, target)
return target
def handle_events(self, data):
"""Event handler for the UnrealIRCd 3.4+ protocol.
This passes most commands to the various handle_ABCD() functions
elsewhere in this module, coersing various sender prefixes from nicks
to UIDs wherever possible.
Unreal 3.4's protocol operates similarly to TS6, where lines can have :
indicating a long argument lasting to the end of the line. Not all commands
send an explicit sender prefix, in which case, it will be set to the SID
of the uplink server.
"""
data = data.split(" ")
try: # Message starts with a SID/UID prefix.
args = self.parseTS6Args(data)
sender = args[0]
command = args[1]
args = args[2:]
# If the sender isn't in UID format, try to convert it automatically.
# Unreal's protocol isn't quite consistent with this yet!
numeric = self._sidToServer(sender) or utils.nickToUid(self.irc, sender) or \
sender
# parseTS6Args() will raise IndexError if the TS6 sender prefix is missing.
except IndexError:
# Raw command without an explicit sender; assume it's being sent by our uplink.
args = self.parseArgs(data)
numeric = self.irc.uplink
command = args[0]
args = args[1:]
try:
func = getattr(self, 'handle_'+command.lower())
except AttributeError: # unhandled command
pass
else:
parsed_args = func(numeric, command, args)
if parsed_args is not None:
return [numeric, command, parsed_args]
def handle_privmsg(self, source, command, args):
# Convert nicks to UIDs, where they exist.
target = self._convertNick(args[0])
# We use lowercase channels internally, but uppercase UIDs.
if utils.isChannel(target):
target = utils.toLower(self.irc, target)
return {'target': target, 'text': args[1]}
handle_notice = handle_privmsg
def handle_join(self, numeric, command, args):
"""Handles the UnrealIRCd JOIN command."""
# <- :GL JOIN #pylink,#test
for channel in args[0].split(','):
c = self.irc.channels[channel]
if args[0] == '0':
# /join 0; part the user from all channels
oldchans = self.irc.users[numeric].channels.copy()
log.debug('(%s) Got /join 0 from %r, channel list is %r',
self.irc.name, numeric, oldchans)
for ch in oldchans:
self.irc.channels[ch].users.discard(numeric)
self.irc.users[numeric].channels.discard(ch)
return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'}
self.irc.users[numeric].channels.add(channel)
self.irc.channels[channel].users.add(numeric)
# Call hooks manually, because one JOIN command in UnrealIRCd can
# have multiple channels...
self.irc.callHooks([numeric, command, {'channel': channel, 'users': [numeric], 'modes':
c.modes, 'ts': c.ts}])
def handle_sjoin(self, numeric, command, args):
"""Handles the UnrealIRCd SJOIN command."""
# <- :001 SJOIN 1444361345 #endlessvoid :001DJ1O02
# memberlist should be a list of UIDs with their channel status prefixes, as
# in ":001AAAAAA @001AAAAAB +001AAAAAC".
# Interestingly, no modes are ever sent in this command as far as I've seen.
channel = utils.toLower(self.irc, args[1])
userlist = args[-1].split()
our_ts = self.irc.channels[channel].ts
their_ts = int(args[0])
if their_ts < our_ts:
# Channel timestamp was reset on burst
log.debug('(%s) Setting channel TS of %s to %s from %s',
self.irc.name, channel, their_ts, our_ts)
self.irc.channels[channel].ts = their_ts
self.irc.channels[channel].modes.clear()
for p in self.irc.channels[channel].prefixmodes.values():
p.clear()
namelist = []
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
for userpair in userlist:
r = re.search(r'([^\d]*)(.*)', userpair)
user = r.group(2)
modeprefix = r.group(1) or ''
finalprefix = ''
assert user, 'Failed to get the UID from %r; our regex needs updating?' % userpair
log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.irc.name, modeprefix, user)
for m in modeprefix:
# Iterate over the mapping of prefix chars to prefixes, and
# find the characters that match.
for char, prefix in self.irc.prefixmodes.items():
if m == prefix:
finalprefix += char
namelist.append(user)
self.irc.users[user].channels.add(channel)
# Only merge the remote's prefix modes if their TS is smaller or equal to ours.
if their_ts <= our_ts:
utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in finalprefix])
self.irc.channels[channel].users.add(user)
return {'channel': channel, 'users': namelist, 'modes': self.irc.channels[channel].modes, 'ts': their_ts}
def handle_mode(self, numeric, command, args):
# <- :unreal.midnight.vpn MODE #endlessvoid +bb test!*@* *!*@bad.net
# <- :unreal.midnight.vpn MODE #endlessvoid +q GL 1444361345
# <- :unreal.midnight.vpn MODE #endlessvoid +ntCo GL 1444361345
# <- :unreal.midnight.vpn MODE #endlessvoid +mntClfo 5 [10t]:5 GL 1444361345
# Well... this seems relatively inconsistent.
# Why does only setting some modes show a TS?
# Also, we need to get rid of that extra space following the +f argument. :|
if utils.isChannel(args[0]):
channel = utils.toLower(self.irc, args[0])
oldobj = self.irc.channels[channel].deepcopy()
modes = list(filter(None, args[1:]))
parsedmodes = utils.parseModes(self.irc, channel, modes)
if parsedmodes:
utils.applyModes(self.irc, channel, parsedmodes)
return {'target': channel, 'modes': parsedmodes, 'oldchan': oldobj}
else:
log.warning("(%s) received MODE for non-channel target: %r",
self.irc.name, args)
def handle_umode2(self, numeric, command, args):
"""Handles UMODE2, used to set user modes on oneself."""
parsedmodes = utils.parseModes(self.irc, numeric, args)
utils.applyModes(self.irc, numeric, parsedmodes)
return {'target': numeric, 'modes': parsedmodes}
Class = UnrealProtocol