From 197ccd7916667f217d51813e4970e7cb2af65694 Mon Sep 17 00:00:00 2001 From: James Lu Date: Tue, 1 Sep 2015 22:01:22 -0700 Subject: [PATCH 01/70] relay/protocols: use utils.toLower() for channel names, respecting IRCd casemappings Closes #102. (cherry picked from commit 0b590d6ab30c32855214410ce83c0a91ad65b2b5) --- plugins/relay.py | 12 ++++++------ protocols/inspircd.py | 28 ++++++++++++++-------------- protocols/ts6.py | 14 +++++++------- utils.py | 7 +++++++ 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 573c22e..e3f1d65 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -754,7 +754,7 @@ def create(irc, source, args): Creates the channel over the relay.""" try: - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) except IndexError: utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.") return @@ -777,7 +777,7 @@ def destroy(irc, source, args): Removes from the relay, delinking all networks linked to it.""" try: - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) except IndexError: utils.msg(irc, source, "Error: not enough arguments. Needs 1: channel.") return @@ -806,13 +806,13 @@ def link(irc, source, args): Links channel on over the relay to . If is not specified, it defaults to the same name as .""" try: - channel = args[1].lower() + channel = utils.toLower(irc, args[1]) remotenet = args[0].lower() except IndexError: utils.msg(irc, source, "Error: not enough arguments. Needs 2-3: remote netname, channel, local channel name (optional).") return try: - localchan = args[2].lower() + localchan = utils.toLower(irc, args[2]) except IndexError: localchan = channel for c in (channel, localchan): @@ -858,7 +858,7 @@ def delink(irc, source, args): Delinks channel . must and can only be specified if you are on the host network for , and allows you to pick which network to delink. To remove a relay entirely, use the 'destroy' command instead.""" try: - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) except IndexError: utils.msg(irc, source, "Error: not enough arguments. Needs 1-2: channel, remote netname (optional).") return @@ -1001,7 +1001,7 @@ def linkacl(irc, source, args): return try: cmd = args[0].lower() - channel = args[1].lower() + channel = utils.toLower(irc, args[1]) except IndexError: utils.msg(irc, source, missingargs) return diff --git a/protocols/inspircd.py b/protocols/inspircd.py index c62b7e0..11240ef 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -53,7 +53,7 @@ def joinClient(irc, client, channel): # InspIRCd doesn't distinguish between burst joins and regular joins, # so what we're actually doing here is sending FJOIN from the server, # on behalf of the clients that call it. - channel = channel.lower() + channel = utils.toLower(irc, channel) server = utils.isInternalClient(irc, client) if not server: log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel) @@ -67,7 +67,7 @@ def joinClient(irc, client, channel): irc.users[client].channels.add(channel) def sjoinServer(irc, server, channel, users, ts=None): - channel = channel.lower() + channel = utils.toLower(irc, channel) server = server or irc.sid assert users, "sjoinServer: No users sent?" log.debug('(%s) sjoinServer: got %r for users', irc.name, users) @@ -113,7 +113,7 @@ def sjoinServer(irc, server, channel, users, ts=None): irc.channels[channel].users.update(uids) def partClient(irc, client, channel, reason=None): - channel = channel.lower() + channel = utils.toLower(irc, channel) if not utils.isInternalClient(irc, client): log.error('(%s) Error trying to part client %r to %r (no such pseudoclient exists)', irc.name, client, channel) raise LookupError('No such PyLink PseudoClient exists.') @@ -156,7 +156,7 @@ def _sendKick(irc, numeric, channel, target, reason=None): """ Sends a kick from a PyLink PseudoClient.""" - channel = channel.lower() + channel = utils.toLower(irc, channel) if not reason: reason = 'No reason given' _send(irc, numeric, 'KICK %s %s :%s' % (channel, target, reason)) @@ -209,7 +209,7 @@ def _sendModes(irc, numeric, target, modes, ts=None): utils.applyModes(irc, target, modes) joinedmodes = utils.joinModes(modes) if utils.isChannel(target): - ts = ts or irc.channels[target.lower()].ts + ts = ts or irc.channels[utils.toLower(irc, target)].ts _send(irc, numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes)) else: _send(irc, numeric, 'MODE %s %s' % (target, joinedmodes)) @@ -372,7 +372,7 @@ def handle_privmsg(irc, source, command, args): target = args[0] # We use lowercase channels internally, but uppercase UIDs. if utils.isChannel(target): - target = target.lower() + target = utils.toLower(irc, target) return {'target': target, 'text': args[1]} handle_notice = handle_privmsg @@ -386,13 +386,13 @@ def handle_kill(irc, source, command, args): def handle_kick(irc, source, command, args): # :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) kicked = args[1] handle_part(irc, kicked, 'KICK', [channel, args[2]]) return {'channel': channel, 'target': kicked, 'text': args[2]} def handle_part(irc, source, command, args): - channels = args[0].lower().split(',') + channels = utils.toLower(irc, args[0]).split(',') for channel in channels: # We should only get PART commands for channels that exist, right?? irc.channels[channel].removeuser(source) @@ -415,7 +415,7 @@ def handle_error(irc, numeric, command, args): def handle_fjoin(irc, servernumeric, command, args): # :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...> - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) # InspIRCd sends each user's channel data in the form of 'modeprefix(es),UID' userlist = args[-1].split() our_ts = irc.channels[channel].ts @@ -488,7 +488,7 @@ def handle_save(irc, numeric, command, args): def handle_fmode(irc, numeric, command, args): # <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) modes = args[2:] changedmodes = utils.parseModes(irc, channel, modes) utils.applyModes(irc, channel, changedmodes) @@ -656,7 +656,7 @@ def spawnServer(irc, name, sid=None, uplink=None, desc='PyLink Server'): def handle_ftopic(irc, numeric, command, args): # <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) ts = args[1] setter = args[2] topic = args[-1] @@ -666,7 +666,7 @@ def handle_ftopic(irc, numeric, command, args): def handle_topic(irc, numeric, command, args): # <- :70MAAAAAA TOPIC #test :test - channel = args[0].lower() + channel = utils.toLower(irc, args[0]) topic = args[1] ts = int(time.time()) irc.channels[channel].topic = topic @@ -676,7 +676,7 @@ def handle_topic(irc, numeric, command, args): def handle_invite(irc, numeric, command, args): # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0 target = args[0] - channel = args[1].lower() + channel = utils.toLower(irc, args[1]) # We don't actually need to process this; it's just something plugins/hooks can use return {'target': target, 'channel': channel} @@ -694,7 +694,7 @@ def handle_encap(irc, numeric, command, args): targetmask = args[0] real_command = args[1] if targetmask == '*' and real_command == 'KNOCK': - channel = args[2].lower() + channel = utils.toLower(irc, args[2]) text = args[3] return {'parse_as': real_command, 'channel': channel, 'text': text} diff --git a/protocols/ts6.py b/protocols/ts6.py index c7b233d..6beadcb 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -51,7 +51,7 @@ def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set() return u def joinClient(irc, client, channel): - channel = channel.lower() + channel = utils.toLower(irc, channel) # JOIN: # parameters: channelTS, channel, '+' (a plus sign) if not utils.isInternalClient(irc, client): @@ -71,7 +71,7 @@ def sjoinServer(irc, server, channel, users, ts=None): # their status ('@+', '@', '+' or ''), for example: # '@+1JJAAAAAB +2JJAAAA4C 1JJAAAADS'. All users must be behind the source server # so it is not possible to use this message to force users to join a channel. - channel = channel.lower() + channel = utils.toLower(irc, channel) server = server or irc.sid assert users, "sjoinServer: No users sent?" log.debug('(%s) sjoinServer: got %r for users', irc.name, users) @@ -124,7 +124,7 @@ def sjoinServer(irc, server, channel, users, ts=None): def _sendModes(irc, numeric, target, modes, ts=None): utils.applyModes(irc, target, modes) if utils.isChannel(target): - ts = ts or irc.channels[target.lower()].ts + ts = ts or irc.channels[utils.toLower(irc, target)].ts # TMODE: # parameters: channelTS, channel, cmode changes, opt. cmode parameters... @@ -371,7 +371,7 @@ def handle_pong(irc, source, command, args): irc.lastping = time.time() def handle_part(irc, source, command, args): - channels = args[0].lower().split(',') + channels = utils.toLower(irc, args[0]).split(',') # We should only get PART commands for channels that exist, right?? for channel in channels: irc.channels[channel].removeuser(source) @@ -389,7 +389,7 @@ def handle_part(irc, source, command, args): def handle_sjoin(irc, servernumeric, command, args): # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist - channel = args[1].lower() + channel = utils.toLower(irc, args[1]) userlist = args[-1].split() our_ts = irc.channels[channel].ts their_ts = int(args[0]) @@ -440,7 +440,7 @@ def handle_join(irc, numeric, command, args): irc.users[numeric].channels.discard(channel) return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} else: - channel = args[1].lower() + channel = utils.toLower(irc, args[1]) our_ts = irc.channels[channel].ts if ts < our_ts: # Channel timestamp was reset on burst @@ -495,7 +495,7 @@ handle_sid = handle_server def handle_tmode(irc, numeric, command, args): # <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4 - channel = args[1].lower() + channel = utils.toLower(irc, args[1]) modes = args[2:] changedmodes = utils.parseModes(irc, channel, modes) utils.applyModes(irc, channel, changedmodes) diff --git a/utils.py b/utils.py index db02ff4..8eeb092 100644 --- a/utils.py +++ b/utils.py @@ -122,6 +122,10 @@ def add_hook(func, command): command_hooks[command].append(func) def toLower(irc, text): + """ + + Returns a lowercase representation of based on 's + casemapping (rfc1459 vs ascii).""" if irc.proto.casemapping == 'rfc1459': text = text.replace('{', '[') text = text.replace('}', ']') @@ -130,6 +134,9 @@ def toLower(irc, text): return text.lower() def nickToUid(irc, nick): + """ + + Returns the UID of a user named , if present.""" nick = toLower(irc, nick) for k, v in irc.users.items(): if toLower(irc, v.nick) == nick: From 7f526267ec33204eabf0b178188ddde123b5e573 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 9 Sep 2015 20:53:04 -0700 Subject: [PATCH 02/70] relay: minor typo in error message --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index 81d71fb..3e563f6 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -80,7 +80,7 @@ def normalizeNick(irc, netname, nick, separator=None, uid=''): nick = normalizeNick(irc, netname, orig_nick, separator=new_sep) finalLength = len(nick) assert finalLength <= maxnicklen, "Normalized nick %r went over max " \ - "nick length (got: %s, allowed: %s!" % (nick, finalLength, maxnicklen) + "nick length (got: %s, allowed: %s!)" % (nick, finalLength, maxnicklen) return nick From df42a81191a0f6ce878237ff4666330df7917ef0 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 10:39:05 -0700 Subject: [PATCH 03/70] classes/protocols: store server descriptions internally Closes #106. --- classes.py | 7 +++++-- protocols/inspircd.py | 7 ++++--- protocols/ts6.py | 5 +++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/classes.py b/classes.py index a622b74..2432ff6 100644 --- a/classes.py +++ b/classes.py @@ -26,7 +26,9 @@ class Irc(): self.lastping = time.time() # Server, channel, and user indexes to be populated by our protocol module - self.servers = {self.sid: IrcServer(None, self.serverdata['hostname'], internal=True)} + self.servers = {self.sid: IrcServer(None, self.serverdata['hostname'], + internal=True, desc=self.serverdata.get('serverdesc') + or self.botdata['serverdesc'])} self.users = {} self.channels = defaultdict(IrcChannel) # Sets flags such as whether to use halfops, etc. The default RFC1459 @@ -307,11 +309,12 @@ class IrcServer(): name: The name of the server. internal: Whether the server is an internal PyLink PseudoServer. """ - def __init__(self, uplink, name, internal=False): + def __init__(self, uplink, name, internal=False, desc="(None given)"): self.uplink = uplink self.users = set() self.internal = internal self.name = name.lower() + self.desc = desc def __repr__(self): return repr(self.__dict__) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 1d17550..466b685 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -285,7 +285,7 @@ class InspIRCdProtocol(TS6BaseProtocol): if not utils.isServerName(name): raise ValueError('Invalid server name %r' % name) self._send(uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc)) - self.irc.servers[sid] = IrcServer(uplink, name, internal=True) + self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc) self._send(sid, 'ENDBURST') return sid @@ -329,7 +329,8 @@ class InspIRCdProtocol(TS6BaseProtocol): if args[2] != self.irc.serverdata['recvpass']: # Check if recvpass is correct raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) - self.irc.servers[numeric] = IrcServer(None, servername) + sdesc = ' '.join(args).split(':', 1)[1] + self.irc.servers[numeric] = IrcServer(None, servername, desc=sdesc) self.irc.uplink = numeric return elif args[0] == 'CAPAB': @@ -459,7 +460,7 @@ class InspIRCdProtocol(TS6BaseProtocol): servername = args[0].lower() sid = args[3] sdesc = args[-1] - self.irc.servers[sid] = IrcServer(numeric, servername) + self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc) return {'name': servername, 'sid': args[3], 'text': sdesc} def handle_fmode(self, numeric, command, args): diff --git a/protocols/ts6.py b/protocols/ts6.py index 513d2cc..ff222c9 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -276,7 +276,7 @@ class TS6Protocol(TS6BaseProtocol): if not utils.isServerName(name): raise ValueError('Invalid server name %r' % name) self._send(uplink, 'SID %s 1 %s :%s' % (name, sid, desc)) - self.irc.servers[sid] = IrcServer(uplink, name, internal=True) + self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc) return sid def squitServer(self, source, target, text='No reason given'): @@ -407,6 +407,7 @@ class TS6Protocol(TS6BaseProtocol): sname = args[1].lower() log.debug('(%s) Found uplink server name as %r', self.irc.name, sname) self.irc.servers[self.irc.uplink].name = sname + self.irc.servers[self.irc.uplink].desc = ' '.join(args).split(':', 1)[1] # According to the TS6 protocol documentation, we should send SVINFO # when we get our uplink's SERVER command. self.irc.send('SVINFO 6 6 0 :%s' % int(time.time())) @@ -593,7 +594,7 @@ class TS6Protocol(TS6BaseProtocol): # XXX: don't just save these by their server names; that's ugly! sid = servername sdesc = args[-1] - self.irc.servers[sid] = IrcServer(numeric, servername) + self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc) return {'name': servername, 'sid': sid, 'text': sdesc} handle_sid = handle_server From 36b0a08368ebca8940ed08b74c953a1827422873 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 12:05:26 -0700 Subject: [PATCH 04/70] ts6_common: return server name in SQUIT handler too --- protocols/ts6_common.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/protocols/ts6_common.py b/protocols/ts6_common.py index a686f5a..ad52ad6 100644 --- a/protocols/ts6_common.py +++ b/protocols/ts6_common.py @@ -81,11 +81,12 @@ class TS6BaseProtocol(Protocol): # Clear empty non-permanent channels. if not (self.irc.channels[c].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[c].modes)): del self.irc.channels[c] + assert numeric not in v.users, "IrcChannel's removeuser() is broken!" sid = numeric[:3] log.debug('Removing client %s from self.irc.users', numeric) del self.irc.users[numeric] - log.debug('Removing client %s from self.irc.servers[%s]', numeric, sid) + log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid) self.irc.servers[sid].users.discard(numeric) def partClient(self, client, channel, reason=None): @@ -211,9 +212,10 @@ class TS6BaseProtocol(Protocol): affected_users.append(user) log.debug('Removing client %s (%s)', user, self.irc.users[user].nick) self.removeClient(user) + sname = self.irc.servers[split_server].name del self.irc.servers[split_server] log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users) - return {'target': split_server, 'users': affected_users} + return {'target': split_server, 'users': affected_users, 'name': sname} def handle_topic(self, numeric, command, args): """Handles incoming TOPIC changes from clients. For topic bursts, From 0cbdefcab117c8446fcb9f4e749629cae6f2bf67 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 12:05:53 -0700 Subject: [PATCH 05/70] ts6/inspircd: fix TS6SIDGenerator not actually incrementing... It was being replaced on every call... It was only supposed to do that if sidgen hadn't been set yet! --- protocols/inspircd.py | 4 ++-- protocols/ts6.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 466b685..63c0cb0 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -24,6 +24,7 @@ class InspIRCdProtocol(TS6BaseProtocol): self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE', 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'} + self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) 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): @@ -272,8 +273,7 @@ class InspIRCdProtocol(TS6BaseProtocol): # "desc" defaults to the configured server description. desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'] if sid is None: # No sid given; generate one! - self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) - sid = self.irc.sidgen.next_sid() + sid = self.sidgen.next_sid() assert len(sid) == 3, "Incorrect SID length" if sid in self.irc.servers: raise ValueError('A server with SID %r already exists!' % sid) diff --git a/protocols/ts6.py b/protocols/ts6.py index ff222c9..6b65807 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -17,6 +17,7 @@ class TS6Protocol(TS6BaseProtocol): super(TS6Protocol, self).__init__(irc) self.casemapping = 'rfc1459' self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} + self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) 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): @@ -263,8 +264,7 @@ class TS6Protocol(TS6BaseProtocol): name = name.lower() desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'] if sid is None: # No sid given; generate one! - self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) - sid = self.irc.sidgen.next_sid() + sid = self.sidgen.next_sid() assert len(sid) == 3, "Incorrect SID length" if sid in self.irc.servers: raise ValueError('A server with SID %r already exists!' % sid) From 95b70f3ebfb8f64e8cfa9ad7ef8242d6f55fd684 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 12:06:58 -0700 Subject: [PATCH 06/70] relay: spawn a server for every linked network (Closes #105) --- plugins/relay.py | 78 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 3e563f6..55b9ad4 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -21,7 +21,8 @@ if confname != 'pylink': dbname += '.db' relayusers = defaultdict(dict) -spawnlocks = defaultdict(threading.Lock) +relayservers = defaultdict(dict) +spawnlocks = defaultdict(threading.RLock) savecache = ExpiringDict(max_len=5, max_age_seconds=10) def relayWhoisHandler(irc, target): @@ -125,6 +126,17 @@ def getPrefixModes(irc, remoteirc, channel, user): modes += remoteirc.cmodes[pmode] return modes +def getRemoteSid(irc, remoteirc): + """Get the remote server SID representing remoteirc on irc, spawning + it if it doesn't exist.""" + with spawnlocks[irc.name]: + try: + sid = relayservers[irc.name][remoteirc.name] + except KeyError: + sid = irc.proto.spawnServer('%s.relay' % remoteirc.name) + relayservers[irc.name][remoteirc.name] = sid + return sid + def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): # If the user (stored here as {('netname', 'UID'): # {'network1': 'UID1', 'network2': 'UID2'}}) exists, don't spawn it @@ -172,10 +184,11 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): hideoper_mode = remoteirc.umodes.get('hideoper') if hideoper_mode: modes.append((hideoper_mode, None)) + rsid = getRemoteSid(remoteirc, irc) u = remoteirc.proto.spawnClient(nick, ident=ident, host=host, realname=realname, modes=modes, ts=userobj.ts, - opertype=opertype).uid + opertype=opertype, server=rsid).uid remoteirc.users[u].remote = (irc.name, user) remoteirc.users[u].opertype = opertype away = userobj.away @@ -249,7 +262,6 @@ def findRemoteChan(irc, remoteirc, channel): def initializeChannel(irc, channel): # We're initializing a relay that already exists. This can be done at # ENDBURST, or on the LINK command. - c = irc.channels[channel] relay = findRelay((irc.name, channel)) log.debug('(%s) initializeChannel being called on %s', irc.name, channel) log.debug('(%s) initializeChannel: relay pair found to be %s', irc.name, relay) @@ -275,10 +287,10 @@ def initializeChannel(irc, channel): # Only update the topic if it's different from what we already have, # and topic bursting is complete. if remoteirc.channels[remotechan].topicset and topic != irc.channels[channel].topic: - irc.proto.topicServer(irc.sid, channel, topic) + irc.proto.topicServer(getRemoteSid(irc, remoteirc), channel, topic) # Send our users and channel modes to the other nets - log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users) - relayJoins(irc, channel, c.users, c.ts) + log.debug('(%s) initializeChannel: joining our (%s) users: %s', irc.name, remotenet, irc.channels[channel].users) + relayJoins(irc, channel, irc.channels[channel].users, irc.channels[channel].ts) irc.proto.joinClient(irc.pseudoclient.uid, channel) def handle_join(irc, numeric, command, args): @@ -300,9 +312,23 @@ utils.add_hook(handle_quit, 'QUIT') def handle_squit(irc, numeric, command, args): users = args['users'] - for user in users: - log.debug('(%s) relay handle_squit: sending handle_quit on %s', irc.name, user) - handle_quit(irc, user, command, {'text': '*.net *.split'}) + target = args['target'] + # Someone /SQUIT one of our relay subservers. Bad! Rejoin them! + if target in relayservers[irc.name].values(): + sname = args['name'] + remotenet = sname.split('.', 1)[0] + del relayservers[irc.name][remotenet] + for userpair in relayusers: + if userpair[0] == remotenet and irc.name in relayusers[userpair]: + del relayusers[userpair][irc.name] + remoteirc = world.networkobjects[remotenet] + initializeAll(remoteirc) + else: + # Some other netsplit happened on the network, we'll have to fake + # some *.net *.split quits for that. + for user in users: + log.debug('(%s) relay handle_squit: sending handle_quit on %s', irc.name, user) + handle_quit(irc, user, command, {'text': '*.net *.split'}) utils.add_hook(handle_squit, 'SQUIT') def handle_nick(irc, numeric, command, args): @@ -431,7 +457,7 @@ def handle_kick(irc, source, command, args): # kick ops, admins can't kick owners, etc. modes = getPrefixModes(remoteirc, irc, remotechan, real_target) # Join the kicked client back with its respective modes. - irc.proto.sjoinServer(irc.sid, channel, [(modes, target)]) + irc.proto.sjoinServer(getRemoteSid(irc, remoteirc), channel, [(modes, target)]) if kicker in irc.users: log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.', irc.name, args['text'], irc.users[source].nick, @@ -455,7 +481,8 @@ def handle_kick(irc, source, command, args): else: # Kick originated from a server, or the kicker isn't in any # common channels with the target relay network. - log.debug('(%s) Relay kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan, remoteirc.sid, kicker, irc.name) + rsid = getRemoteSid(remoteirc, irc) + log.debug('(%s) Relay kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan, rsid, kicker, irc.name) try: if kicker in irc.servers: kname = irc.servers[kicker].name @@ -464,8 +491,7 @@ def handle_kick(irc, source, command, args): text = "(%s/%s) %s" % (kname, irc.name, text) except AttributeError: text = "(@%s) %s" % (irc.name, text) - remoteirc.proto.kickServer(remoteirc.sid, - remotechan, real_target, text) + remoteirc.proto.kickServer(rsid, remotechan, real_target, text) # If the target isn't on any channels, quit them. if origuser and origuser[0] != remoteirc.name and not remoteirc.users[real_target].channels: @@ -586,7 +612,8 @@ def relayModes(irc, remoteirc, sender, channel, modes=None): if u: remoteirc.proto.modeClient(u, remotechan, supported_modes) else: - remoteirc.proto.modeServer(remoteirc.sid, remotechan, supported_modes) + rsid = getRemoteSid(remoteirc, irc) + remoteirc.proto.modeServer(rsid, remotechan, supported_modes) def getSupportedUmodes(irc, remoteirc, modes): supported_modes = [] @@ -656,7 +683,8 @@ def handle_topic(irc, numeric, command, args): if remoteuser: remoteirc.proto.topicClient(remoteuser, remotechan, topic) else: - remoteirc.proto.topicServer(remoteirc.sid, remotechan, topic) + rsid = getRemoteSid(remoteirc, irc) + remoteirc.proto.topicServer(rsid, remotechan, topic) utils.add_hook(handle_topic, 'TOPIC') def handle_kill(irc, numeric, command, args): @@ -676,7 +704,7 @@ def handle_kill(irc, numeric, command, args): modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser) client = getRemoteUser(remoteirc, irc, realuser[1]) - irc.proto.sjoinServer(irc.sid, localchan, [(modes, client)]) + irc.proto.sjoinServer(getRemoteSid(irc, remoteirc), localchan, [(modes, client)]) if userdata and numeric in irc.users: log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.', irc.name, args['text'], irc.users[numeric].nick, @@ -706,8 +734,10 @@ def isRelayClient(irc, user): # Is the .remote attribute set? If so, don't relay already # relayed clients; that'll trigger an endless loop! return True - except (KeyError, AttributeError): # Nope, it isn't. + except AttributeError: # Nope, it isn't. pass + except KeyError: # The user doesn't exist?!? + return True return False def relayJoins(irc, channel, users, ts, burst=True): @@ -745,8 +775,9 @@ def relayJoins(irc, channel, users, ts, burst=True): # Burst was explicitly given, or we're trying to join multiple # users/someone with a prefix. if burst or len(queued_users) > 1 or queued_users[0][0]: - remoteirc.proto.sjoinServer(remoteirc.sid, remotechan, queued_users, ts=ts) - relayModes(irc, remoteirc, irc.sid, channel, irc.channels[channel].modes) + rsid = getRemoteSid(remoteirc, irc) + remoteirc.proto.sjoinServer(rsid, remotechan, queued_users, ts=ts) + relayModes(irc, remoteirc, getRemoteSid(irc, remoteirc), channel, irc.channels[channel].modes) else: remoteirc.proto.joinClient(queued_users[0][1], remotechan) @@ -964,7 +995,14 @@ def handle_disconnect(irc, numeric, command, args): if irc.name in v: del relayusers[k][irc.name] if k[0] == irc.name: - handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Home network lost connection.'}) + del relayusers[k] + for name, ircobj in world.networkobjects.items(): + if name != irc.name: + rsid = getRemoteSid(ircobj, irc) + ircobj.proto.squitServer(ircobj.sid, rsid, text='Home network lost connection.') + del relayservers[name][irc.name] + del relayservers[irc.name] + # handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Home network lost connection.'}) utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT") From 3a8d3d146c92ccaa494ca2896d26bddca7c65c4d Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 16:02:51 -0700 Subject: [PATCH 07/70] protocols: make "uidgen" a protocol instance attribute --- classes.py | 3 --- protocols/inspircd.py | 9 ++++----- protocols/ts6.py | 9 ++++----- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/classes.py b/classes.py index 2432ff6..1211088 100644 --- a/classes.py +++ b/classes.py @@ -57,9 +57,6 @@ class Irc(): self.uplink = None self.start_ts = int(time.time()) - # UID generators, for servers that need it - self.uidgen = {} - def __init__(self, netname, proto): # Initialize some variables self.name = netname.lower() diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 63c0cb0..f118ba9 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -25,6 +25,7 @@ class InspIRCdProtocol(TS6BaseProtocol): 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'} self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) + self.uidgen = {} 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): @@ -35,11 +36,9 @@ class InspIRCdProtocol(TS6BaseProtocol): server = server or self.irc.sid if not utils.isInternalServer(self.irc, server): raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) - # We need a separate UID generator instance for every PseudoServer - # we spawn. Otherwise, things won't wrap around properly. - if server not in self.irc.uidgen: - self.irc.uidgen[server] = utils.TS6UIDGenerator(server) - uid = self.irc.uidgen[server].next_uid() + # Create an UIDGenerator instance for every SID, so that each gets + # distinct values. + 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 diff --git a/protocols/ts6.py b/protocols/ts6.py index 6b65807..c2b7b6b 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -18,6 +18,7 @@ class TS6Protocol(TS6BaseProtocol): self.casemapping = 'rfc1459' self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) + self.uidgen = {} 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): @@ -28,11 +29,9 @@ class TS6Protocol(TS6BaseProtocol): server = server or self.irc.sid if not utils.isInternalServer(self.irc, server): raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) - # We need a separate UID generator instance for every PseudoServer - # we spawn. Otherwise, things won't wrap around properly. - if server not in self.irc.uidgen: - self.irc.uidgen[server] = utils.TS6UIDGenerator(server) - uid = self.irc.uidgen[server].next_uid() + # Create an UIDGenerator instance for every SID, so that each gets + # distinct values. + uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(server)).next_uid() # EUID: # parameters: nickname, hopcount, nickTS, umodes, username, # visible hostname, IP address, UID, real hostname, account name, gecos From 1c97927b5d08d5abff3016b233ce4d4ac6b11ca5 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 16:03:17 -0700 Subject: [PATCH 08/70] relay: monitor SAVE floods by network, not by target nick --- plugins/relay.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 55b9ad4..b545bb9 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -1020,7 +1020,7 @@ def handle_save(irc, numeric, command, args): nick = remoteirc.users[remoteuser].nick # Limit how many times we can attempt to fix our nick, to prevent # floods and such. - if savecache.setdefault(target, 0) <= 5: + if savecache.setdefault(irc.name, 0) <= 5: newnick = normalizeNick(irc, remotenet, nick) log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s', irc.name, target, nick, newnick) @@ -1028,8 +1028,8 @@ def handle_save(irc, numeric, command, args): else: log.warning('(%s) SAVE received for relay client %r (%s), not ' 'fixing nick again due to 5 failed attempts in ' - 'the last 10 seconds!', irc.name, target, nick) - savecache[target] += 1 + 'the last 10 seconds!', irc.name, target, nick) + savecache[irc.name] += 1 else: # Somebody else on the network (not a PyLink client) had a nick collision; # relay this as a nick change appropriately. From 1fcacd0d7c5557a2163fa65e8f31e04adbac1c5f Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 16:03:59 -0700 Subject: [PATCH 09/70] relay: more compact "showuser" output --- plugins/relay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index b545bb9..3e0f901 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -1157,12 +1157,12 @@ def showuser(irc, source, args): else: nicks = [] if remoteusers: - nicks.append('%s (home network): \x02%s\x02' % (userpair[0], + nicks.append('%s:\x02%s\x02' % (userpair[0], world.networkobjects[userpair[0]].users[userpair[1]].nick)) for r in remoteusers: remotenet, remoteuser = r remoteirc = world.networkobjects[remotenet] - nicks.append('%s: \x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick)) + nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick)) irc.msg(source, "\x02Relay nicks\x02: %s" % ', '.join(nicks)) relaychannels = [] for ch in irc.users[u].channels: From b3902c7711afd86bc63033e3e9a8f5b02b3955b9 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 17:41:49 -0700 Subject: [PATCH 10/70] relay: KILL war protection (#23) TODO: possibly integrate this into core? --- plugins/relay.py | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 3e0f901..fc722be 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -24,6 +24,7 @@ relayusers = defaultdict(dict) relayservers = defaultdict(dict) spawnlocks = defaultdict(threading.RLock) savecache = ExpiringDict(max_len=5, max_age_seconds=10) +killcache = ExpiringDict(max_len=5, max_age_seconds=10) def relayWhoisHandler(irc, target): user = irc.users[target] @@ -697,26 +698,34 @@ def handle_kill(irc, numeric, command, args): # We don't allow killing over the relay, so we must respawn the affected # client and rejoin it to its channels. del relayusers[realuser][irc.name] - remoteirc = world.networkobjects[realuser[0]] - for remotechan in remoteirc.channels.copy(): - localchan = findRemoteChan(remoteirc, irc, remotechan) - if localchan: - modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) - log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser) - client = getRemoteUser(remoteirc, irc, realuser[1]) - irc.proto.sjoinServer(getRemoteSid(irc, remoteirc), localchan, [(modes, client)]) - if userdata and numeric in irc.users: - log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.', - irc.name, args['text'], irc.users[numeric].nick, - remoteirc.users[realuser[1]].nick, realuser[0]) - irc.msg(numeric, "Your kill to %s has been blocked " - "because PyLink does not allow killing" - " users over the relay at this time." % \ - userdata.nick, notice=True) + if killcache.setdefault(irc.name, 0) <= 5: + remoteirc = world.networkobjects[realuser[0]] + for remotechan in remoteirc.channels.copy(): + localchan = findRemoteChan(remoteirc, irc, remotechan) + if localchan: + modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) + log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser) + client = getRemoteUser(remoteirc, irc, realuser[1]) + irc.proto.sjoinServer(getRemoteSid(irc, remoteirc), localchan, [(modes, client)]) + if userdata and numeric in irc.users: + log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.', + irc.name, args['text'], irc.users[numeric].nick, + remoteirc.users[realuser[1]].nick, realuser[0]) + irc.msg(numeric, "Your kill to %s has been blocked " + "because PyLink does not allow killing" + " users over the relay at this time." % \ + userdata.nick, notice=True) + else: + log.info('(%s) Relay claim: Blocked KILL (reason %r) from server %s to relay client %s/%s.', + irc.name, args['text'], irc.servers[numeric].name, + remoteirc.users[realuser[1]].nick, realuser[0]) else: - log.info('(%s) Relay claim: Blocked KILL (reason %r) from server %s to relay client %s/%s.', - irc.name, args['text'], irc.servers[numeric].name, - remoteirc.users[realuser[1]].nick, realuser[0]) + log.error('(%s) Too many kills received for target %s, aborting!', + irc.name, userdata.nick) + irc.aborted.set() + killcache[irc.name] += 1 + log.debug('killcache: %s', irc.name, killcache) + # Target user was local. else: # IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages From 46fab1cab70def1f847334782cf817e3cde76072 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 22:26:38 -0700 Subject: [PATCH 11/70] classes.FakeProto: store UIDs as strings --- classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes.py b/classes.py index 1211088..9216063 100644 --- a/classes.py +++ b/classes.py @@ -400,7 +400,7 @@ class FakeProto(Protocol): pass def spawnClient(self, nick, *args, **kwargs): - uid = randint(1, 10000000000) + uid = str(randint(1, 10000000000)) ts = int(time.time()) self.irc.users[uid] = user = IrcUser(nick, ts, uid) return user From 4b7ef449250b3dad7c5a1be5ed322d031bba02bd Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 22:27:52 -0700 Subject: [PATCH 12/70] runtests: allow specifying files to test --- runtests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtests.py b/runtests.py index 5b64f8b..b4d7996 100755 --- a/runtests.py +++ b/runtests.py @@ -11,7 +11,10 @@ suites = [] # Yay, import hacks! sys.path.append(os.path.join(os.getcwd(), 'tests')) -for testfile in glob.glob('tests/test_*.py'): + +query = sys.argv[1:] or glob.glob('tests/test_*.py') + +for testfile in query: # Strip the tests/ and .py extension: tests/test_whatever.py => test_whatever module = testfile.replace('.py', '').replace('tests/', '') module = __import__(module) From f23cff845ca05d1c4013fd52a703b4ed6fd93905 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 22:28:34 -0700 Subject: [PATCH 13/70] utils: Documentation and cleanup --- utils.py | 76 +++++++++++++++++++++++--------------------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/utils.py b/utils.py index b5f3ae2..6ea1359 100644 --- a/utils.py +++ b/utils.py @@ -40,10 +40,9 @@ class TS6UIDGenerator(): return uid class TS6SIDGenerator(): - """ - + """ TS6 SID Generator. is a 3 character string with any combination of - uppercase letters, digits, and #'s. must contain at least one #, + uppercase letters, digits, and #'s. it must contain at least one #, which are used by the generator as a wildcard. On every next_sid() call, the first available wildcard character (from the right) will be incremented to generate the next SID. @@ -110,10 +109,8 @@ def add_hook(func, command): world.command_hooks[command].append(func) def toLower(irc, text): - """ - - Returns a lowercase representation of based on 's - casemapping (rfc1459 vs ascii).""" + """Returns a lowercase representation of text based on the IRC object's + casemapping (rfc1459 or ascii).""" if irc.proto.casemapping == 'rfc1459': text = text.replace('{', '[') text = text.replace('}', ']') @@ -122,39 +119,42 @@ def toLower(irc, text): return text.lower() def nickToUid(irc, nick): - """ - - Returns the UID of a user named , if present.""" + """Returns the UID of a user named nick, if present.""" nick = toLower(irc, nick) for k, v in irc.users.items(): if toLower(irc, v.nick) == nick: return k def clientToServer(irc, numeric): - """ - - Finds the server SID of user and returns it.""" + """Finds the SID of the server a user is on.""" for server in irc.servers: if numeric in irc.servers[server].users: return server -# A+ regex _nickregex = r'^[A-Za-z\|\\_\[\]\{\}\^\`][A-Z0-9a-z\-\|\\_\[\]\{\}\^\`]*$' def isNick(s, nicklen=None): + """Checks whether the string given is a valid nick.""" if nicklen and len(s) > nicklen: return False return bool(re.match(_nickregex, s)) def isChannel(s): - return s.startswith('#') + """Checks whether the string given is a valid channel name.""" + return str(s).startswith('#') def _isASCII(s): chars = string.ascii_letters + string.digits + string.punctuation return all(char in chars for char in s) def isServerName(s): + """Checks whether the string given is a server name.""" return _isASCII(s) and '.' in s and not s.startswith('.') +hostmaskRe = re.compile(r'^\S+!\S+@\S+$') +def isHostmask(text): + """Returns whether the given text is a valid hostmask.""" + return bool(hostmaskRe.match(text)) + def parseModes(irc, target, args): """Parses a modestring list into a list of (mode, argument) tuples. ['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')] @@ -220,10 +220,9 @@ def parseModes(irc, target, args): return res def applyModes(irc, target, changedmodes): - """ + """Takes a list of parsed IRC modes, and applies them on the given target. - Takes a list of parsed IRC modes (, in the format of parseModes()), and applies them on . - can be either a channel or a user; this is handled automatically.""" + The target can be either a channel or a user; this is handled automatically.""" usermodes = not isChannel(target) log.debug('(%s) Using usermodes for this query? %s', irc.name, usermodes) if usermodes: @@ -292,11 +291,10 @@ def applyModes(irc, target, changedmodes): irc.channels[target].modes = modelist def joinModes(modes): - """ + """Takes a list of (mode, arg) tuples in parseModes() format, and + joins them into a string. - Takes a list of (mode, arg) tuples in parseModes() format, and - joins them into a string. See testJoinModes in tests/test_utils.py - for some examples.""" + See testJoinModes in tests/test_utils.py for some examples.""" prefix = '+' # Assume we're adding modes unless told otherwise modelist = '' args = [] @@ -357,26 +355,21 @@ def reverseModes(irc, target, modes): return newmodes def isInternalClient(irc, numeric): - """ - - Checks whether is a PyLink PseudoClient, - returning the SID of the PseudoClient's server if True. + """ + Checks whether the given numeric is a PyLink Client, + returning the SID of the server it's on if so. """ for sid in irc.servers: if irc.servers[sid].internal and numeric in irc.servers[sid].users: return sid def isInternalServer(irc, sid): - """ - - Returns whether is an internal PyLink PseudoServer. - """ + """Returns whether the given SID is an internal PyLink server.""" return (sid in irc.servers and irc.servers[sid].internal) def isOper(irc, uid, allowAuthed=True, allowOper=True): - """ - - Returns whether has operator status on PyLink. This can be achieved + """ + Returns whether the given user has operator status on PyLink. This can be achieved by either identifying to PyLink as admin (if allowAuthed is True), or having user mode +o set (if allowOper is True). At least one of allowAuthed or allowOper must be True for this to give any meaningful @@ -390,10 +383,10 @@ def isOper(irc, uid, allowAuthed=True, allowOper=True): return False def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True): - """ - - Checks whether user has operator status on PyLink, raising - NotAuthenticatedError and logging the access denial if not.""" + """ + Checks whetherthe given user has operator status on PyLink, raising + NotAuthenticatedError and logging the access denial if not. + """ lastfunc = inspect.stack()[1][3] if not isOper(irc, uid, allowAuthed=allowAuthed, allowOper=allowOper): log.warning('(%s) Access denied for %s calling %r', irc.name, @@ -402,9 +395,7 @@ def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True): return True def getHostmask(irc, user): - """ - - Gets the hostmask of user , if present.""" + """Gets the hostmask of the given user, if present.""" userobj = irc.users.get(user) if userobj is None: return '' @@ -421,8 +412,3 @@ def getHostmask(irc, user): except AttributeError: host = '' return '%s!%s@%s' % (nick, ident, host) - -hostmaskRe = re.compile(r'^\S+!\S+@\S+$') -def isHostmask(text): - """Returns whether is a valid hostmask.""" - return bool(hostmaskRe.match(text)) From dab29cfc19447c4c239c135bd7963f396edeeda8 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 22:29:53 -0700 Subject: [PATCH 14/70] utils: add a proper reverseModes() with tests Closes #107. --- tests/test_utils.py | 40 ++++++++++++++++++----- utils.py | 79 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 94a0194..d2d34f2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -101,19 +101,43 @@ class TestUtils(unittest.TestCase): ('+b', '*!*@*.badisp.net')]) self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.badisp.net') - @unittest.skip('Wait, we need to work out the kinks first! (reversing changes of modes with arguments)') + def _reverseModes(self, query, expected, target='#test'): + res = utils.reverseModes(self.irc, target, query) + self.assertEqual(res, expected) + def testReverseModes(self): - f = lambda x: utils.reverseModes(self.irc, '#test', x) + test = lambda x, y: self.assertEqual(utils.reverseModes(self.irc, '#test', x), y) # Strings. - self.assertEqual(f("+nt-lk"), "-nt+lk") - self.assertEqual(f("nt-k"), "-nt+k") + self._reverseModes("+mi-lk test", "-mi+lk test") + self._reverseModes("mi-k test", "-mi+k test") # Lists. - self.assertEqual(f([('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')]), - [('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')]) + self._reverseModes([('+m', None), ('+r', None), ('+l', '3'), ('-o', 'person')], + {('-m', None), ('-r', None), ('-l', None), ('+o', 'person')}) # Sets. - self.assertEqual(f({('s', None), ('+o', 'whoever')}), {('-s', None), ('-o', 'whoever')}) + self._reverseModes({('s', None), ('+o', 'whoever')}, {('-s', None), ('-o', 'whoever')}) # Combining modes with an initial + and those without - self.assertEqual(f({('s', None), ('+n', None)}), {('-s', None), ('-n', None)}) + self._reverseModes({('s', None), ('+R', None)}, {('-s', None), ('-R', None)}) + + def testReverseModesUser(self): + self._reverseModes({('+i', None), ('l', 'asfasd')}, {('-i', None), ('-l', 'asfasd')}, + target=self.irc.pseudoclient.uid) + + def testReverseModesExisting(self): + utils.applyModes(self.irc, '#test', [('+m', None), ('+l', '50'), ('+k', 'supersecret'), + ('+o', '9PYAAAAAA')]) + + self._reverseModes({('+i', None), ('+l', '3')}, {('-i', None), ('+l', '50')}) + self._reverseModes('-n', '+n') + self._reverseModes('-l', '+l 50') + self._reverseModes('+k derp', '+k supersecret') + self._reverseModes('-mk *', '+mk supersecret') + + # Existing modes are ignored. + self._reverseModes([('+t', None)], set()) + self._reverseModes('+n', '+') + self._reverseModes('+oo GLolol 9PYAAAAAA', '-o GLolol') + self._reverseModes('+o 9PYAAAAAA', '+') + self._reverseModes('+vvvvM test abcde atat abcd', '-vvvvM test abcde atat abcd') if __name__ == '__main__': unittest.main() diff --git a/utils.py b/utils.py index 6ea1359..39c8805 100644 --- a/utils.py +++ b/utils.py @@ -326,10 +326,20 @@ def joinModes(modes): modelist += ' %s' % ' '.join(args) return modelist -def reverseModes(irc, target, modes): - """ +def _flip(mode): + """Flips a mode character.""" + # Make it a list first, strings don't support item assignment + mode = list(mode) + if mode[0] == '-': # Query is something like "-n" + mode[0] = '+' # Change it to "+n" + elif mode[0] == '+': + mode[0] = '-' + else: # No prefix given, assume + + mode.insert(0, '-') + return ''.join(mode) - Reverses/Inverts the mode string or mode list given. +def reverseModes(irc, target, modes): + """Reverses/Inverts the mode string or mode list given. "+nt-lk" => "-nt+lk" "nt-k" => "-nt+k" @@ -338,21 +348,56 @@ def reverseModes(irc, target, modes): [('s', None), ('+n', None)] => [('-s', None), ('-n', None)] """ origtype = type(modes) - # Operate on joined modestrings only; it's easier. - if origtype != str: - modes = joinModes(modes) - # Swap the +'s and -'s by replacing one with a dummy character, and then changing it back. - assert '\x00' not in modes, 'NUL cannot be in the mode list (it is a reserved character)!' - if not modes.startswith(('+', '-')): - modes = '+' + modes - newmodes = modes.replace('+', '\x00') - newmodes = newmodes.replace('-', '+') - newmodes = newmodes.replace('\x00', '-') - if origtype != str: - # If the original query isn't a string, send back the parseModes() output. - return parseModes(irc, target, newmodes.split(" ")) + # If the query is a string, we have to parse it first. + if origtype == str: + modes = parseModes(irc, target, modes.split(" ")) + # Get the current mode list first. + if isChannel(target): + oldmodes = irc.channels[target].modes.copy() + possible_modes = irc.cmodes.copy() + # For channels, this also includes the list of prefix modes. + possible_modes['*A'] += ''.join(irc.prefixmodes) + for name, userlist in irc.channels[target].prefixmodes.items(): + try: + oldmodes.update([(irc.cmodes[name[:-1]], u) for u in userlist]) + except KeyError: + continue else: - return newmodes + oldmodes = irc.users[target].modes + possible_modes = irc.umodes + newmodes = [] + for char, arg in modes: + # Mode types: + # A = Mode that adds or removes a nick or address to a list. Always has a parameter. + # B = Mode that changes a setting and always has a parameter. + # C = Mode that changes a setting and only has a parameter when set. + # D = Mode that changes a setting and never has a parameter. + mchar = char[-1] + if mchar in possible_modes['*B'] + possible_modes['*C']: + # We need to find the current mode list, so we can reset arguments + # for modes that have arguments. For example, setting +l 30 on a channel + # that had +l 50 set should give "+l 30", not "-l". + oldarg = [m for m in oldmodes if m[0] == mchar] + if oldarg: # Old mode argument for this mode existed, use that. + oldarg = oldarg[0] + mpair = ('+%s' % oldarg[0], oldarg[1]) + else: # Not found, flip the mode then. + # Mode takes no arguments when unsetting. + if mchar in possible_modes['*C'] and char[0] != '-': + arg = None + mpair = (_flip(char), arg) + else: + mpair = (_flip(char), arg) + if char[0] != '-' and (mchar, arg) in oldmodes: + # Mode is already set. + continue + newmodes.append(mpair) + + if origtype == str: + # If the original query is a string, send it back as a string. + return joinModes(newmodes) + else: + return set(newmodes) def isInternalClient(irc, numeric): """ From dfaa5036ab93490337832013da710e9189a39884 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 22:34:55 -0700 Subject: [PATCH 15/70] Fix test cases - conf: add sidrange server config variable (needed by TS6SIDGenerator) - protocols: stop hardcoding various UIDs that may or may not exist. --- conf.py | 3 ++- tests/test_proto_inspircd.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/conf.py b/conf.py index 9ee2d81..93067a4 100644 --- a/conf.py +++ b/conf.py @@ -27,7 +27,8 @@ testconf = {'bot': 'hostname': "pylink.unittest", 'sid': "9PY", 'channels': ["#pylink"], - 'maxnicklen': 20 + 'maxnicklen': 20, + 'sidrange': '8##' }) } if world.testing: diff --git a/tests/test_proto_inspircd.py b/tests/test_proto_inspircd.py index fdc8ec2..ab1d88b 100644 --- a/tests/test_proto_inspircd.py +++ b/tests/test_proto_inspircd.py @@ -174,8 +174,8 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase): hookdata = self.irc.takeHooks() expected = [['70M', 'FJOIN', {'channel': '#pylink', 'ts': 123, 'modes': [('+n', None)], 'users': ['10XAAAAAA', '10XAAAAAB']}], - ['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50'), ('+o', '9PYAAAAAA'), ('+t', None)], 'ts': 123}], - ['70M', 'FMODE', {'target': '#pylink', 'modes': [('-o', '9PYAAAAAA')], 'ts': 123}]] + ['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50'), ('+o', self.u), ('+t', None)], 'ts': 123}], + ['70M', 'FMODE', {'target': '#pylink', 'modes': [('-o', self.u)], 'ts': 123}]] self.assertEqual(expected, hookdata) def testHandleFModeRemovesOldParams(self): @@ -238,24 +238,24 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase): self.assertIn('00C', self.irc.servers) def testHandleNick(self): - self.irc.run(':9PYAAAAAA NICK PyLink-devel 1434744242') + self.irc.run(':%s NICK PyLink-devel 1434744242' % self.u) hookdata = self.irc.takeHooks()[0][-1] expected = {'newnick': 'PyLink-devel', 'oldnick': 'PyLink', 'ts': 1434744242} self.assertEqual(hookdata, expected) - self.assertEqual('PyLink-devel', self.irc.users['9PYAAAAAA'].nick) + self.assertEqual('PyLink-devel', self.irc.users[self.u].nick) def testHandleSave(self): - self.irc.run(':9PYAAAAAA NICK Derp_ 1433728673') - self.irc.run(':70M SAVE 9PYAAAAAA 1433728673') + self.irc.run(':%s NICK Derp_ 1433728673' % self.u) + self.irc.run(':70M SAVE %s 1433728673' % self.u) hookdata = self.irc.takeHooks()[-1][-1] - self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'ts': 1433728673, 'oldnick': 'Derp_'}) - self.assertEqual('9PYAAAAAA', self.irc.users['9PYAAAAAA'].nick) + self.assertEqual(hookdata, {'target': self.u, 'ts': 1433728673, 'oldnick': 'Derp_'}) + self.assertEqual(self.u, self.irc.users[self.u].nick) def testHandleInvite(self): - self.irc.run(':10XAAAAAA INVITE 9PYAAAAAA #blah 0') + self.irc.run(':10XAAAAAA INVITE %s #blah 0' % self.u) hookdata = self.irc.takeHooks()[-1][-1] del hookdata['ts'] - self.assertEqual(hookdata, {'target': '9PYAAAAAA', 'channel': '#blah'}) + self.assertEqual(hookdata, {'target': self.u, 'channel': '#blah'}) def testHandleOpertype(self): self.irc.run('SERVER whatever. abcd 0 10X :Whatever Server - Hellas Planitia, Mars') From 945306af34fa4b2954f29bf561db08dcee868b8c Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 22:50:53 -0700 Subject: [PATCH 16/70] relay: make CLAIM checking a shared function --- plugins/relay.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index fc722be..e6924a5 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -416,12 +416,26 @@ def handle_privmsg(irc, numeric, command, args): utils.add_hook(handle_privmsg, 'PRIVMSG') utils.add_hook(handle_privmsg, 'NOTICE') +def checkClaim(irc, channel, sender): + """ + Checks whether the sender of a kick/mode change passes CLAIM checks for + a given channel. This returns True if any of the following criteria are met: + + 1) The originating network is in the CLAIM list for the relay in question. + 2) The sender is halfop or above in the channel. + 3) The sender is a PyLink client (checks are suppressed in this case). + """ + relay = findRelay((irc.name, channel)) + sender_modes = getPrefixModes(irc, irc, channel, sender) + return irc.name in db[relay]['claim'] or \ + any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ + or utils.isInternalClient(irc, sender) + def handle_kick(irc, source, command, args): channel = args['channel'] target = args['target'] text = args['text'] kicker = source - kicker_modes = getPrefixModes(irc, irc, channel, kicker) relay = findRelay((irc.name, channel)) # Don't allow kicks to the PyLink client to be relayed. if relay is None or target == irc.pseudoclient.uid: @@ -447,10 +461,7 @@ def handle_kick(irc, source, command, args): else: log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target) real_target = getLocalUser(irc, target, targetirc=remoteirc) - log.debug('(%s) Relay kick: kicker_modes are %r', irc.name, kicker_modes) - if irc.name not in db[relay]['claim'] and not \ - (any([mode in kicker_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ - or utils.isInternalClient(irc, kicker)): + if not checkClaim(irc, channel, kicker): log.debug('(%s) Relay kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target) # Home network is not in the channel's claim AND the kicker is not # opped. We won't propograte the kick then. @@ -464,8 +475,8 @@ def handle_kick(irc, source, command, args): irc.name, args['text'], irc.users[source].nick, remoteirc.users[real_target].nick, remoteirc.name, channel) irc.msg(kicker, "This channel is claimed; your kick to " - "%s has been blocked because you are not " - "(half)opped." % channel, notice=True) + "%s has been blocked because you are not " + "(half)opped." % channel, notice=True) else: log.info('(%s) Relay claim: Blocked KICK (reason %r) from server %s to relay client %s/%s on %s.', irc.name, args['text'], irc.servers[source].name, From 17282aa0497daa48139ae2a2daba7181f7c0db71 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 23:35:20 -0700 Subject: [PATCH 17/70] protocols: fix typo and ts6._sendModes when input is a set() --- protocols/inspircd.py | 2 +- protocols/ts6.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index f118ba9..2cc07fb 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -157,7 +157,7 @@ class InspIRCdProtocol(TS6BaseProtocol): """Internal function to send mode changes from a PyLink client/server.""" # -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA # -> :9PYAAAAAA MODE 9PYAAAAAA -i+w - log.debug('(%s) inspself.ircd._sendModes: received %r for mode list', self.irc.name, modes) + log.debug('(%s) inspircd._sendModes: received %r for mode list', self.irc.name, modes) if ('+o', None) in modes and not utils.isChannel(target): # https://github.com/inspself.ircd/inspself.ircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28 # Servers need a special command to set umode +o on people. diff --git a/protocols/ts6.py b/protocols/ts6.py index c2b7b6b..223bf42 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -135,6 +135,7 @@ class TS6Protocol(TS6BaseProtocol): def _sendModes(self, numeric, target, modes, ts=None): """Internal function to send mode changes from a PyLink client/server.""" utils.applyModes(self.irc, target, modes) + modes = list(modes) if utils.isChannel(target): ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts # TMODE: From fa4583c2726754cb527367aab7db8b9bf92df5bb Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 12 Sep 2015 23:36:52 -0700 Subject: [PATCH 18/70] relay: basic CLAIM checking when setting modes (#51) There's still some desyncs with checking op statuses, because hooks are only called AFTER the internal state updates. Fix for this will come soon. --- plugins/relay.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index e6924a5..70da041 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -423,13 +423,16 @@ def checkClaim(irc, channel, sender): 1) The originating network is in the CLAIM list for the relay in question. 2) The sender is halfop or above in the channel. - 3) The sender is a PyLink client (checks are suppressed in this case). + 3) The sender is a PyLink client/server (checks are suppressed in this case). + 4) No relay exists for the channel in question. + 5) The originating network is the one that created the relay. """ relay = findRelay((irc.name, channel)) sender_modes = getPrefixModes(irc, irc, channel, sender) - return irc.name in db[relay]['claim'] or \ + return (not relay) or irc.name == relay[0] or irc.name in db[relay]['claim'] or \ any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ - or utils.isInternalClient(irc, sender) + or utils.isInternalClient(irc, sender) or \ + utils.isInternalServer(irc, sender) def handle_kick(irc, source, command, args): channel = args['channel'] @@ -662,7 +665,14 @@ def handle_mode(irc, numeric, command, args): if irc.name == name or not remoteirc.connected.is_set(): continue if utils.isChannel(target): - relayModes(irc, remoteirc, numeric, target, modes) + if checkClaim(irc, target, numeric): + relayModes(irc, remoteirc, numeric, target, modes) + else: # Mode change blocked by CLAIM. + reversed_modes = utils.reverseModes(irc, target, modes) + log.debug('(%s) Reversing mode changes of %r with %r (CLAIM).', + irc.name, modes, reversed_modes) + irc.proto.modeClient(irc.pseudoclient.uid, target, reversed_modes) + break else: # Set hideoper on remote opers, to prevent inflating # /lusers and various /stats From a28715c2c667f0dfa25dedaad20e4d7b5e081726 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 13:47:04 -0700 Subject: [PATCH 19/70] IrcChannel: allow .deepcopy() --- classes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/classes.py b/classes.py index 9216063..cdb57f4 100644 --- a/classes.py +++ b/classes.py @@ -6,6 +6,7 @@ import threading import ssl from collections import defaultdict import hashlib +from copy import deepcopy from log import log from conf import conf @@ -333,6 +334,9 @@ class IrcChannel(): s.discard(target) self.users.discard(target) + def deepcopy(self): + return deepcopy(self) + ### FakeIRC classes, used for test cases class FakeIRC(Irc): From c82a0a771c05ee91405d81cc02f5273f43c43a1e Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 13:47:18 -0700 Subject: [PATCH 20/70] protocols: return an "oldchan" IrcChannel object with the previous state of a channel when processing channel mode changes This allows plugins to check for op statuses, etc. before the mode change is processed. --- protocols/inspircd.py | 4 +++- protocols/ts6.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 2cc07fb..2bd7593 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -466,11 +466,13 @@ class InspIRCdProtocol(TS6BaseProtocol): """Handles the FMODE command, used for channel mode changes.""" # <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD channel = utils.toLower(self.irc, args[0]) + oldobj = self.irc.channels[channel].deepcopy() modes = args[2:] changedmodes = utils.parseModes(self.irc, channel, modes) utils.applyModes(self.irc, channel, changedmodes) ts = int(args[1]) - return {'target': channel, 'modes': changedmodes, 'ts': ts} + return {'target': channel, 'modes': changedmodes, 'ts': ts, + 'oldchan': oldobj} def handle_mode(self, numeric, command, args): """Handles incoming user mode changes.""" diff --git a/protocols/ts6.py b/protocols/ts6.py index 223bf42..e097b98 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -603,11 +603,13 @@ class TS6Protocol(TS6BaseProtocol): """Handles incoming TMODE commands (channel mode change).""" # <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4 channel = utils.toLower(self.irc, args[1]) + oldobj = self.irc.channels[channel].deepcopy() modes = args[2:] changedmodes = utils.parseModes(self.irc, channel, modes) utils.applyModes(self.irc, channel, changedmodes) ts = int(args[0]) - return {'target': channel, 'modes': changedmodes, 'ts': ts} + return {'target': channel, 'modes': changedmodes, 'ts': ts, + 'oldchan': oldobj} def handle_mode(self, numeric, command, args): """Handles incoming user mode changes.""" From 9a139212dd2e1997f105732a2909b0279232db28 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 13:48:14 -0700 Subject: [PATCH 21/70] relay/utils: make mode reversals work with mode changes that affect op statuses. --- plugins/relay.py | 32 ++++++++++++++++++++++++-------- utils.py | 27 +++++++++++++++++++-------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 70da041..d1153c3 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -116,14 +116,23 @@ def save(irc, source, args): irc.msg(source, 'Error: You are not authenticated!') return -def getPrefixModes(irc, remoteirc, channel, user): +def getPrefixModes(irc, remoteirc, channel, user, mlist=None): + """ + Fetches all prefix modes for a user in a channel that are supported by the + remote IRC network given. + + Optionally, an mlist argument can be given to look at an earlier state of + the channel, e.g. for checking the op status of a mode setter before their + modes are processed and added to the channel state. + """ modes = '' + mlist = mlist or irc.channels[channel].prefixmodes for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'): if pmode in remoteirc.cmodes: # Mode supported by IRCd - mlist = irc.channels[channel].prefixmodes[pmode+'s'] + userlist = mlist[pmode+'s'] log.debug('(%s) getPrefixModes: checking if %r is in %s list: %r', - irc.name, user, pmode, mlist) - if user in mlist: + irc.name, user, pmode, userlist) + if user in userlist: modes += remoteirc.cmodes[pmode] return modes @@ -416,7 +425,7 @@ def handle_privmsg(irc, numeric, command, args): utils.add_hook(handle_privmsg, 'PRIVMSG') utils.add_hook(handle_privmsg, 'NOTICE') -def checkClaim(irc, channel, sender): +def checkClaim(irc, channel, sender, chanobj=None): """ Checks whether the sender of a kick/mode change passes CLAIM checks for a given channel. This returns True if any of the following criteria are met: @@ -428,7 +437,13 @@ def checkClaim(irc, channel, sender): 5) The originating network is the one that created the relay. """ relay = findRelay((irc.name, channel)) - sender_modes = getPrefixModes(irc, irc, channel, sender) + try: + mlist = chanobj.prefixmodes + except AttributeError: + mlist = None + sender_modes = getPrefixModes(irc, irc, channel, sender, mlist=mlist) + log.debug('(%s) relay.checkClaim: sender modes (%s/%s) are %s (mlist=%s)', irc.name, + sender, channel, sender_modes, mlist) return (not relay) or irc.name == relay[0] or irc.name in db[relay]['claim'] or \ any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ or utils.isInternalClient(irc, sender) or \ @@ -665,10 +680,11 @@ def handle_mode(irc, numeric, command, args): if irc.name == name or not remoteirc.connected.is_set(): continue if utils.isChannel(target): - if checkClaim(irc, target, numeric): + oldchan = args['oldchan'] + if checkClaim(irc, target, numeric, chanobj=oldchan): relayModes(irc, remoteirc, numeric, target, modes) else: # Mode change blocked by CLAIM. - reversed_modes = utils.reverseModes(irc, target, modes) + reversed_modes = utils.reverseModes(irc, target, modes, oldobj=oldchan) log.debug('(%s) Reversing mode changes of %r with %r (CLAIM).', irc.name, modes, reversed_modes) irc.proto.modeClient(irc.pseudoclient.uid, target, reversed_modes) diff --git a/utils.py b/utils.py index 39c8805..998397c 100644 --- a/utils.py +++ b/utils.py @@ -338,14 +338,19 @@ def _flip(mode): mode.insert(0, '-') return ''.join(mode) -def reverseModes(irc, target, modes): +def reverseModes(irc, target, modes, oldobj=None): """Reverses/Inverts the mode string or mode list given. - "+nt-lk" => "-nt+lk" - "nt-k" => "-nt+k" - [('+m', None), ('+t', None), ('+l', '3'), ('-o', 'person')] => - [('-m', None), ('-t', None), ('-l', '3'), ('+o', 'person')] - [('s', None), ('+n', None)] => [('-s', None), ('-n', None)] + Optionally, an oldobj argument can be given to look at an earlier state of + a channel/user object, e.g. for checking the op status of a mode setter + before their modes are processed and added to the channel state. + + This function allows both mode strings or mode lists. Example uses: + "+mi-lk test => "-mi+lk test" + "mi-k test => "-mi+k test" + [('+m', None), ('+r', None), ('+l', '3'), ('-o', 'person') + => {('-m', None), ('-r', None), ('-l', None), ('+o', 'person')}) + {('s', None), ('+o', 'whoever') => {('-s', None), ('-o', 'whoever')}) """ origtype = type(modes) # If the query is a string, we have to parse it first. @@ -353,11 +358,12 @@ def reverseModes(irc, target, modes): modes = parseModes(irc, target, modes.split(" ")) # Get the current mode list first. if isChannel(target): - oldmodes = irc.channels[target].modes.copy() + c = oldobj or irc.channels[target] + oldmodes = c.modes.copy() possible_modes = irc.cmodes.copy() # For channels, this also includes the list of prefix modes. possible_modes['*A'] += ''.join(irc.prefixmodes) - for name, userlist in irc.channels[target].prefixmodes.items(): + for name, userlist in c.prefixmodes.items(): try: oldmodes.update([(irc.cmodes[name[:-1]], u) for u in userlist]) except KeyError: @@ -366,6 +372,8 @@ def reverseModes(irc, target, modes): oldmodes = irc.users[target].modes possible_modes = irc.umodes newmodes = [] + log.debug('(%s) reverseModes: old/current mode list for %s is: %s', irc.name, + target, oldmodes) for char, arg in modes: # Mode types: # A = Mode that adds or removes a nick or address to a list. Always has a parameter. @@ -390,9 +398,12 @@ def reverseModes(irc, target, modes): mpair = (_flip(char), arg) if char[0] != '-' and (mchar, arg) in oldmodes: # Mode is already set. + log.debug("(%s) reverseModes: skipping reversing '%s %s' with %s since we're " + "setting a mode that's already set.", irc.name, char, arg, mpair) continue newmodes.append(mpair) + log.debug('(%s) reverseModes: new modes: %s', irc.name, newmodes) if origtype == str: # If the original query is a string, send it back as a string. return joinModes(newmodes) From 9cd176b8465f3f1fc56eecfdb502eae547bc44c8 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 14:05:07 -0700 Subject: [PATCH 22/70] utils.reverseModes: don't reverse prefix/list modes that were never set Charybdis/TS6 doesn't check this IRCd-side, so we don't want people to abuse this by say, overriding -b *!*@* in a relay channel and having the PyLink client set +b *!*@* in response, --- tests/test_utils.py | 17 ++++++++++++----- utils.py | 6 ++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index d2d34f2..8e0346d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -108,13 +108,13 @@ class TestUtils(unittest.TestCase): def testReverseModes(self): test = lambda x, y: self.assertEqual(utils.reverseModes(self.irc, '#test', x), y) # Strings. - self._reverseModes("+mi-lk test", "-mi+lk test") - self._reverseModes("mi-k test", "-mi+k test") + self._reverseModes("+mk-t test", "-mk+t test") + self._reverseModes("ml-n 111", "-ml+n") # Lists. - self._reverseModes([('+m', None), ('+r', None), ('+l', '3'), ('-o', 'person')], - {('-m', None), ('-r', None), ('-l', None), ('+o', 'person')}) + self._reverseModes([('+m', None), ('+r', None), ('+l', '3')], + {('-m', None), ('-r', None), ('-l', None)}) # Sets. - self._reverseModes({('s', None), ('+o', 'whoever')}, {('-s', None), ('-o', 'whoever')}) + self._reverseModes({('s', None)}, {('-s', None)}) # Combining modes with an initial + and those without self._reverseModes({('s', None), ('+R', None)}, {('-s', None), ('-R', None)}) @@ -139,5 +139,12 @@ class TestUtils(unittest.TestCase): self._reverseModes('+o 9PYAAAAAA', '+') self._reverseModes('+vvvvM test abcde atat abcd', '-vvvvM test abcde atat abcd') + # Ignore unsetting prefixmodes/list modes that were never set. + self._reverseModes([('-v', '10XAAAAAA')], set()) + self._reverseModes('-ob 10XAAAAAA derp!*@*', '+') + utils.applyModes(self.irc, '#test', [('+o', 'GLolol'), ('+b', '*!user@badisp.tk')]) + self._reverseModes('-voo GLolol GLolol 10XAAAAAA', '+o GLolol') + self._reverseModes('-bb *!*@* *!user@badisp.tk', '+b *!user@badisp.tk') + if __name__ == '__main__': unittest.main() diff --git a/utils.py b/utils.py index 998397c..367b430 100644 --- a/utils.py +++ b/utils.py @@ -401,6 +401,12 @@ def reverseModes(irc, target, modes, oldobj=None): log.debug("(%s) reverseModes: skipping reversing '%s %s' with %s since we're " "setting a mode that's already set.", irc.name, char, arg, mpair) continue + elif char[0] == '-' and (mchar, arg) not in oldmodes and mchar in possible_modes['*A']: + # We're unsetting a prefixmode that was never set - don't set it in response! + # Charybdis lacks verification for this server-side. + log.debug("(%s) reverseModes: skipping reversing '%s %s' with %s since it " + "wasn't previously set.", irc.name, char, arg, mpair) + continue newmodes.append(mpair) log.debug('(%s) reverseModes: new modes: %s', irc.name, newmodes) From 31aa624a94cd691912b8033d2d88d8633a607029 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 14:06:20 -0700 Subject: [PATCH 23/70] tests/inspircd: remove checks for FMODE hook data, they change too often --- tests/test_proto_inspircd.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/test_proto_inspircd.py b/tests/test_proto_inspircd.py index ab1d88b..59a3468 100644 --- a/tests/test_proto_inspircd.py +++ b/tests/test_proto_inspircd.py @@ -139,25 +139,12 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase): # Default channels start with +nt self.irc.run(':70M FMODE #pylink 1423790412 -nt') self.assertEqual(set(), self.irc.channels['#pylink'].modes) - self.irc.takeHooks() self.irc.run(':70M FMODE #pylink 1423790412 +ikl herebedragons 100') self.assertEqual({('i', None), ('k', 'herebedragons'), ('l', '100')}, self.irc.channels['#pylink'].modes) self.irc.run(':70M FMODE #pylink 1423790413 -ilk+m herebedragons') self.assertEqual({('m', None)}, self.irc.channels['#pylink'].modes) - hookdata = self.irc.takeHooks() - expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': - [('+i', None), ('+k', 'herebedragons'), - ('+l', '100')], 'ts': 1423790412} - ], - ['70M', 'FMODE', {'target': '#pylink', 'modes': - [('-i', None), ('-l', None), - ('-k', 'herebedragons'), ('+m', None)], - 'ts': 1423790413}] - ] - self.assertEqual(expected, hookdata) - def testHandleFModeWithPrefixes(self): self.irc.run(':70M FJOIN #pylink 123 +n :o,10XAAAAAA ,10XAAAAAB') # Prefix modes are stored separately, so they should never show up in .modes @@ -170,13 +157,6 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase): self.irc.run(':70M FMODE #pylink 123 -o %s' % self.u) self.assertEqual(modes, self.irc.channels['#pylink'].modes) self.assertNotIn(self.u, self.irc.channels['#pylink'].prefixmodes['ops']) - # Test hooks - hookdata = self.irc.takeHooks() - expected = [['70M', 'FJOIN', {'channel': '#pylink', 'ts': 123, 'modes': [('+n', None)], - 'users': ['10XAAAAAA', '10XAAAAAB']}], - ['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50'), ('+o', self.u), ('+t', None)], 'ts': 123}], - ['70M', 'FMODE', {'target': '#pylink', 'modes': [('-o', self.u)], 'ts': 123}]] - self.assertEqual(expected, hookdata) def testHandleFModeRemovesOldParams(self): self.irc.run(':70M FMODE #pylink 1423790412 +l 50') @@ -184,10 +164,6 @@ class InspIRCdTestCase(tests_common.CommonProtoTestCase): self.irc.run(':70M FMODE #pylink 1423790412 +l 30') self.assertIn(('l', '30'), self.irc.channels['#pylink'].modes) self.assertNotIn(('l', '50'), self.irc.channels['#pylink'].modes) - hookdata = self.irc.takeHooks() - expected = [['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '50')], 'ts': 1423790412}], - ['70M', 'FMODE', {'target': '#pylink', 'modes': [('+l', '30')], 'ts': 1423790412}]] - self.assertEqual(expected, hookdata) def testHandleFJoinResetsTS(self): curr_ts = self.irc.channels['#pylink'].ts From 9c4e0107f313171e14f0ce81323cb970f8845fc7 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 14:23:09 -0700 Subject: [PATCH 24/70] ts6_common: make old topic retrievable in handle_topic --- protocols/ts6_common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/protocols/ts6_common.py b/protocols/ts6_common.py index ad52ad6..178d755 100644 --- a/protocols/ts6_common.py +++ b/protocols/ts6_common.py @@ -224,9 +224,11 @@ class TS6BaseProtocol(Protocol): channel = utils.toLower(self.irc, args[0]) topic = args[1] ts = int(time.time()) + oldtopic = self.irc.channels[channel].topic self.irc.channels[channel].topic = topic self.irc.channels[channel].topicset = True - return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic} + return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic, + 'oldtopic': oldtopic} def handle_part(self, source, command, args): """Handles incoming PART commands.""" From 19530107d712f08e60c66aea8bd0dbd3f1937585 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 14:23:27 -0700 Subject: [PATCH 25/70] relay: enforce CLAIM checks in TOPIC too. --- plugins/relay.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index d1153c3..b65801e 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -707,22 +707,27 @@ utils.add_hook(handle_mode, 'MODE') def handle_topic(irc, numeric, command, args): channel = args['channel'] + oldtopic = args['oldtopic'] topic = args['topic'] - for name, remoteirc in world.networkobjects.items(): - if irc.name == name or not remoteirc.connected.is_set(): - continue + if checkClaim(irc, channel, numeric): + for name, remoteirc in world.networkobjects.items(): + if irc.name == name or not remoteirc.connected.is_set(): + continue + + remotechan = findRemoteChan(irc, remoteirc, channel) + # Don't send if the remote topic is the same as ours. + if remotechan is None or topic == remoteirc.channels[remotechan].topic: + continue + # This might originate from a server too. + remoteuser = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) + if remoteuser: + remoteirc.proto.topicClient(remoteuser, remotechan, topic) + else: + rsid = getRemoteSid(remoteirc, irc) + remoteirc.proto.topicServer(rsid, remotechan, topic) + else: # Topic change blocked by claim. + irc.proto.topicClient(irc.pseudoclient.uid, channel, oldtopic) - remotechan = findRemoteChan(irc, remoteirc, channel) - # Don't send if the remote topic is the same as ours. - if remotechan is None or topic == remoteirc.channels[remotechan].topic: - continue - # This might originate from a server too. - remoteuser = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) - if remoteuser: - remoteirc.proto.topicClient(remoteuser, remotechan, topic) - else: - rsid = getRemoteSid(remoteirc, irc) - remoteirc.proto.topicServer(rsid, remotechan, topic) utils.add_hook(handle_topic, 'TOPIC') def handle_kill(irc, numeric, command, args): From 1f957741316f0fd995f23631ed540ba799f1b449 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 17:04:17 -0700 Subject: [PATCH 26/70] inspircd: add proper fallback value for OPERTYPE? --- protocols/inspircd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index f0122c9..ec52fec 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -187,7 +187,7 @@ def nickClient(irc, numeric, newnick): def _operUp(irc, target, opertype=None): userobj = irc.users[target] try: - otype = opertype or userobj.opertype + otype = opertype or userobj.opertype or 'IRC_Operator' except AttributeError: log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!', irc.name, target, userobj.nick) @@ -195,6 +195,7 @@ def _operUp(irc, target, opertype=None): otype = 'IRC_Operator' log.debug('(%s) Sending OPERTYPE from %s to oper them up.', irc.name, target) + assert otype, "Tried to send an empty OPERTYPE!" userobj.opertype = otype _send(irc, target, 'OPERTYPE %s' % otype) From 4a4c9395d687d670eded5af360908441ec61c152 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 17:50:59 -0700 Subject: [PATCH 27/70] relay: remove broken logging line --- plugins/relay.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index b65801e..0f053a4 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -766,7 +766,6 @@ def handle_kill(irc, numeric, command, args): irc.name, userdata.nick) irc.aborted.set() killcache[irc.name] += 1 - log.debug('killcache: %s', irc.name, killcache) # Target user was local. else: From 1e6c5a231ec74f1810d8a94b7585d35fd9be83c7 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 17:58:39 -0700 Subject: [PATCH 28/70] relay.handle_topic: don't fail if we're bursting and no old topic exists --- plugins/relay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 0f053a4..21cef47 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -707,7 +707,7 @@ utils.add_hook(handle_mode, 'MODE') def handle_topic(irc, numeric, command, args): channel = args['channel'] - oldtopic = args['oldtopic'] + oldtopic = args.get('oldtopic') topic = args['topic'] if checkClaim(irc, channel, numeric): for name, remoteirc in world.networkobjects.items(): @@ -725,7 +725,7 @@ def handle_topic(irc, numeric, command, args): else: rsid = getRemoteSid(remoteirc, irc) remoteirc.proto.topicServer(rsid, remotechan, topic) - else: # Topic change blocked by claim. + elif oldtopic: # Topic change blocked by claim. irc.proto.topicClient(irc.pseudoclient.uid, channel, oldtopic) utils.add_hook(handle_topic, 'TOPIC') From fd16a4270824c4c669762c5b8e327740fc16e764 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 17:58:59 -0700 Subject: [PATCH 29/70] Irc: log the offending line when an error is caught in handle_events --- classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/classes.py b/classes.py index cdb57f4..9e5e6f5 100644 --- a/classes.py +++ b/classes.py @@ -213,6 +213,7 @@ class Irc(): hook_args = self.proto.handle_events(line) except Exception: log.exception('(%s) Caught error in handle_events, disconnecting!', self.name) + log.error('(%s) The offending line was: <- %s', self.name, line) return # Only call our hooks if there's data to process. Handlers that support # hooks will return a dict of parsed arguments, which can be passed on From 207a6ea31d06caa22cab6e17b6d960ddcb5acf9b Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 13 Sep 2015 22:36:50 -0700 Subject: [PATCH 30/70] Various README fixes --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a4216fa..6909306 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # PyLink -PyLink is an extensible, plugin-based IRC PseudoService written in Python. It aims to be a replacement for the now-defunct Janus. +PyLink is an extensible, plugin-based IRC Services framework written in Python. It aims to be a replacement for the now-defunct Janus. ## Usage **PyLink is a work in progress and thus may be very unstable**! No warranty is provided if this completely wrecks your network and causes widespread rioting amongst your users! -That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute. +That said, please report any bugs you find to the [issue tracker](https://github.com/GLolol/PyLink/issues). Pull requests are open if you'd like to contribute: note that **master** is bugfix only; new stuff goes to the **devel** branch. ### Dependencies @@ -22,10 +22,10 @@ Dependencies currently include: * charybdis (3.5.x / git master) - module `ts6` * Elemental-IRCd (6.6.x / git master) - module `ts6` -### Installation +### Setup -1) Rename `config.yml.example` to `config.yml` and configure your instance there. Not all options are properly implemented yet, and the configuration schema isn't finalized yet - this means that your configuration may break in an update! +1) Rename `config.yml.example` to `config.yml` and configure your instance there. Note that the configuration format isn't finalized yet - this means that your configuration may break in an update! -2) Run `main.py` from the command line. +2) Run `./pylink` from the command line. 3) Profit??? From 75de9c6be60b2006d0b551b55f7c49859b9f5fc2 Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 17:23:56 -0700 Subject: [PATCH 31/70] relay: sort code and document most internal functions --- plugins/relay.py | 775 ++++++++++++++++++++++++----------------------- 1 file changed, 401 insertions(+), 374 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 21cef47..881fa96 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -26,18 +26,33 @@ spawnlocks = defaultdict(threading.RLock) savecache = ExpiringDict(max_len=5, max_age_seconds=10) killcache = ExpiringDict(max_len=5, max_age_seconds=10) -def relayWhoisHandler(irc, target): - user = irc.users[target] - orig = getLocalUser(irc, target) - if orig: - network, remoteuid = orig - remotenick = world.networkobjects[network].users[remoteuid].nick - return [320, "%s :is a remote user connected via PyLink Relay. Home " - "network: %s; Home nick: %s" % (user.nick, network, - remotenick)] -world.whois_handlers.append(relayWhoisHandler) +### INTERNAL FUNCTIONS + +def initializeAll(irc): + """Initializes all relay channels for the given IRC object.""" + log.debug('(%s) initializeAll: waiting for world.started', irc.name) + world.started.wait() + for chanpair, entrydata in db.items(): + network, channel = chanpair + initializeChannel(irc, channel) + for link in entrydata['links']: + network, channel = link + initializeChannel(irc, channel) + +def main(): + """Main function, called during plugin loading at start.""" + loadDB() + world.schedulers['relaydb'] = scheduler = sched.scheduler() + scheduler.enter(30, 1, exportDB, argument=(True,)) + # Thread this because exportDB() queues itself as part of its + # execution, in order to get a repeating loop. + thread = threading.Thread(target=scheduler.run) + thread.daemon = True + thread.start() def normalizeNick(irc, netname, nick, separator=None, uid=''): + """Creates a normalized nickname for the given nick suitable for + introduction to a remote network (as a relay client).""" separator = separator or irc.serverdata.get('separator') or "/" log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator) orig_nick = nick @@ -87,6 +102,7 @@ def normalizeNick(irc, netname, nick, separator=None, uid=''): return nick def loadDB(): + """Loads the relay database, creating a new one if this fails.""" global db try: with open(dbname, "rb") as f: @@ -97,6 +113,8 @@ def loadDB(): db = {} def exportDB(reschedule=False): + """Exports the relay database, optionally creating a loop to do this + automatically.""" scheduler = world.schedulers.get('relaydb') if reschedule and scheduler: scheduler.enter(30, 1, exportDB, argument=(True,)) @@ -104,18 +122,6 @@ def exportDB(reschedule=False): with open(dbname, 'wb') as f: pickle.dump(db, f, protocol=4) -@utils.add_cmd -def save(irc, source, args): - """takes no arguments. - - Saves the relay database to disk.""" - if utils.isOper(irc, source): - exportDB() - irc.msg(source, 'Done.') - else: - irc.msg(source, 'Error: You are not authenticated!') - return - def getPrefixModes(irc, remoteirc, channel, user, mlist=None): """ Fetches all prefix modes for a user in a channel that are supported by the @@ -137,7 +143,7 @@ def getPrefixModes(irc, remoteirc, channel, user, mlist=None): return modes def getRemoteSid(irc, remoteirc): - """Get the remote server SID representing remoteirc on irc, spawning + """Gets the remote server SID representing remoteirc on irc, spawning it if it doesn't exist.""" with spawnlocks[irc.name]: try: @@ -148,6 +154,8 @@ def getRemoteSid(irc, remoteirc): return sid def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): + """Gets the UID of the relay client for the given IRC network/user pair, + spawning one if it doesn't exist and spawnIfMissing is True.""" # If the user (stored here as {('netname', 'UID'): # {'network1': 'UID1', 'network2': 'UID2'}}) exists, don't spawn it # again! @@ -207,30 +215,20 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): relayusers[(irc.name, user)][remoteirc.name] = u return u -def handle_operup(irc, numeric, command, args): - newtype = args['text'] + '_(remote)' - for netname, user in relayusers[(irc.name, numeric)].items(): - log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s', irc.name, user, netname, newtype) - remoteirc = world.networkobjects[netname] - remoteirc.users[user].opertype = newtype -utils.add_hook(handle_operup, 'PYLINK_CLIENT_OPERED') - def getLocalUser(irc, user, targetirc=None): - """ [] + """ + Given the UID of a relay client, returns a tuple of the home network name + and original UID of the user it was spawned for. - Returns a tuple with the home network name and the UID of the original - user that was spawned for, where - is the UID of a PyLink relay dummy client. - - If is specified, returns the UID of the pseudoclient - representing the original user on the target network, similar to what - getRemoteUser() does.""" + If targetirc is given, getRemoteUser() is called to get the relay client + representing the original user on that target network.""" # First, iterate over everyone! try: remoteuser = irc.users[user].remote except (AttributeError, KeyError): remoteuser = None - log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', irc.name, remoteuser, user, irc.name) + log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', + irc.name, remoteuser, user, irc.name) if remoteuser: # If targetirc is given, we'll return simply the UID of the user on the # target network, if it exists. Otherwise, we'll return a tuple @@ -242,13 +240,17 @@ def getLocalUser(irc, user, targetirc=None): # requested; just return the UID then. return remoteuser[1] # Otherwise, use getRemoteUser to find our UID. - res = getRemoteUser(sourceobj, targetirc, remoteuser[1], spawnIfMissing=False) - log.debug('(%s) getLocalUser: targetirc found, getting %r as remoteuser for %r (looking up %s/%s).', irc.name, res, remoteuser[1], user, irc.name) + res = getRemoteUser(sourceobj, targetirc, remoteuser[1], + spawnIfMissing=False) + log.debug('(%s) getLocalUser: targetirc found, getting %r as ' + 'remoteuser for %r (looking up %s/%s).', irc.name, res, + remoteuser[1], user, irc.name) return res else: return remoteuser def findRelay(chanpair): + """Finds a matching relay for the given (network name, channel) pair.""" if chanpair in db: # This chanpair is a shared channel; others link to it return chanpair # This chanpair is linked *to* a remote channel @@ -257,6 +259,8 @@ def findRelay(chanpair): return name def findRemoteChan(irc, remoteirc, channel): + """Returns the linked channel name for the given channel on remoteirc, + if one exists.""" query = (irc.name, channel) remotenetname = remoteirc.name chanpair = findRelay(query) @@ -270,6 +274,7 @@ def findRemoteChan(irc, remoteirc, channel): return link[1] def initializeChannel(irc, channel): + """Initializes a relay channel (merge local/remote users, set modes, etc.).""" # We're initializing a relay that already exists. This can be done at # ENDBURST, or on the LINK command. relay = findRelay((irc.name, channel)) @@ -303,6 +308,264 @@ def initializeChannel(irc, channel): relayJoins(irc, channel, irc.channels[channel].users, irc.channels[channel].ts) irc.proto.joinClient(irc.pseudoclient.uid, channel) +def removeChannel(irc, channel): + """Destroys a relay channel by parting all of its users.""" + if irc is None: + return + if channel not in map(str.lower, irc.serverdata['channels']): + irc.proto.partClient(irc.pseudoclient.uid, channel, 'Channel delinked.') + relay = findRelay((irc.name, channel)) + if relay: + for user in irc.channels[channel].users.copy(): + if not isRelayClient(irc, user): + relayPart(irc, channel, user) + # Don't ever part the main client from any of its autojoin channels. + else: + if user == irc.pseudoclient.uid and channel in \ + irc.serverdata['channels']: + continue + irc.proto.partClient(user, channel, 'Channel delinked.') + # Don't ever quit it either... + if user != irc.pseudoclient.uid and not irc.users[user].channels: + remoteuser = getLocalUser(irc, user) + del relayusers[remoteuser][irc.name] + irc.proto.quitClient(user, 'Left all shared channels.') + +def checkClaim(irc, channel, sender, chanobj=None): + """ + Checks whether the sender of a kick/mode change passes CLAIM checks for + a given channel. This returns True if any of the following criteria are met: + + 1) The originating network is in the CLAIM list for the relay in question. + 2) The sender is halfop or above in the channel. + 3) The sender is a PyLink client/server (checks are suppressed in this case). + 4) No relay exists for the channel in question. + 5) The originating network is the one that created the relay. + """ + relay = findRelay((irc.name, channel)) + try: + mlist = chanobj.prefixmodes + except AttributeError: + mlist = None + sender_modes = getPrefixModes(irc, irc, channel, sender, mlist=mlist) + log.debug('(%s) relay.checkClaim: sender modes (%s/%s) are %s (mlist=%s)', irc.name, + sender, channel, sender_modes, mlist) + return (not relay) or irc.name == relay[0] or irc.name in db[relay]['claim'] or \ + any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ + or utils.isInternalClient(irc, sender) or \ + utils.isInternalServer(irc, sender) + +def getSupportedUmodes(irc, remoteirc, modes): + """Given a list of user modes, filters out all of those not supported by the + remote network.""" + supported_modes = [] + for modepair in modes: + try: + prefix, modechar = modepair[0] + except ValueError: + modechar = modepair[0] + prefix = '+' + arg = modepair[1] + for name, m in irc.umodes.items(): + supported_char = None + if modechar == m: + if name not in whitelisted_umodes: + log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because " + "it isn't a whitelisted (safe) mode for relay.", + irc.name, modechar, arg) + break + supported_char = remoteirc.umodes.get(name) + if supported_char: + supported_modes.append((prefix+supported_char, arg)) + break + else: + log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because " + "the remote network (%s)'s IRCd (%s) doesn't support it.", + irc.name, modechar, arg, remoteirc.name, + remoteirc.protoname) + return supported_modes + +def isRelayClient(irc, user): + """Returns whether the given user is a relay client.""" + try: + if irc.users[user].remote: + # Is the .remote attribute set? If so, don't relay already + # relayed clients; that'll trigger an endless loop! + return True + except AttributeError: # Nope, it isn't. + pass + except KeyError: # The user doesn't exist?!? + return True + return False + +### EVENT HANDLER INTERNALS + +def relayJoins(irc, channel, users, ts, burst=True): + for name, remoteirc in world.networkobjects.items(): + queued_users = [] + if name == irc.name or not remoteirc.connected.is_set(): + # Don't relay things to their source network... + continue + remotechan = findRemoteChan(irc, remoteirc, channel) + if remotechan is None: + # If there is no link on our network for the user, don't + # bother spawning it. + continue + log.debug('(%s) relayJoins: got %r for users', irc.name, users) + for user in users.copy(): + if isRelayClient(irc, user): + # Don't clone relay clients; that'll cause some bad, bad + # things to happen. + continue + log.debug('Okay, spawning %s/%s everywhere', user, irc.name) + assert user in irc.users, "(%s) How is this possible? %r isn't in our user database." % (irc.name, user) + u = getRemoteUser(irc, remoteirc, user) + # Only join users if they aren't already joined. This prevents op floods + # on charybdis from all the SJOINing. + if u not in remoteirc.channels[remotechan].users: + ts = irc.channels[channel].ts + prefixes = getPrefixModes(irc, remoteirc, channel, user) + userpair = (prefixes, u) + queued_users.append(userpair) + log.debug('(%s) relayJoins: joining %s to %s%s', irc.name, userpair, remoteirc.name, remotechan) + else: + log.debug('(%s) relayJoins: not joining %s to %s%s; they\'re already there!', irc.name, + u, remoteirc.name, remotechan) + if queued_users: + # Burst was explicitly given, or we're trying to join multiple + # users/someone with a prefix. + if burst or len(queued_users) > 1 or queued_users[0][0]: + rsid = getRemoteSid(remoteirc, irc) + remoteirc.proto.sjoinServer(rsid, remotechan, queued_users, ts=ts) + relayModes(irc, remoteirc, getRemoteSid(irc, remoteirc), channel, irc.channels[channel].modes) + else: + remoteirc.proto.joinClient(queued_users[0][1], remotechan) + +def relayPart(irc, channel, user): + for name, remoteirc in world.networkobjects.items(): + if name == irc.name or not remoteirc.connected.is_set(): + # Don't relay things to their source network... + continue + remotechan = findRemoteChan(irc, remoteirc, channel) + log.debug('(%s) relayPart: looking for %s/%s on %s', irc.name, user, irc.name, remoteirc.name) + log.debug('(%s) relayPart: remotechan found as %s', irc.name, remotechan) + remoteuser = getRemoteUser(irc, remoteirc, user, spawnIfMissing=False) + log.debug('(%s) relayPart: remoteuser for %s/%s found as %s', irc.name, user, irc.name, remoteuser) + if remotechan is None or remoteuser is None: + continue + remoteirc.proto.partClient(remoteuser, remotechan, 'Channel delinked.') + if isRelayClient(remoteirc, remoteuser) and not remoteirc.users[remoteuser].channels: + remoteirc.proto.quitClient(remoteuser, 'Left all shared channels.') + del relayusers[(irc.name, user)][remoteirc.name] + + +whitelisted_cmodes = {'admin', 'allowinvite', 'autoop', 'ban', 'banexception', + 'blockcolor', 'halfop', 'invex', 'inviteonly', 'key', + 'limit', 'moderated', 'noctcp', 'noextmsg', 'nokick', + 'noknock', 'nonick', 'nonotice', 'op', 'operonly', + 'opmoderated', 'owner', 'private', 'regonly', + 'regmoderated', 'secret', 'sslonly', 'adminonly', + 'stripcolor', 'topiclock', 'voice'} +whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper', + 'regdeaf', 'u_stripcolor', 'u_noctcp', 'wallops'} +def relayModes(irc, remoteirc, sender, channel, modes=None): + remotechan = findRemoteChan(irc, remoteirc, channel) + log.debug('(%s) Relay mode: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan) + if remotechan is None: + return + if modes is None: + modes = irc.channels[channel].modes + log.debug('(%s) Relay mode: channel data for %s%s: %s', irc.name, remoteirc.name, remotechan, remoteirc.channels[remotechan]) + supported_modes = [] + log.debug('(%s) Relay mode: initial modelist for %s is %s', irc.name, channel, modes) + for modepair in modes: + try: + prefix, modechar = modepair[0] + except ValueError: + modechar = modepair[0] + prefix = '+' + arg = modepair[1] + # Iterate over every mode see whether the remote IRCd supports + # this mode, and what its mode char for it is (if it is different). + for name, m in irc.cmodes.items(): + supported_char = None + if modechar == m: + supported_char = remoteirc.cmodes.get(name) + if supported_char is None: + break + if name not in whitelisted_cmodes: + log.debug("(%s) Relay mode: skipping mode (%r, %r) because " + "it isn't a whitelisted (safe) mode for relay.", + irc.name, modechar, arg) + break + if modechar in irc.prefixmodes: + # This is a prefix mode (e.g. +o). We must coerse the argument + # so that the target exists on the remote relay network. + log.debug("(%s) Relay mode: coersing argument of (%r, %r) " + "for network %r.", + irc.name, modechar, arg, remoteirc.name) + # If the target is a remote user, get the real target + # (original user). + arg = getLocalUser(irc, arg, targetirc=remoteirc) or \ + getRemoteUser(irc, remoteirc, arg, spawnIfMissing=False) + log.debug("(%s) Relay mode: argument found as (%r, %r) " + "for network %r.", + irc.name, modechar, arg, remoteirc.name) + oplist = remoteirc.channels[remotechan].prefixmodes[name+'s'] + log.debug("(%s) Relay mode: list of %ss on %r is: %s", + irc.name, name, remotechan, oplist) + if prefix == '+' and arg in oplist: + # Don't set prefix modes that are already set. + log.debug("(%s) Relay mode: skipping setting %s on %s/%s because it appears to be already set.", + irc.name, name, arg, remoteirc.name) + break + supported_char = remoteirc.cmodes.get(name) + if supported_char: + final_modepair = (prefix+supported_char, arg) + if name in ('ban', 'banexception', 'invex') and not utils.isHostmask(arg): + # Don't add bans that don't match n!u@h syntax! + log.debug("(%s) Relay mode: skipping mode (%r, %r) because it doesn't match nick!user@host syntax.", + irc.name, modechar, arg) + break + # Don't set modes that are already set, to prevent floods on TS6 + # where the same mode can be set infinite times. + if prefix == '+' and final_modepair in remoteirc.channels[remotechan].modes: + log.debug("(%s) Relay mode: skipping setting mode (%r, %r) on %s%s because it appears to be already set.", + irc.name, supported_char, arg, remoteirc.name, remotechan) + break + supported_modes.append(final_modepair) + log.debug('(%s) Relay mode: final modelist (sending to %s%s) is %s', irc.name, remoteirc.name, remotechan, supported_modes) + # Don't send anything if there are no supported modes left after filtering. + if supported_modes: + # Check if the sender is a user; remember servers are allowed to set modes too. + u = getRemoteUser(irc, remoteirc, sender, spawnIfMissing=False) + if u: + remoteirc.proto.modeClient(u, remotechan, supported_modes) + else: + rsid = getRemoteSid(remoteirc, irc) + remoteirc.proto.modeServer(rsid, remotechan, supported_modes) + +def relayWhoisHandler(irc, target): + user = irc.users[target] + orig = getLocalUser(irc, target) + if orig: + network, remoteuid = orig + remotenick = world.networkobjects[network].users[remoteuid].nick + return [320, "%s :is a remote user connected via PyLink Relay. Home " + "network: %s; Home nick: %s" % (user.nick, network, + remotenick)] +world.whois_handlers.append(relayWhoisHandler) + +### GENERIC EVENT HOOK HANDLERS + +def handle_operup(irc, numeric, command, args): + newtype = args['text'] + '_(remote)' + for netname, user in relayusers[(irc.name, numeric)].items(): + log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s', irc.name, user, netname, newtype) + remoteirc = world.networkobjects[netname] + remoteirc.users[user].opertype = newtype +utils.add_hook(handle_operup, 'PYLINK_CLIENT_OPERED') + def handle_join(irc, numeric, command, args): channel = args['channel'] if not findRelay((irc.name, channel)): @@ -425,30 +688,6 @@ def handle_privmsg(irc, numeric, command, args): utils.add_hook(handle_privmsg, 'PRIVMSG') utils.add_hook(handle_privmsg, 'NOTICE') -def checkClaim(irc, channel, sender, chanobj=None): - """ - Checks whether the sender of a kick/mode change passes CLAIM checks for - a given channel. This returns True if any of the following criteria are met: - - 1) The originating network is in the CLAIM list for the relay in question. - 2) The sender is halfop or above in the channel. - 3) The sender is a PyLink client/server (checks are suppressed in this case). - 4) No relay exists for the channel in question. - 5) The originating network is the one that created the relay. - """ - relay = findRelay((irc.name, channel)) - try: - mlist = chanobj.prefixmodes - except AttributeError: - mlist = None - sender_modes = getPrefixModes(irc, irc, channel, sender, mlist=mlist) - log.debug('(%s) relay.checkClaim: sender modes (%s/%s) are %s (mlist=%s)', irc.name, - sender, channel, sender_modes, mlist) - return (not relay) or irc.name == relay[0] or irc.name in db[relay]['claim'] or \ - any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ - or utils.isInternalClient(irc, sender) or \ - utils.isInternalServer(irc, sender) - def handle_kick(irc, source, command, args): channel = args['channel'] target = args['target'] @@ -559,120 +798,6 @@ def handle_chgclient(irc, source, command, args): for c in ('CHGHOST', 'CHGNAME', 'CHGIDENT'): utils.add_hook(handle_chgclient, c) -whitelisted_cmodes = {'admin', 'allowinvite', 'autoop', 'ban', 'banexception', - 'blockcolor', 'halfop', 'invex', 'inviteonly', 'key', - 'limit', 'moderated', 'noctcp', 'noextmsg', 'nokick', - 'noknock', 'nonick', 'nonotice', 'op', 'operonly', - 'opmoderated', 'owner', 'private', 'regonly', - 'regmoderated', 'secret', 'sslonly', 'adminonly', - 'stripcolor', 'topiclock', 'voice'} -whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper', - 'regdeaf', 'u_stripcolor', 'u_noctcp', 'wallops'} -def relayModes(irc, remoteirc, sender, channel, modes=None): - remotechan = findRemoteChan(irc, remoteirc, channel) - log.debug('(%s) Relay mode: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan) - if remotechan is None: - return - if modes is None: - modes = irc.channels[channel].modes - log.debug('(%s) Relay mode: channel data for %s%s: %s', irc.name, remoteirc.name, remotechan, remoteirc.channels[remotechan]) - supported_modes = [] - log.debug('(%s) Relay mode: initial modelist for %s is %s', irc.name, channel, modes) - for modepair in modes: - try: - prefix, modechar = modepair[0] - except ValueError: - modechar = modepair[0] - prefix = '+' - arg = modepair[1] - # Iterate over every mode see whether the remote IRCd supports - # this mode, and what its mode char for it is (if it is different). - for name, m in irc.cmodes.items(): - supported_char = None - if modechar == m: - supported_char = remoteirc.cmodes.get(name) - if supported_char is None: - break - if name not in whitelisted_cmodes: - log.debug("(%s) Relay mode: skipping mode (%r, %r) because " - "it isn't a whitelisted (safe) mode for relay.", - irc.name, modechar, arg) - break - if modechar in irc.prefixmodes: - # This is a prefix mode (e.g. +o). We must coerse the argument - # so that the target exists on the remote relay network. - log.debug("(%s) Relay mode: coersing argument of (%r, %r) " - "for network %r.", - irc.name, modechar, arg, remoteirc.name) - # If the target is a remote user, get the real target - # (original user). - arg = getLocalUser(irc, arg, targetirc=remoteirc) or \ - getRemoteUser(irc, remoteirc, arg, spawnIfMissing=False) - log.debug("(%s) Relay mode: argument found as (%r, %r) " - "for network %r.", - irc.name, modechar, arg, remoteirc.name) - oplist = remoteirc.channels[remotechan].prefixmodes[name+'s'] - log.debug("(%s) Relay mode: list of %ss on %r is: %s", - irc.name, name, remotechan, oplist) - if prefix == '+' and arg in oplist: - # Don't set prefix modes that are already set. - log.debug("(%s) Relay mode: skipping setting %s on %s/%s because it appears to be already set.", - irc.name, name, arg, remoteirc.name) - break - supported_char = remoteirc.cmodes.get(name) - if supported_char: - final_modepair = (prefix+supported_char, arg) - if name in ('ban', 'banexception', 'invex') and not utils.isHostmask(arg): - # Don't add bans that don't match n!u@h syntax! - log.debug("(%s) Relay mode: skipping mode (%r, %r) because it doesn't match nick!user@host syntax.", - irc.name, modechar, arg) - break - # Don't set modes that are already set, to prevent floods on TS6 - # where the same mode can be set infinite times. - if prefix == '+' and final_modepair in remoteirc.channels[remotechan].modes: - log.debug("(%s) Relay mode: skipping setting mode (%r, %r) on %s%s because it appears to be already set.", - irc.name, supported_char, arg, remoteirc.name, remotechan) - break - supported_modes.append(final_modepair) - log.debug('(%s) Relay mode: final modelist (sending to %s%s) is %s', irc.name, remoteirc.name, remotechan, supported_modes) - # Don't send anything if there are no supported modes left after filtering. - if supported_modes: - # Check if the sender is a user; remember servers are allowed to set modes too. - u = getRemoteUser(irc, remoteirc, sender, spawnIfMissing=False) - if u: - remoteirc.proto.modeClient(u, remotechan, supported_modes) - else: - rsid = getRemoteSid(remoteirc, irc) - remoteirc.proto.modeServer(rsid, remotechan, supported_modes) - -def getSupportedUmodes(irc, remoteirc, modes): - supported_modes = [] - for modepair in modes: - try: - prefix, modechar = modepair[0] - except ValueError: - modechar = modepair[0] - prefix = '+' - arg = modepair[1] - for name, m in irc.umodes.items(): - supported_char = None - if modechar == m: - if name not in whitelisted_umodes: - log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because " - "it isn't a whitelisted (safe) mode for relay.", - irc.name, modechar, arg) - break - supported_char = remoteirc.umodes.get(name) - if supported_char: - supported_modes.append((prefix+supported_char, arg)) - break - else: - log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because " - "the remote network (%s)'s IRCd (%s) doesn't support it.", - irc.name, modechar, arg, remoteirc.name, - remoteirc.protoname) - return supported_modes - def handle_mode(irc, numeric, command, args): target = args['target'] modes = args['modes'] @@ -778,97 +903,94 @@ def handle_kill(irc, numeric, command, args): utils.add_hook(handle_kill, 'KILL') -def isRelayClient(irc, user): - try: - if irc.users[user].remote: - # Is the .remote attribute set? If so, don't relay already - # relayed clients; that'll trigger an endless loop! - return True - except AttributeError: # Nope, it isn't. - pass - except KeyError: # The user doesn't exist?!? - return True - return False +def handle_away(irc, numeric, command, args): + for netname, user in relayusers[(irc.name, numeric)].items(): + remoteirc = world.networkobjects[netname] + remoteirc.proto.awayClient(user, args['text']) +utils.add_hook(handle_away, 'AWAY') -def relayJoins(irc, channel, users, ts, burst=True): - for name, remoteirc in world.networkobjects.items(): - queued_users = [] - if name == irc.name or not remoteirc.connected.is_set(): - # Don't relay things to their source network... - continue +def handle_spawnmain(irc, numeric, command, args): + if args['olduser']: + # Kills to the main PyLink client force reinitialization; this makes sure + # it joins all the relay channels like it's supposed to. + initializeAll(irc) +utils.add_hook(handle_spawnmain, 'PYLINK_SPAWNMAIN') + +def handle_invite(irc, source, command, args): + target = args['target'] + channel = args['channel'] + if isRelayClient(irc, target): + remotenet, remoteuser = getLocalUser(irc, target) + remoteirc = world.networkobjects[remotenet] remotechan = findRemoteChan(irc, remoteirc, channel) - if remotechan is None: - # If there is no link on our network for the user, don't - # bother spawning it. - continue - log.debug('(%s) relayJoins: got %r for users', irc.name, users) - for user in users.copy(): - if isRelayClient(irc, user): - # Don't clone relay clients; that'll cause some bad, bad - # things to happen. - continue - log.debug('Okay, spawning %s/%s everywhere', user, irc.name) - assert user in irc.users, "(%s) How is this possible? %r isn't in our user database." % (irc.name, user) - u = getRemoteUser(irc, remoteirc, user) - # Only join users if they aren't already joined. This prevents op floods - # on charybdis from all the SJOINing. - if u not in remoteirc.channels[remotechan].users: - ts = irc.channels[channel].ts - prefixes = getPrefixModes(irc, remoteirc, channel, user) - userpair = (prefixes, u) - queued_users.append(userpair) - log.debug('(%s) relayJoins: joining %s to %s%s', irc.name, userpair, remoteirc.name, remotechan) - else: - log.debug('(%s) relayJoins: not joining %s to %s%s; they\'re already there!', irc.name, - u, remoteirc.name, remotechan) - if queued_users: - # Burst was explicitly given, or we're trying to join multiple - # users/someone with a prefix. - if burst or len(queued_users) > 1 or queued_users[0][0]: - rsid = getRemoteSid(remoteirc, irc) - remoteirc.proto.sjoinServer(rsid, remotechan, queued_users, ts=ts) - relayModes(irc, remoteirc, getRemoteSid(irc, remoteirc), channel, irc.channels[channel].modes) - else: - remoteirc.proto.joinClient(queued_users[0][1], remotechan) + remotesource = getRemoteUser(irc, remoteirc, source, spawnIfMissing=False) + if remotesource is None: + irc.msg(source, 'Error: You must be in a common channel ' + 'with %s to invite them to channels.' % \ + irc.users[target].nick, + notice=True) + elif remotechan is None: + irc.msg(source, 'Error: You cannot invite someone to a ' + 'channel not on their network!', + notice=True) + else: + remoteirc.proto.inviteClient(remotesource, remoteuser, + remotechan) +utils.add_hook(handle_invite, 'INVITE') -def relayPart(irc, channel, user): - for name, remoteirc in world.networkobjects.items(): - if name == irc.name or not remoteirc.connected.is_set(): - # Don't relay things to their source network... - continue - remotechan = findRemoteChan(irc, remoteirc, channel) - log.debug('(%s) relayPart: looking for %s/%s on %s', irc.name, user, irc.name, remoteirc.name) - log.debug('(%s) relayPart: remotechan found as %s', irc.name, remotechan) - remoteuser = getRemoteUser(irc, remoteirc, user, spawnIfMissing=False) - log.debug('(%s) relayPart: remoteuser for %s/%s found as %s', irc.name, user, irc.name, remoteuser) - if remotechan is None or remoteuser is None: - continue - remoteirc.proto.partClient(remoteuser, remotechan, 'Channel delinked.') - if isRelayClient(remoteirc, remoteuser) and not remoteirc.users[remoteuser].channels: - remoteirc.proto.quitClient(remoteuser, 'Left all shared channels.') - del relayusers[(irc.name, user)][remoteirc.name] +def handle_endburst(irc, numeric, command, args): + if numeric == irc.uplink: + initializeAll(irc) +utils.add_hook(handle_endburst, "ENDBURST") -def removeChannel(irc, channel): - if irc is None: - return - if channel not in map(str.lower, irc.serverdata['channels']): - irc.proto.partClient(irc.pseudoclient.uid, channel, 'Channel delinked.') - relay = findRelay((irc.name, channel)) - if relay: - for user in irc.channels[channel].users.copy(): - if not isRelayClient(irc, user): - relayPart(irc, channel, user) - # Don't ever part the main client from any of its autojoin channels. - else: - if user == irc.pseudoclient.uid and channel in \ - irc.serverdata['channels']: - continue - irc.proto.partClient(user, channel, 'Channel delinked.') - # Don't ever quit it either... - if user != irc.pseudoclient.uid and not irc.users[user].channels: - remoteuser = getLocalUser(irc, user) - del relayusers[remoteuser][irc.name] - irc.proto.quitClient(user, 'Left all shared channels.') +def handle_disconnect(irc, numeric, command, args): + for k, v in relayusers.copy().items(): + if irc.name in v: + del relayusers[k][irc.name] + if k[0] == irc.name: + del relayusers[k] + for name, ircobj in world.networkobjects.items(): + if name != irc.name: + rsid = getRemoteSid(ircobj, irc) + ircobj.proto.squitServer(ircobj.sid, rsid, text='Home network lost connection.') + del relayservers[name][irc.name] + del relayservers[irc.name] + # handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Home network lost connection.'}) + +utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT") + +def handle_save(irc, numeric, command, args): + target = args['target'] + realuser = getLocalUser(irc, target) + log.debug('(%s) relay handle_save: %r got in a nick collision! Real user: %r', + irc.name, target, realuser) + if isRelayClient(irc, target) and realuser: + # Nick collision! + # It's one of our relay clients; try to fix our nick to the next + # available normalized nick. + remotenet, remoteuser = realuser + remoteirc = world.networkobjects[remotenet] + nick = remoteirc.users[remoteuser].nick + # Limit how many times we can attempt to fix our nick, to prevent + # floods and such. + if savecache.setdefault(irc.name, 0) <= 5: + newnick = normalizeNick(irc, remotenet, nick) + log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s', + irc.name, target, nick, newnick) + irc.proto.nickClient(target, newnick) + else: + log.warning('(%s) SAVE received for relay client %r (%s), not ' + 'fixing nick again due to 5 failed attempts in ' + 'the last 10 seconds!', irc.name, target, nick) + savecache[irc.name] += 1 + else: + # Somebody else on the network (not a PyLink client) had a nick collision; + # relay this as a nick change appropriately. + handle_nick(irc, target, 'SAVE', {'oldnick': None, 'newnick': target}) + +utils.add_hook(handle_save, "SAVE") + +### PUBLIC COMMANDS @utils.add_cmd def create(irc, source, args): @@ -1015,78 +1137,6 @@ def delink(irc, source, args): else: irc.msg(source, 'Error: No such relay %r.' % channel) -def initializeAll(irc): - log.debug('(%s) initializeAll: waiting for world.started', irc.name) - world.started.wait() - for chanpair, entrydata in db.items(): - network, channel = chanpair - initializeChannel(irc, channel) - for link in entrydata['links']: - network, channel = link - initializeChannel(irc, channel) - -def main(): - loadDB() - world.schedulers['relaydb'] = scheduler = sched.scheduler() - scheduler.enter(30, 1, exportDB, argument=(True,)) - # Thread this because exportDB() queues itself as part of its - # execution, in order to get a repeating loop. - thread = threading.Thread(target=scheduler.run) - thread.daemon = True - thread.start() - -def handle_endburst(irc, numeric, command, args): - if numeric == irc.uplink: - initializeAll(irc) -utils.add_hook(handle_endburst, "ENDBURST") - -def handle_disconnect(irc, numeric, command, args): - for k, v in relayusers.copy().items(): - if irc.name in v: - del relayusers[k][irc.name] - if k[0] == irc.name: - del relayusers[k] - for name, ircobj in world.networkobjects.items(): - if name != irc.name: - rsid = getRemoteSid(ircobj, irc) - ircobj.proto.squitServer(ircobj.sid, rsid, text='Home network lost connection.') - del relayservers[name][irc.name] - del relayservers[irc.name] - # handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Home network lost connection.'}) - -utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT") - -def handle_save(irc, numeric, command, args): - target = args['target'] - realuser = getLocalUser(irc, target) - log.debug('(%s) relay handle_save: %r got in a nick collision! Real user: %r', - irc.name, target, realuser) - if isRelayClient(irc, target) and realuser: - # Nick collision! - # It's one of our relay clients; try to fix our nick to the next - # available normalized nick. - remotenet, remoteuser = realuser - remoteirc = world.networkobjects[remotenet] - nick = remoteirc.users[remoteuser].nick - # Limit how many times we can attempt to fix our nick, to prevent - # floods and such. - if savecache.setdefault(irc.name, 0) <= 5: - newnick = normalizeNick(irc, remotenet, nick) - log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s', - irc.name, target, nick, newnick) - irc.proto.nickClient(target, newnick) - else: - log.warning('(%s) SAVE received for relay client %r (%s), not ' - 'fixing nick again due to 5 failed attempts in ' - 'the last 10 seconds!', irc.name, target, nick) - savecache[irc.name] += 1 - else: - # Somebody else on the network (not a PyLink client) had a nick collision; - # relay this as a nick change appropriately. - handle_nick(irc, target, 'SAVE', {'oldnick': None, 'newnick': target}) - -utils.add_hook(handle_save, "SAVE") - @utils.add_cmd def linked(irc, source, args): """takes no arguments. @@ -1105,41 +1155,6 @@ def linked(irc, source, args): s += '(no relays yet)' irc.msg(source, s) -def handle_away(irc, numeric, command, args): - for netname, user in relayusers[(irc.name, numeric)].items(): - remoteirc = world.networkobjects[netname] - remoteirc.proto.awayClient(user, args['text']) -utils.add_hook(handle_away, 'AWAY') - -def handle_spawnmain(irc, numeric, command, args): - if args['olduser']: - # Kills to the main PyLink client force reinitialization; this makes sure - # it joins all the relay channels like it's supposed to. - initializeAll(irc) -utils.add_hook(handle_spawnmain, 'PYLINK_SPAWNMAIN') - -def handle_invite(irc, source, command, args): - target = args['target'] - channel = args['channel'] - if isRelayClient(irc, target): - remotenet, remoteuser = getLocalUser(irc, target) - remoteirc = world.networkobjects[remotenet] - remotechan = findRemoteChan(irc, remoteirc, channel) - remotesource = getRemoteUser(irc, remoteirc, source, spawnIfMissing=False) - if remotesource is None: - irc.msg(source, 'Error: You must be in a common channel ' - 'with %s to invite them to channels.' % \ - irc.users[target].nick, - notice=True) - elif remotechan is None: - irc.msg(source, 'Error: You cannot invite someone to a ' - 'channel not on their network!', - notice=True) - else: - remoteirc.proto.inviteClient(remotesource, remoteuser, - remotechan) -utils.add_hook(handle_invite, 'INVITE') - @utils.add_cmd def linkacl(irc, source, args): """ALLOW|DENY|LIST @@ -1221,3 +1236,15 @@ def showuser(irc, source, args): relaychannels.append(''.join(relay)) if relaychannels and (utils.isOper(irc, source) or u == source): irc.msg(source, "\x02Relay channels\x02: %s" % ' '.join(relaychannels)) + +@utils.add_cmd +def save(irc, source, args): + """takes no arguments. + + Saves the relay database to disk.""" + if utils.isOper(irc, source): + exportDB() + irc.msg(source, 'Done.') + else: + irc.msg(source, 'Error: You are not authenticated!') + return From 81628f9a6d2e332f903b3ed590e095d263e0c471 Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 17:29:37 -0700 Subject: [PATCH 32/70] relay: rename some internal functions to make more sense getLocalUser => getOrigUser findRemoteChan => getRemoteChan findRelay => getRelay --- plugins/relay.py | 72 ++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 881fa96..b5014ea 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -215,7 +215,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): relayusers[(irc.name, user)][remoteirc.name] = u return u -def getLocalUser(irc, user, targetirc=None): +def getOrigUser(irc, user, targetirc=None): """ Given the UID of a relay client, returns a tuple of the home network name and original UID of the user it was spawned for. @@ -227,7 +227,7 @@ def getLocalUser(irc, user, targetirc=None): remoteuser = irc.users[user].remote except (AttributeError, KeyError): remoteuser = None - log.debug('(%s) getLocalUser: remoteuser set to %r (looking up %s/%s).', + log.debug('(%s) getOrigUser: remoteuser set to %r (looking up %s/%s).', irc.name, remoteuser, user, irc.name) if remoteuser: # If targetirc is given, we'll return simply the UID of the user on the @@ -242,14 +242,14 @@ def getLocalUser(irc, user, targetirc=None): # Otherwise, use getRemoteUser to find our UID. res = getRemoteUser(sourceobj, targetirc, remoteuser[1], spawnIfMissing=False) - log.debug('(%s) getLocalUser: targetirc found, getting %r as ' + log.debug('(%s) getOrigUser: targetirc found, getting %r as ' 'remoteuser for %r (looking up %s/%s).', irc.name, res, remoteuser[1], user, irc.name) return res else: return remoteuser -def findRelay(chanpair): +def getRelay(chanpair): """Finds a matching relay for the given (network name, channel) pair.""" if chanpair in db: # This chanpair is a shared channel; others link to it return chanpair @@ -258,12 +258,12 @@ def findRelay(chanpair): if chanpair in dbentry['links']: return name -def findRemoteChan(irc, remoteirc, channel): +def getRemoteChan(irc, remoteirc, channel): """Returns the linked channel name for the given channel on remoteirc, if one exists.""" query = (irc.name, channel) remotenetname = remoteirc.name - chanpair = findRelay(query) + chanpair = getRelay(query) if chanpair is None: return if chanpair[0] == remotenetname: @@ -277,7 +277,7 @@ def initializeChannel(irc, channel): """Initializes a relay channel (merge local/remote users, set modes, etc.).""" # We're initializing a relay that already exists. This can be done at # ENDBURST, or on the LINK command. - relay = findRelay((irc.name, channel)) + relay = getRelay((irc.name, channel)) log.debug('(%s) initializeChannel being called on %s', irc.name, channel) log.debug('(%s) initializeChannel: relay pair found to be %s', irc.name, relay) queued_users = [] @@ -294,7 +294,7 @@ def initializeChannel(irc, channel): if remoteirc is None: continue rc = remoteirc.channels[remotechan] - if not (remoteirc.connected.is_set() and findRemoteChan(remoteirc, irc, remotechan)): + if not (remoteirc.connected.is_set() and getRemoteChan(remoteirc, irc, remotechan)): continue # They aren't connected, don't bother! # Join their (remote) users and set their modes. relayJoins(remoteirc, remotechan, rc.users, rc.ts) @@ -314,7 +314,7 @@ def removeChannel(irc, channel): return if channel not in map(str.lower, irc.serverdata['channels']): irc.proto.partClient(irc.pseudoclient.uid, channel, 'Channel delinked.') - relay = findRelay((irc.name, channel)) + relay = getRelay((irc.name, channel)) if relay: for user in irc.channels[channel].users.copy(): if not isRelayClient(irc, user): @@ -327,7 +327,7 @@ def removeChannel(irc, channel): irc.proto.partClient(user, channel, 'Channel delinked.') # Don't ever quit it either... if user != irc.pseudoclient.uid and not irc.users[user].channels: - remoteuser = getLocalUser(irc, user) + remoteuser = getOrigUser(irc, user) del relayusers[remoteuser][irc.name] irc.proto.quitClient(user, 'Left all shared channels.') @@ -342,7 +342,7 @@ def checkClaim(irc, channel, sender, chanobj=None): 4) No relay exists for the channel in question. 5) The originating network is the one that created the relay. """ - relay = findRelay((irc.name, channel)) + relay = getRelay((irc.name, channel)) try: mlist = chanobj.prefixmodes except AttributeError: @@ -406,7 +406,7 @@ def relayJoins(irc, channel, users, ts, burst=True): if name == irc.name or not remoteirc.connected.is_set(): # Don't relay things to their source network... continue - remotechan = findRemoteChan(irc, remoteirc, channel) + remotechan = getRemoteChan(irc, remoteirc, channel) if remotechan is None: # If there is no link on our network for the user, don't # bother spawning it. @@ -446,7 +446,7 @@ def relayPart(irc, channel, user): if name == irc.name or not remoteirc.connected.is_set(): # Don't relay things to their source network... continue - remotechan = findRemoteChan(irc, remoteirc, channel) + remotechan = getRemoteChan(irc, remoteirc, channel) log.debug('(%s) relayPart: looking for %s/%s on %s', irc.name, user, irc.name, remoteirc.name) log.debug('(%s) relayPart: remotechan found as %s', irc.name, remotechan) remoteuser = getRemoteUser(irc, remoteirc, user, spawnIfMissing=False) @@ -469,7 +469,7 @@ whitelisted_cmodes = {'admin', 'allowinvite', 'autoop', 'ban', 'banexception', whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper', 'regdeaf', 'u_stripcolor', 'u_noctcp', 'wallops'} def relayModes(irc, remoteirc, sender, channel, modes=None): - remotechan = findRemoteChan(irc, remoteirc, channel) + remotechan = getRemoteChan(irc, remoteirc, channel) log.debug('(%s) Relay mode: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan) if remotechan is None: return @@ -506,7 +506,7 @@ def relayModes(irc, remoteirc, sender, channel, modes=None): irc.name, modechar, arg, remoteirc.name) # If the target is a remote user, get the real target # (original user). - arg = getLocalUser(irc, arg, targetirc=remoteirc) or \ + arg = getOrigUser(irc, arg, targetirc=remoteirc) or \ getRemoteUser(irc, remoteirc, arg, spawnIfMissing=False) log.debug("(%s) Relay mode: argument found as (%r, %r) " "for network %r.", @@ -547,7 +547,7 @@ def relayModes(irc, remoteirc, sender, channel, modes=None): def relayWhoisHandler(irc, target): user = irc.users[target] - orig = getLocalUser(irc, target) + orig = getOrigUser(irc, target) if orig: network, remoteuid = orig remotenick = world.networkobjects[network].users[remoteuid].nick @@ -568,7 +568,7 @@ utils.add_hook(handle_operup, 'PYLINK_CLIENT_OPERED') def handle_join(irc, numeric, command, args): channel = args['channel'] - if not findRelay((irc.name, channel)): + if not getRelay((irc.name, channel)): # No relay here, return. return ts = args['ts'] @@ -621,7 +621,7 @@ def handle_part(irc, numeric, command, args): for channel in channels: for netname, user in relayusers[(irc.name, numeric)].copy().items(): remoteirc = world.networkobjects[netname] - remotechan = findRemoteChan(irc, remoteirc, channel) + remotechan = getRemoteChan(irc, remoteirc, channel) if remotechan is None: continue remoteirc.proto.partClient(user, remotechan, text) @@ -636,7 +636,7 @@ def handle_privmsg(irc, numeric, command, args): text = args['text'] if target == irc.pseudoclient.uid: return - relay = findRelay((irc.name, target)) + relay = getRelay((irc.name, target)) remoteusers = relayusers[(irc.name, numeric)] # HACK: Don't break on sending to @#channel or similar. try: @@ -657,7 +657,7 @@ def handle_privmsg(irc, numeric, command, args): if utils.isChannel(target): for netname, user in relayusers[(irc.name, numeric)].items(): remoteirc = world.networkobjects[netname] - real_target = findRemoteChan(irc, remoteirc, target) + real_target = getRemoteChan(irc, remoteirc, target) if not real_target: continue real_target = prefix + real_target @@ -666,7 +666,7 @@ def handle_privmsg(irc, numeric, command, args): else: remoteirc.proto.messageClient(user, real_target, text) else: - remoteuser = getLocalUser(irc, target) + remoteuser = getOrigUser(irc, target) if remoteuser is None: return homenet, real_target = remoteuser @@ -693,15 +693,15 @@ def handle_kick(irc, source, command, args): target = args['target'] text = args['text'] kicker = source - relay = findRelay((irc.name, channel)) + relay = getRelay((irc.name, channel)) # Don't allow kicks to the PyLink client to be relayed. if relay is None or target == irc.pseudoclient.uid: return - origuser = getLocalUser(irc, target) + origuser = getOrigUser(irc, target) for name, remoteirc in world.networkobjects.items(): if irc.name == name or not remoteirc.connected.is_set(): continue - remotechan = findRemoteChan(irc, remoteirc, channel) + remotechan = getRemoteChan(irc, remoteirc, channel) log.debug('(%s) Relay kick: remotechan for %s on %s is %s', irc.name, channel, name, remotechan) if remotechan is None: continue @@ -717,7 +717,7 @@ def handle_kick(irc, source, command, args): log.debug('(%s) Relay kick: real target for %s is %s', irc.name, target, real_target) else: log.debug('(%s) Relay kick: target %s is an internal client, going to look up the real user', irc.name, target) - real_target = getLocalUser(irc, target, targetirc=remoteirc) + real_target = getOrigUser(irc, target, targetirc=remoteirc) if not checkClaim(irc, channel, kicker): log.debug('(%s) Relay kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target) # Home network is not in the channel's claim AND the kicker is not @@ -839,7 +839,7 @@ def handle_topic(irc, numeric, command, args): if irc.name == name or not remoteirc.connected.is_set(): continue - remotechan = findRemoteChan(irc, remoteirc, channel) + remotechan = getRemoteChan(irc, remoteirc, channel) # Don't send if the remote topic is the same as ours. if remotechan is None or topic == remoteirc.channels[remotechan].topic: continue @@ -858,7 +858,7 @@ utils.add_hook(handle_topic, 'TOPIC') def handle_kill(irc, numeric, command, args): target = args['target'] userdata = args['userdata'] - realuser = getLocalUser(irc, target) or userdata.__dict__.get('remote') + realuser = getOrigUser(irc, target) or userdata.__dict__.get('remote') log.debug('(%s) relay handle_kill: realuser is %r', irc.name, realuser) # Target user was remote: if realuser and realuser[0] != irc.name: @@ -868,7 +868,7 @@ def handle_kill(irc, numeric, command, args): if killcache.setdefault(irc.name, 0) <= 5: remoteirc = world.networkobjects[realuser[0]] for remotechan in remoteirc.channels.copy(): - localchan = findRemoteChan(remoteirc, irc, remotechan) + localchan = getRemoteChan(remoteirc, irc, remotechan) if localchan: modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser) @@ -920,9 +920,9 @@ def handle_invite(irc, source, command, args): target = args['target'] channel = args['channel'] if isRelayClient(irc, target): - remotenet, remoteuser = getLocalUser(irc, target) + remotenet, remoteuser = getOrigUser(irc, target) remoteirc = world.networkobjects[remotenet] - remotechan = findRemoteChan(irc, remoteirc, channel) + remotechan = getRemoteChan(irc, remoteirc, channel) remotesource = getRemoteUser(irc, remoteirc, source, spawnIfMissing=False) if remotesource is None: irc.msg(source, 'Error: You must be in a common channel ' @@ -961,7 +961,7 @@ utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT") def handle_save(irc, numeric, command, args): target = args['target'] - realuser = getLocalUser(irc, target) + realuser = getOrigUser(irc, target) log.debug('(%s) relay handle_save: %r got in a nick collision! Real user: %r', irc.name, target, realuser) if isRelayClient(irc, target) and realuser: @@ -1072,7 +1072,7 @@ def link(irc, source, args): if remotenet not in world.networkobjects: irc.msg(source, 'Error: No network named %r exists.' % remotenet) return - localentry = findRelay((irc.name, localchan)) + localentry = getRelay((irc.name, localchan)) if localentry: irc.msg(source, 'Error: Channel %r is already part of a relay.' % localchan) return @@ -1116,7 +1116,7 @@ def delink(irc, source, args): if not utils.isChannel(channel): irc.msg(source, 'Error: Invalid channel %r.' % channel) return - entry = findRelay((irc.name, channel)) + entry = getRelay((irc.name, channel)) if entry: if entry[0] == irc.name: # We own this channel. if not remotenet: @@ -1174,7 +1174,7 @@ def linkacl(irc, source, args): if not utils.isChannel(channel): irc.msg(source, 'Error: Invalid channel %r.' % channel) return - relay = findRelay((irc.name, channel)) + relay = getRelay((irc.name, channel)) if not relay: irc.msg(source, 'Error: No such relay %r exists.' % channel) return @@ -1215,7 +1215,7 @@ def showuser(irc, source, args): u = utils.nickToUid(irc, target) if u: try: - userpair = getLocalUser(irc, u) or (irc.name, u) + userpair = getOrigUser(irc, u) or (irc.name, u) remoteusers = relayusers[userpair].items() except KeyError: pass @@ -1231,7 +1231,7 @@ def showuser(irc, source, args): irc.msg(source, "\x02Relay nicks\x02: %s" % ', '.join(nicks)) relaychannels = [] for ch in irc.users[u].channels: - relay = findRelay((irc.name, ch)) + relay = getRelay((irc.name, ch)) if relay: relaychannels.append(''.join(relay)) if relaychannels and (utils.isOper(irc, source) or u == source): From 78080bde6b4bf56d772de264500f7cfe8741297b Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 17:36:41 -0700 Subject: [PATCH 33/70] relay: don't fail if oldchan is not given in MODE --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index b5014ea..6fd0104 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -805,7 +805,7 @@ def handle_mode(irc, numeric, command, args): if irc.name == name or not remoteirc.connected.is_set(): continue if utils.isChannel(target): - oldchan = args['oldchan'] + oldchan = args.get('oldchan') if checkClaim(irc, target, numeric, chanobj=oldchan): relayModes(irc, remoteirc, numeric, target, modes) else: # Mode change blocked by CLAIM. From c5b6658200a987b9b2db2a021605cc22adc1d9d1 Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 17:44:07 -0700 Subject: [PATCH 34/70] bots: more validation in "MODE" to prevent bad things from happening This adds a new "allow_forceset_usermodes" attribute to protocol modules, which determines whether the IRCd allows us to force usermode changes on other servers' clients. Also, make sure our target is a valid nick/UID/channel, and that the parsed modes are not empty! --- classes.py | 3 +++ plugins/bots.py | 30 +++++++++++++++++------------- protocols/inspircd.py | 3 +++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/classes.py b/classes.py index 9e5e6f5..e156370 100644 --- a/classes.py +++ b/classes.py @@ -396,6 +396,9 @@ class Protocol(): self.casemapping = 'rfc1459' self.hook_map = {} + # Whether the IRCd allows forcing user mode changes on other servers' clients. + self.allow_forceset_usermodes = False + class FakeProto(Protocol): """Dummy protocol module for testing purposes.""" def handle_events(self, data): diff --git a/plugins/bots.py b/plugins/bots.py index 771621d..82d594c 100644 --- a/plugins/bots.py +++ b/plugins/bots.py @@ -138,30 +138,34 @@ def kick(irc, source, args): def mode(irc, source, args): """ - Admin-only. Sets modes on from , where is either the nick of a PyLink client, or the SID of a PyLink server.""" + Admin-only. Sets modes on from , where is either the nick of a PyLink client, or the SID of a PyLink server. can be either a nick or a channel.""" utils.checkAuthenticated(irc, source, allowOper=False) try: modesource, target, modes = args[0], args[1], args[2:] except IndexError: irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.') return - if not modes: - irc.msg(source, "Error: No modes given to set!") - return - parsedmodes = utils.parseModes(irc, target, modes) - targetuid = utils.nickToUid(irc, target) - if targetuid: - target = targetuid - elif not utils.isChannel(target): + target = utils.nickToUid(irc, target) or target + if not (target in irc.users or target in irc.channels): irc.msg(source, "Error: Invalid channel or nick %r." % target) return + elif target in irc.users and not irc.proto.allow_forceset_usermodes: + irc.msg(source, "Error: this IRCd does not allow forcing user mode " + "changes on other servers' users!") + return + parsedmodes = utils.parseModes(irc, target, modes) + if not parsedmodes: + irc.msg(source, "Error: No valid modes were given.") + return if utils.isInternalServer(irc, modesource): + # Setting modes from a server. irc.proto.modeServer(modesource, target, parsedmodes) - irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) else: - sourceuid = utils.nickToUid(irc, modesource) - irc.proto.modeClient(sourceuid, target, parsedmodes) - irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) + # Setting modes from a client. + modesource = utils.nickToUid(irc, modesource) + irc.proto.modeClient(modesource, target, parsedmodes) + irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE', + {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) @utils.add_cmd def msg(irc, source, args): diff --git a/protocols/inspircd.py b/protocols/inspircd.py index f59e4df..c64055c 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -27,6 +27,9 @@ class InspIRCdProtocol(TS6BaseProtocol): self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) self.uidgen = {} + # Whether the IRCd allows forcing user mode changes on other servers' clients. + self.allow_forceset_usermodes = True + 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): """Spawns a client with nick on the given IRC connection. From 2e0a5e52e241e56f451d8a52fc61640129bdae2f Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 17:55:58 -0700 Subject: [PATCH 35/70] utils.parseModes: fix IndexError on empty query --- utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils.py b/utils.py index 69ae101..a8d26fd 100644 --- a/utils.py +++ b/utils.py @@ -170,11 +170,10 @@ def parseModes(irc, target, args): # B = Mode that changes a setting and always has a parameter. # C = Mode that changes a setting and only has a parameter when set. # D = Mode that changes a setting and never has a parameter. + assert args, 'No valid modes were supplied!' usermodes = not isChannel(target) prefix = '' modestring = args[0] - if not modestring: - return ValueError('No modes supplied in parseModes query: %r' % modes) args = args[1:] if usermodes: log.debug('(%s) Using irc.umodes for this query: %s', irc.name, irc.umodes) From 48573b603375a4ef6f84ea56bb18ed9208e54c5f Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 17:57:20 -0700 Subject: [PATCH 36/70] utils.parseModes: autoconvert nicks->UIDs for prefix modes, skipping if the target doesn't exist. --- utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/utils.py b/utils.py index 36c65a1..5a2349b 100644 --- a/utils.py +++ b/utils.py @@ -206,6 +206,13 @@ def parseModes(irc, target, args): # We're setting a prefix mode on someone (e.g. +o user1) log.debug('Mode %s: This mode is a prefix mode.', mode) arg = args.pop(0) + # Convert nicks to UIDs implicitly; most IRCds will want + # this already. + arg = nickToUid(irc, arg) or arg + if arg not in irc.users: # Target doesn't exist, skip it. + log.debug('(%s) Skipping setting mode "%s %s"; the ' + 'target doesn\'t seem to exist!') + continue elif prefix == '+' and mode in supported_modes['*C']: # Only has parameter when setting. log.debug('Mode %s: Only has parameter when setting.', mode) From b8d6e1e2ef4786bd9c351b7161636fcb7751692c Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 18:03:39 -0700 Subject: [PATCH 37/70] bots: still allow setting modes on internal clients --- plugins/bots.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/bots.py b/plugins/bots.py index 82d594c..8c454f2 100644 --- a/plugins/bots.py +++ b/plugins/bots.py @@ -149,7 +149,8 @@ def mode(irc, source, args): if not (target in irc.users or target in irc.channels): irc.msg(source, "Error: Invalid channel or nick %r." % target) return - elif target in irc.users and not irc.proto.allow_forceset_usermodes: + elif target in irc.users and not utils.isInternalClient(irc, target) and \ + not irc.proto.allow_forceset_usermodes: irc.msg(source, "Error: this IRCd does not allow forcing user mode " "changes on other servers' users!") return From 7ced47e9b37382e2d40bd11f163bc2519f66fe96 Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 18:09:39 -0700 Subject: [PATCH 38/70] bots: don't allow setting umode +o on InspIRCd users (forbidden and causes desync) --- classes.py | 3 +++ plugins/bots.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/classes.py b/classes.py index e156370..6cf9630 100644 --- a/classes.py +++ b/classes.py @@ -399,6 +399,9 @@ class Protocol(): # Whether the IRCd allows forcing user mode changes on other servers' clients. self.allow_forceset_usermodes = False + # Ditto, but for setting user mode +o. InspIRCd forbids this as an example. + self.allow_forceoper = False + class FakeProto(Protocol): """Dummy protocol module for testing purposes.""" def handle_events(self, data): diff --git a/plugins/bots.py b/plugins/bots.py index 8c454f2..c3d0f5a 100644 --- a/plugins/bots.py +++ b/plugins/bots.py @@ -146,17 +146,21 @@ def mode(irc, source, args): irc.msg(source, 'Error: Not enough arguments. Needs 3: source nick, target, modes to set.') return target = utils.nickToUid(irc, target) or target + extclient = target in irc.users and not utils.isInternalClient(irc, target) + parsedmodes = utils.parseModes(irc, target, modes) if not (target in irc.users or target in irc.channels): irc.msg(source, "Error: Invalid channel or nick %r." % target) return - elif target in irc.users and not utils.isInternalClient(irc, target) and \ - not irc.proto.allow_forceset_usermodes: + elif not parsedmodes: + irc.msg(source, "Error: No valid modes were given.") + return + elif extclient and not irc.proto.allow_forceset_usermodes: irc.msg(source, "Error: this IRCd does not allow forcing user mode " "changes on other servers' users!") return - parsedmodes = utils.parseModes(irc, target, modes) - if not parsedmodes: - irc.msg(source, "Error: No valid modes were given.") + elif extclient and ("+o", None) in parsedmodes and not irc.proto.allow_forceoper: + irc.msg(source, "Error: this IRCd does not allow forcing an oper up " + "for other servers' users!") return if utils.isInternalServer(irc, modesource): # Setting modes from a server. From b531a180dca94390fc86a48900002c72a9ed8f8e Mon Sep 17 00:00:00 2001 From: James Lu Date: Mon, 14 Sep 2015 18:43:19 -0700 Subject: [PATCH 39/70] commands: add a "showchan" command --- plugins/commands.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/plugins/commands.py b/plugins/commands.py index 81b9ea1..f714f79 100644 --- a/plugins/commands.py +++ b/plugins/commands.py @@ -126,6 +126,52 @@ def showuser(irc, source, args): (userobj.realhost, userobj.ip, userobj.away or '\x1D(not set)\x1D')) f('\x02Channels\x02: %s' % (' '.join(userobj.channels).strip() or '\x1D(none)\x1D')) +@utils.add_cmd +def showchan(irc, source, args): + """ + + Shows information about .""" + try: + channel = utils.toLower(irc, args[0]) + except IndexError: + irc.msg(source, "Error: Not enough arguments. Needs 1: channel.") + return + if channel not in irc.channels: + irc.msg(source, 'Error: Unknown channel %r.' % channel) + return + + f = lambda s: irc.msg(source, s) + c = irc.channels[channel] + # Only show verbose info if caller is oper or is in the target channel. + verbose = source in c.users or utils.isOper(irc, source) + secret = ('s', None) in c.modes + if secret and not verbose: + # Hide secret channels from normal users. + irc.msg(source, 'Error: Unknown channel %r.' % channel) + return + + nicks = [irc.users[u].nick for u in c.users] + pmodes = ('owner', 'admin', 'op', 'halfop', 'voice') + + f('Information on channel \x02%s\x02:' % channel) + f('\x02Channel topic\x02: %s' % c.topic) + f('\x02Channel creation time\x02: %s (%s)' % (ctime(c.ts), c.ts)) + # Show only modes that aren't list-style modes. + modes = utils.joinModes([m for m in c.modes if m[0] not in irc.cmodes['*A']]) + f('\x02Channel modes\x02: %s' % modes) + if verbose: + nicklist = [] + # Iterate over the user list, sorted by nick. + for user, nick in sorted(zip(c.users, nicks), + key=lambda userpair: userpair[1].lower()): + prefixmodes = [irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') + for pmode in pmodes if user in c.prefixmodes[pmode+'s']] + nicklist.append(''.join(prefixmodes) + nick) + + while nicklist[:20]: # 20 nicks per line to prevent message cutoff. + f('\x02User list\x02: %s' % ' '.join(nicklist[:20])) + nicklist = nicklist[20:] + @utils.add_cmd def shutdown(irc, source, args): """takes no arguments. From 04ca3221545e57ffa496de601cbb27f8964d5fed Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 16 Sep 2015 20:59:08 -0700 Subject: [PATCH 40/70] relay: skip claim checks when claim list is empty for a channel --- plugins/relay.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 6fd0104..df4aa1c 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -250,7 +250,8 @@ def getOrigUser(irc, user, targetirc=None): return remoteuser def getRelay(chanpair): - """Finds a matching relay for the given (network name, channel) pair.""" + """Finds the matching relay entry name for the given (network name, channel) + pair, if one exists.""" if chanpair in db: # This chanpair is a shared channel; others link to it return chanpair # This chanpair is linked *to* a remote channel @@ -336,11 +337,12 @@ def checkClaim(irc, channel, sender, chanobj=None): Checks whether the sender of a kick/mode change passes CLAIM checks for a given channel. This returns True if any of the following criteria are met: - 1) The originating network is in the CLAIM list for the relay in question. - 2) The sender is halfop or above in the channel. - 3) The sender is a PyLink client/server (checks are suppressed in this case). - 4) No relay exists for the channel in question. - 5) The originating network is the one that created the relay. + 1) No relay exists for the channel in question. + 2) The originating network is the one that created the relay. + 3) The CLAIM list for the relay in question is empty. + 4) The originating network is in the CLAIM list for the relay in question. + 5) The sender is halfop or above in the channel. + 6) The sender is a PyLink client/server (checks are suppressed in this case). """ relay = getRelay((irc.name, channel)) try: @@ -350,7 +352,8 @@ def checkClaim(irc, channel, sender, chanobj=None): sender_modes = getPrefixModes(irc, irc, channel, sender, mlist=mlist) log.debug('(%s) relay.checkClaim: sender modes (%s/%s) are %s (mlist=%s)', irc.name, sender, channel, sender_modes, mlist) - return (not relay) or irc.name == relay[0] or irc.name in db[relay]['claim'] or \ + return (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \ + irc.name in db[relay]['claim'] or \ any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ or utils.isInternalClient(irc, sender) or \ utils.isInternalServer(irc, sender) From f92c2f731c5b3278c219780213a5dee320e34a6a Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 16 Sep 2015 20:59:32 -0700 Subject: [PATCH 41/70] relay: CLAIM command! Closes #51. --- plugins/relay.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/plugins/relay.py b/plugins/relay.py index df4aa1c..df5d21a 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -1251,3 +1251,37 @@ def save(irc, source, args): else: irc.msg(source, 'Error: You are not authenticated!') return + +@utils.add_cmd +def claim(irc, source, args): + """ [] + + Sets the CLAIM for a channel to a case-sensitive list of networks. If no list of networks is given, shows which networks have claim over the channel. A single hyphen (-) can also be given as a list of networks to remove claim from the channel entirely. + + CLAIM is a way of enforcing network ownership for a channel, similarly to Janus. Unless the list is empty, only networks on the CLAIM list for a channel (plus the creating network) are allowed to override kicks, mode changes, and topic changes in it - attempts from other networks' opers to do this are simply blocked or reverted.""" + utils.checkAuthenticated(irc, source) + try: + channel = utils.toLower(irc, args[0]) + except IndexError: + irc.msg(source, "Error: Not enough arguments. Needs 1-2: channel, list of networks (optional).") + return + + # We override getRelay() here to limit the search to the current network. + relay = (irc.name, channel) + if relay not in db: + irc.msg(source, 'Error: No such relay %r exists.' % channel) + return + claimed = db[relay]["claim"] + try: + nets = args[1].strip() + except IndexError: # No networks given. + irc.msg(source, 'Channel \x02%s\x02 is claimed by: %s' % + (channel, ', '.join(claimed) or '\x1D(none)\x1D')) + else: + if nets == '-' or not nets: + claimed = set() + else: + claimed = set(nets.split(',')) + db[relay]["claim"] = claimed + irc.msg(source, 'CLAIM for channel \x02%s\x02 set to: %s' % + (channel, ', '.join(claimed) or '\x1D(none)\x1D')) From 7011aa3b3689816939cc64693ea911c4d88f8227 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 16 Sep 2015 21:23:09 -0700 Subject: [PATCH 42/70] relay: send kick rejoins from CLAIM from the main relay server --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index df5d21a..cab86ea 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -729,7 +729,7 @@ def handle_kick(irc, source, command, args): # kick ops, admins can't kick owners, etc. modes = getPrefixModes(remoteirc, irc, remotechan, real_target) # Join the kicked client back with its respective modes. - irc.proto.sjoinServer(getRemoteSid(irc, remoteirc), channel, [(modes, target)]) + irc.proto.sjoinServer(irc.sid, channel, [(modes, target)]) if kicker in irc.users: log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.', irc.name, args['text'], irc.users[source].nick, From b87e34efa25067fb4dacf1d6515730325b20a47f Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 16 Sep 2015 21:32:27 -0700 Subject: [PATCH 43/70] Slightly more consistent logging format... --- classes.py | 8 +++++--- protocols/ts6.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/classes.py b/classes.py index 6cf9630..735e80f 100644 --- a/classes.py +++ b/classes.py @@ -236,15 +236,17 @@ class Irc(): if command in hook_map: hook_cmd = hook_map[command] hook_cmd = parsed_args.get('parse_as') or hook_cmd - log.debug('Parsed args %r received from %s handler (calling hook %s)', parsed_args, command, hook_cmd) + log.debug('(%s) Parsed args %r received from %s handler (calling hook %s)', + self.name, parsed_args, command, hook_cmd) # Iterate over hooked functions, catching errors accordingly for hook_func in world.command_hooks[hook_cmd]: try: - log.debug('Calling function %s', hook_func) + log.debug('(%s) Calling function %s', self.name, hook_func) hook_func(self, numeric, command, parsed_args) except Exception: # We don't want plugins to crash our servers... - log.exception('Unhandled exception caught in %r' % hook_func) + log.exception('(%s) Unhandled exception caught in %r', + self.name, hook_func) continue def send(self, data): diff --git a/protocols/ts6.py b/protocols/ts6.py index e097b98..6cf150f 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -99,7 +99,7 @@ class TS6Protocol(TS6BaseProtocol): self.irc.channels[channel].modes.clear() for p in self.irc.channels[channel].prefixmodes.values(): p.clear() - log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, ts, + log.debug("(%s) sending SJOIN to %s with ts %s (that's %r)", self.irc.name, channel, ts, time.strftime("%c", time.localtime(ts))) modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']] changedmodes = [] From 069cd628658703e2a41dd785b8d81e07738a766a Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 17 Sep 2015 19:01:54 -0700 Subject: [PATCH 44/70] core: allow marking spawned clients as manipulatable or protected This flag determines whether the client should be manipulated by commands like bots.py's MODE/QUIT/JOIN commands, or protected from them (services). --- classes.py | 11 +++++++++-- protocols/inspircd.py | 5 +++-- protocols/ts6.py | 5 +++-- utils.py | 9 +++++++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/classes.py b/classes.py index 735e80f..0834a4f 100644 --- a/classes.py +++ b/classes.py @@ -274,7 +274,9 @@ class Irc(): host = self.serverdata["hostname"] log.info('(%s) Connected! Spawning main client %s.', self.name, nick) olduserobj = self.pseudoclient - self.pseudoclient = self.proto.spawnClient(nick, ident, host, modes={("+o", None)}) + self.pseudoclient = self.proto.spawnClient(nick, ident, host, + modes={("+o", None)}, + manipulatable=True) for chan in self.serverdata['channels']: self.proto.joinClient(self.pseudoclient.uid, chan) # PyLink internal hook called when spawnMain is called and the @@ -284,7 +286,7 @@ class Irc(): class IrcUser(): def __init__(self, nick, ts, uid, ident='null', host='null', realname='PyLink dummy client', realhost='null', - ip='0.0.0.0'): + ip='0.0.0.0', manipulatable=False): self.nick = nick self.ts = ts self.uid = uid @@ -299,6 +301,11 @@ class IrcUser(): self.channels = set() self.away = '' + # Whether the client should be marked as manipulatable + # (i.e. we are allowed to play with it using bots.py's commands). + # For internal services clients, this should always be False. + self.manipulatable = manipulatable + def __repr__(self): return repr(self.__dict__) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index c64055c..488372e 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -31,7 +31,8 @@ class InspIRCdProtocol(TS6BaseProtocol): self.allow_forceset_usermodes = True 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): + server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, + manipulatable=False): """Spawns a client with nick on the given IRC connection. Note: No nick collision / valid nickname checks are done here; it is @@ -47,7 +48,7 @@ class InspIRCdProtocol(TS6BaseProtocol): 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) + realhost=realhost, ip=ip, manipulatable=manipulatable) utils.applyModes(self.irc, uid, modes) self.irc.servers[server].users.add(uid) self._send(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}" diff --git a/protocols/ts6.py b/protocols/ts6.py index 6cf150f..dc010b4 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -21,7 +21,8 @@ class TS6Protocol(TS6BaseProtocol): self.uidgen = {} 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): + server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, + manipulatable=False): """Spawns a client with nick on the given IRC connection. Note: No nick collision / valid nickname checks are done here; it is @@ -40,7 +41,7 @@ class TS6Protocol(TS6BaseProtocol): 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) + realhost=realhost, ip=ip, manipulatable=manipulatable) utils.applyModes(self.irc, uid, modes) self.irc.servers[server].users.add(uid) self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " diff --git a/utils.py b/utils.py index 5a2349b..06b1904 100644 --- a/utils.py +++ b/utils.py @@ -462,6 +462,15 @@ def checkAuthenticated(irc, uid, allowAuthed=True, allowOper=True): raise NotAuthenticatedError("You are not authenticated!") return True +def isManipulatableClient(irc, uid): + """ + Returns whether the given user is marked as an internal, manipulatable + client. Usually, automatically spawned services clients should have this + set True to prevent interactions with opers (like mode changes) from + causing desyncs. + """ + return isInternalClient(irc, uid) and irc.users[uid].manipulatable + def getHostmask(irc, user): """Gets the hostmask of the given user, if present.""" userobj = irc.users.get(user) From 10f3cd1fb8d0fc7fac9a96f0a84ad71eec9ce00e Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 17 Sep 2015 19:07:16 -0700 Subject: [PATCH 45/70] bots: limit most commands to non-protected internal PyLink clients --- plugins/bots.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/bots.py b/plugins/bots.py index c3d0f5a..6b6c438 100644 --- a/plugins/bots.py +++ b/plugins/bots.py @@ -22,7 +22,7 @@ def spawnclient(irc, source, args): except ValueError: irc.msg(source, "Error: Not enough arguments. Needs 3: nick, user, host.") return - irc.proto.spawnClient(nick, ident, host) + irc.proto.spawnClient(nick, ident, host, manipulatable=True) @utils.add_cmd def quit(irc, source, args): @@ -40,6 +40,9 @@ def quit(irc, source, args): return u = utils.nickToUid(irc, nick) quitmsg = ' '.join(args[1:]) or 'Client Quit' + if not utils.isManipulatableClient(irc, u): + irc.msg(source, "Error: Cannot force quit a protected PyLink services client.") + return irc.proto.quitClient(u, quitmsg) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) @@ -57,6 +60,9 @@ def joinclient(irc, source, args): irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.") return u = utils.nickToUid(irc, nick) + if not utils.isManipulatableClient(irc, u): + irc.msg(source, "Error: Cannot force join a protected PyLink services client.") + return for channel in clist: if not utils.isChannel(channel): irc.msg(source, "Error: Invalid channel name %r." % channel) @@ -85,6 +91,9 @@ def nick(irc, source, args): elif not utils.isNick(newnick): irc.msg(source, 'Error: Invalid nickname %r.' % newnick) return + elif not utils.isManipulatableClient(irc, u): + irc.msg(source, "Error: Cannot force nick changes for a protected PyLink services client.") + return irc.proto.nickClient(u, newnick) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}]) @@ -102,6 +111,9 @@ def part(irc, source, args): irc.msg(source, "Error: Not enough arguments. Needs 2: nick, comma separated list of channels.") return u = utils.nickToUid(irc, nick) + if not utils.isManipulatableClient(irc, u): + irc.msg(source, "Error: Cannot force part a protected PyLink services client.") + return for channel in clist: if not utils.isChannel(channel): irc.msg(source, "Error: Invalid channel name %r." % channel) @@ -148,19 +160,15 @@ def mode(irc, source, args): target = utils.nickToUid(irc, target) or target extclient = target in irc.users and not utils.isInternalClient(irc, target) parsedmodes = utils.parseModes(irc, target, modes) - if not (target in irc.users or target in irc.channels): + ischannel = target in irc.channels + if not (target in irc.users or ischannel): irc.msg(source, "Error: Invalid channel or nick %r." % target) return elif not parsedmodes: irc.msg(source, "Error: No valid modes were given.") return - elif extclient and not irc.proto.allow_forceset_usermodes: - irc.msg(source, "Error: this IRCd does not allow forcing user mode " - "changes on other servers' users!") - return - elif extclient and ("+o", None) in parsedmodes and not irc.proto.allow_forceoper: - irc.msg(source, "Error: this IRCd does not allow forcing an oper up " - "for other servers' users!") + elif not (ischannel or utils.isManipulatableClient(irc, target)): + irc.msg(source, "Error: Can only set modes on channels or non-protected PyLink clients.") return if utils.isInternalServer(irc, modesource): # Setting modes from a server. From fb91ff8ea34f1f0904d5f26d7883a171b7ca705c Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 17 Sep 2015 19:07:37 -0700 Subject: [PATCH 46/70] Remove allow_abcd attributes from protocols (not needed anymore) --- classes.py | 6 ------ protocols/inspircd.py | 3 --- 2 files changed, 9 deletions(-) diff --git a/classes.py b/classes.py index 0834a4f..6d3d340 100644 --- a/classes.py +++ b/classes.py @@ -405,12 +405,6 @@ class Protocol(): self.casemapping = 'rfc1459' self.hook_map = {} - # Whether the IRCd allows forcing user mode changes on other servers' clients. - self.allow_forceset_usermodes = False - - # Ditto, but for setting user mode +o. InspIRCd forbids this as an example. - self.allow_forceoper = False - class FakeProto(Protocol): """Dummy protocol module for testing purposes.""" def handle_events(self, data): diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 488372e..7a8086c 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -27,9 +27,6 @@ class InspIRCdProtocol(TS6BaseProtocol): self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) self.uidgen = {} - # Whether the IRCd allows forcing user mode changes on other servers' clients. - self.allow_forceset_usermodes = True - 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=False): From 8faf86aa8f742b4f69bf080f1e5bb970d5109056 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 17 Sep 2015 19:15:51 -0700 Subject: [PATCH 47/70] relay: rejoin killed users to the RIGHT channels --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index 286956e..804e196 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -670,7 +670,7 @@ def handle_kill(irc, numeric, command, args): # client and rejoin it to its channels. del relayusers[realuser][irc.name] remoteirc = world.networkobjects[realuser[0]] - for remotechan in remoteirc.channels.copy(): + for remotechan in remoteirc.users[realuser[1]].channels: localchan = findRemoteChan(remoteirc, irc, remotechan) if localchan: modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) From 70227bf3e0ffcccfed21fadb83c1d97ff23a49d1 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 17 Sep 2015 19:22:34 -0700 Subject: [PATCH 48/70] relay: use utils.checkAuthenticated() for checking oper status --- plugins/relay.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index cab86ea..50b4c58 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -870,7 +870,7 @@ def handle_kill(irc, numeric, command, args): del relayusers[realuser][irc.name] if killcache.setdefault(irc.name, 0) <= 5: remoteirc = world.networkobjects[realuser[0]] - for remotechan in remoteirc.channels.copy(): + for remotechan in remoteirc.users[realuser[1]].channels: localchan = getRemoteChan(remoteirc, irc, remotechan) if localchan: modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) @@ -1011,9 +1011,7 @@ def create(irc, source, args): if source not in irc.channels[channel].users: irc.msg(source, 'Error: You must be in %r to complete this operation.' % channel) return - if not utils.isOper(irc, source): - irc.msg(source, 'Error: You must be opered in order to complete this operation.') - return + utils.checkAuthenticated(irc, source) db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()} initializeChannel(irc, channel) irc.msg(source, 'Done.') @@ -1031,9 +1029,7 @@ def destroy(irc, source, args): if not utils.isChannel(channel): irc.msg(source, 'Error: Invalid channel %r.' % channel) return - if not utils.isOper(irc, source): - irc.msg(source, 'Error: You must be opered in order to complete this operation.') - return + utils.checkAuthenticated(irc, source) entry = (irc.name, channel) if entry in db: @@ -1069,9 +1065,7 @@ def link(irc, source, args): if source not in irc.channels[localchan].users: irc.msg(source, 'Error: You must be in %r to complete this operation.' % localchan) return - if not utils.isOper(irc, source): - irc.msg(source, 'Error: You must be opered in order to complete this operation.') - return + utils.checkAuthenticated(irc, source) if remotenet not in world.networkobjects: irc.msg(source, 'Error: No network named %r exists.' % remotenet) return @@ -1113,9 +1107,7 @@ def delink(irc, source, args): remotenet = args[1].lower() except IndexError: remotenet = None - if not utils.isOper(irc, source): - irc.msg(source, 'Error: You must be opered in order to complete this operation.') - return + utils.checkAuthenticated(irc, source) if not utils.isChannel(channel): irc.msg(source, 'Error: Invalid channel %r.' % channel) return @@ -1165,9 +1157,7 @@ def linkacl(irc, source, args): Allows blocking / unblocking certain networks from linking to a relay, based on a blacklist. LINKACL LIST returns a list of blocked networks for a channel, while the ALLOW and DENY subcommands allow manipulating this blacklist.""" missingargs = "Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)." - if not utils.isOper(irc, source): - irc.msg(source, 'Error: You must be opered in order to complete this operation.') - return + utils.checkAuthenticated(irc, source) try: cmd = args[0].lower() channel = utils.toLower(irc, args[1]) @@ -1245,12 +1235,9 @@ def save(irc, source, args): """takes no arguments. Saves the relay database to disk.""" - if utils.isOper(irc, source): - exportDB() - irc.msg(source, 'Done.') - else: - irc.msg(source, 'Error: You are not authenticated!') - return + utils.checkAuthenticated(irc, source) + exportDB() + irc.msg(source, 'Done.') @utils.add_cmd def claim(irc, source, args): From cb83db4c1c61569639b57cb627a8ef946f69fc6f Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 17 Sep 2015 19:24:38 -0700 Subject: [PATCH 49/70] relay: don't allow creating a channel that's already part of a relay --- plugins/relay.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/relay.py b/plugins/relay.py index 804e196..b196605 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -808,6 +808,10 @@ def create(irc, source, args): if not utils.isOper(irc, source): utils.msg(irc, source, 'Error: You must be opered in order to complete this operation.') return + localentry = findRelay((irc.name, channel)) + if localentry: + utils.msg(irc, source, 'Error: Channel %r is already part of a relay.' % channel) + return db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), 'blocked_nets': set()} initializeChannel(irc, channel) utils.msg(irc, source, 'Done.') From 63189e9990dafbd71c5dbac2b89b3e3d2c9c76c7 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 17 Sep 2015 19:30:38 -0700 Subject: [PATCH 50/70] relay: look at the right prefix mode list when rejoining from KILL --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index b196605..3112479 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -673,7 +673,7 @@ def handle_kill(irc, numeric, command, args): for remotechan in remoteirc.users[realuser[1]].channels: localchan = findRemoteChan(remoteirc, irc, remotechan) if localchan: - modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) + modes = getPrefixModes(remoteirc, irc, remotechan, realuser[1]) log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser) client = getRemoteUser(remoteirc, irc, realuser[1]) irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)]) From 504e2683fb8fdd1f4f79c4cd4627a6528ca86cd5 Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 18 Sep 2015 22:05:51 -0700 Subject: [PATCH 51/70] relay: hide +s/+p channels in LINKED output (closes #110) --- plugins/relay.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/relay.py b/plugins/relay.py index f63b0cc..0f76b7b 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -1148,6 +1148,16 @@ def linked(irc, source, args): # Sort relay DB by channel name, and then sort. for k, v in sorted(db.items(), key=lambda channel: channel[0][1]): s = '\x02%s%s\x02 ' % k + remoteirc = world.networkobjects.get(k[0]) + channel = k[1] + if remoteirc and channel in remoteirc.channels: + c = remoteirc.channels[channel] + if ('s', None) in c.modes or ('p', None) in c.modes: + # Only show hidden channels to opers. + if utils.isOper(irc, source): + s += '\x02[secret]\x02 ' + else: + continue if v['links']: s += ' '.join([''.join(link) for link in v['links']]) else: From c3e8420aa0cec347eb6e1a2cc5d8f92995c4a4ca Mon Sep 17 00:00:00 2001 From: James Lu Date: Fri, 18 Sep 2015 22:11:27 -0700 Subject: [PATCH 52/70] relay/ts6_common: be more error tolerant with network (dis)connections --- plugins/relay.py | 34 +++++++++++++++++++++++----------- protocols/ts6_common.py | 1 + 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 0f76b7b..f0f2652 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -30,8 +30,6 @@ killcache = ExpiringDict(max_len=5, max_age_seconds=10) def initializeAll(irc): """Initializes all relay channels for the given IRC object.""" - log.debug('(%s) initializeAll: waiting for world.started', irc.name) - world.started.wait() for chanpair, entrydata in db.items(): network, channel = chanpair initializeChannel(irc, channel) @@ -149,7 +147,12 @@ def getRemoteSid(irc, remoteirc): try: sid = relayservers[irc.name][remoteirc.name] except KeyError: - sid = irc.proto.spawnServer('%s.relay' % remoteirc.name) + try: + sid = irc.proto.spawnServer('%s.relay' % remoteirc.name) + except ValueError: # Network not initialized yet. + log.exception('(%s) Failed to spawn server for %r:', + irc.name, remoteirc.name) + return relayservers[irc.name][remoteirc.name] = sid return sid @@ -203,10 +206,15 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): if hideoper_mode: modes.append((hideoper_mode, None)) rsid = getRemoteSid(remoteirc, irc) - u = remoteirc.proto.spawnClient(nick, ident=ident, - host=host, realname=realname, - modes=modes, ts=userobj.ts, - opertype=opertype, server=rsid).uid + try: + u = remoteirc.proto.spawnClient(nick, ident=ident, + host=host, realname=realname, + modes=modes, ts=userobj.ts, + opertype=opertype, server=rsid).uid + except ValueError: + log.exception('(%s) Failed to spawn relay user %s on %s.', irc.name, + nick, remoteirc.name) + return remoteirc.users[u].remote = (irc.name, user) remoteirc.users[u].opertype = opertype away = userobj.away @@ -955,10 +963,14 @@ def handle_disconnect(irc, numeric, command, args): for name, ircobj in world.networkobjects.items(): if name != irc.name: rsid = getRemoteSid(ircobj, irc) - ircobj.proto.squitServer(ircobj.sid, rsid, text='Home network lost connection.') - del relayservers[name][irc.name] - del relayservers[irc.name] - # handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Home network lost connection.'}) + # Let's be super extra careful here... + if rsid and name in relayservers and irc.name in relayservers[name]: + ircobj.proto.squitServer(ircobj.sid, rsid, text='Home network lost connection.') + del relayservers[name][irc.name] + try: + del relayservers[irc.name] + except KeyError: + pass utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT") diff --git a/protocols/ts6_common.py b/protocols/ts6_common.py index 178d755..22a3f76 100644 --- a/protocols/ts6_common.py +++ b/protocols/ts6_common.py @@ -201,6 +201,7 @@ class TS6BaseProtocol(Protocol): split_server = args[0] affected_users = [] log.info('(%s) Netsplit on server %s', self.irc.name, split_server) + assert split_server in self.irc.servers, "Tried to split a server (%s) that didn't exist!" % split_server # Prevent RuntimeError: dictionary changed size during iteration old_servers = self.irc.servers.copy() for sid, data in old_servers.items(): From 6ee2ec8a2d52317b807b0a0de228a58a604cf416 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 10:17:25 -0700 Subject: [PATCH 53/70] relay: make oper status/IP hiding configurable Closes #108. --- config.yml.example | 16 ++++++++++++++++ plugins/relay.py | 19 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/config.yml.example b/config.yml.example index e11cd60..c638978 100644 --- a/config.yml.example +++ b/config.yml.example @@ -20,6 +20,22 @@ login: user: admin password: changeme +relay: + # This block defines various options for the Relay plugin. You don't need this + # if you aren't using it. + + # Determines whether remote opers will have user mode +H (hideoper) set on them. + # This has the benefit of lowering the oper count in /lusers and /stats (P|p), + # but only on IRCds that supported the mode. + # It defaults to true if not set. + hideoper: true + + # Determines whether real IPs should be sent across the relay. You should + # generally have a consensus with your linked networks whether this should + # be turned on. You will see other networks' user IP addresses, and they + # will see yours. + show_ips: false + servers: yournet: # Server IP, port, and passwords diff --git a/plugins/relay.py b/plugins/relay.py index f0f2652..035da09 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -203,14 +203,29 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): # Set hideoper on remote opers, to prevent inflating # /lusers and various /stats hideoper_mode = remoteirc.umodes.get('hideoper') - if hideoper_mode: + try: + use_hideoper = irc.conf['relay']['hideoper'] + except KeyError: + use_hideoper = True + if hideoper_mode and use_hideoper: modes.append((hideoper_mode, None)) rsid = getRemoteSid(remoteirc, irc) + try: + showRealIP = irc.conf['relay']['show_ips'] + except KeyError: + showRealIP = False + if showRealIP: + ip = userobj.ip + realhost = userobj.realhost + else: + realhost = None + ip = '0.0.0.0' try: u = remoteirc.proto.spawnClient(nick, ident=ident, host=host, realname=realname, modes=modes, ts=userobj.ts, - opertype=opertype, server=rsid).uid + opertype=opertype, server=rsid, + ip=ip, realhost=realhost).uid except ValueError: log.exception('(%s) Failed to spawn relay user %s on %s.', irc.name, nick, remoteirc.name) From c77f92357abadc881af8c27e9ddbc778074eb580 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 10:25:50 -0700 Subject: [PATCH 54/70] coreplugin: show real IP/host in WHOIS --- coreplugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/coreplugin.py b/coreplugin.py index 9effdab..4e9ce27 100644 --- a/coreplugin.py +++ b/coreplugin.py @@ -44,7 +44,9 @@ def handle_whois(irc, source, command, args): target = args['target'] user = irc.users.get(target) if user is None: - log.warning('(%s) Got a WHOIS request for %r from %r, but the target doesn\'t exist in irc.users!', irc.name, target, source) + log.warning('(%s) Got a WHOIS request for %r from %r, but the target ' + 'doesn\'t exist in irc.users!', irc.name, target, source) + return f = irc.proto.numericServer server = utils.clientToServer(irc, target) or irc.sid nick = user.nick @@ -86,6 +88,7 @@ def handle_whois(irc, source, command, args): # 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd. # Only show this to opers! if sourceisOper: + f(server, 378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip)) f(server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes))) # 317: shows idle and signon time. However, we don't track the user's real # idle time, so we simply return 0. From 19c4c2ce3f512cdfe2ac62139c00ee29a67249c3 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 10:31:43 -0700 Subject: [PATCH 55/70] classes: split up run() into run() and runline() This helps the debugging process, by allowing us to also inject lines directly into the protocol module. --- classes.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/classes.py b/classes.py index 6d3d340..628f627 100644 --- a/classes.py +++ b/classes.py @@ -207,21 +207,24 @@ class Irc(): line = line.strip(b'\r') # FIXME: respect other encodings? line = line.decode("utf-8", "replace") - log.debug("(%s) <- %s", self.name, line) - hook_args = None - try: - hook_args = self.proto.handle_events(line) - except Exception: - log.exception('(%s) Caught error in handle_events, disconnecting!', self.name) - log.error('(%s) The offending line was: <- %s', self.name, line) - return - # Only call our hooks if there's data to process. Handlers that support - # hooks will return a dict of parsed arguments, which can be passed on - # to plugins and the like. For example, the JOIN handler will return - # something like: {'channel': '#whatever', 'users': ['UID1', 'UID2', - # 'UID3']}, etc. - if hook_args is not None: - self.callHooks(hook_args) + self.runline(line) + + def runline(self, line): + """Sends a command to the protocol module.""" + log.debug("(%s) <- %s", self.name, line) + try: + hook_args = self.proto.handle_events(line) + except Exception: + log.exception('(%s) Caught error in handle_events, disconnecting!', self.name) + log.error('(%s) The offending line was: <- %s', self.name, line) + return + # Only call our hooks if there's data to process. Handlers that support + # hooks will return a dict of parsed arguments, which can be passed on + # to plugins and the like. For example, the JOIN handler will return + # something like: {'channel': '#whatever', 'users': ['UID1', 'UID2', + # 'UID3']}, etc. + if hook_args is not None: + self.callHooks(hook_args) def callHooks(self, hook_args): numeric, command, parsed_args = hook_args From 2c23cbe01ed3cfe455f5df4f1e7f9957eef3b5c7 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 10:32:45 -0700 Subject: [PATCH 56/70] relay: fix some logging peculiarities in handle_kick? --- plugins/relay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 035da09..f052a33 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -754,9 +754,9 @@ def handle_kick(irc, source, command, args): # Join the kicked client back with its respective modes. irc.proto.sjoinServer(irc.sid, channel, [(modes, target)]) if kicker in irc.users: - log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.', + log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s on %s.', irc.name, args['text'], irc.users[source].nick, - remoteirc.users[real_target].nick, remoteirc.name, channel) + remoteirc.users[real_target].nick, channel) irc.msg(kicker, "This channel is claimed; your kick to " "%s has been blocked because you are not " "(half)opped." % channel, notice=True) From dfe09263b94e75e7a4f5fd7d678462125509fdd5 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 10:39:05 -0700 Subject: [PATCH 57/70] plugins/exec: add 'eval' command too --- plugins/exec.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/exec.py b/plugins/exec.py index 21bdd5d..3083d69 100755 --- a/plugins/exec.py +++ b/plugins/exec.py @@ -19,3 +19,17 @@ def _exec(irc, source, args): log.info('(%s) Executing %r for %s', irc.name, args, utils.getHostmask(irc, source)) exec(args, globals(), locals()) utils.add_cmd(_exec, 'exec') + +def _eval(irc, source, args): + """ + + Admin-only. Evaluates the given Python expression and returns the result. + \x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02""" + utils.checkAuthenticated(irc, source, allowOper=False) + args = ' '.join(args) + if not args.strip(): + irc.msg(source, 'No code entered!') + return + log.info('(%s) Evaluating %r for %s', irc.name, args, utils.getHostmask(irc, source)) + irc.msg(source, eval(args)) +utils.add_cmd(_eval, 'eval') From 0d207f7d08add1d877d3fa2e774d96a4f081cf55 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 10:39:17 -0700 Subject: [PATCH 58/70] relay: better description for 'showuser' --- plugins/relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index f052a33..71affc2 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -1239,7 +1239,7 @@ def linkacl(irc, source, args): def showuser(irc, source, args): """ - Shows relay data about user . This is intended to be used alongside the 'commands' plugin, which provides a 'showuser' command with more general information.""" + Shows relay data about user . This supplements the 'showuser' command in the 'commands' plugin, which provides more general information.""" try: target = args[0] except IndexError: From 0f26848b166d6181eabe1ec1d2f40d0d1d1f5aae Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 11:51:56 -0700 Subject: [PATCH 59/70] Add a useful "version" command --- plugins/commands.py | 8 ++++++++ pylink | 3 +-- world.py | 12 ++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/commands.py b/plugins/commands.py index f714f79..76df0a6 100644 --- a/plugins/commands.py +++ b/plugins/commands.py @@ -185,3 +185,11 @@ def shutdown(irc, source, args): # Disable auto-connect first by setting the time to negative. ircobj.serverdata['autoconnect'] = -1 ircobj.aborted.set() + +@utils.add_cmd +def version(irc, source, args): + """takes no arguments. + + Returns the version of the currently running PyLink instance.""" + irc.msg(source, "PyLink version \x02%s\x02, released under the Mozilla Public License version 2.0." % world.version) + irc.msg(source, "The source of this program is available at \x02%s\x02." % world.source) diff --git a/pylink b/pylink index 3b99c8f..213bf16 100755 --- a/pylink +++ b/pylink @@ -14,7 +14,7 @@ import classes import coreplugin if __name__ == '__main__': - log.info('PyLink starting...') + log.info('PyLink %s starting...', world.version) if conf.conf['login']['password'] == 'changeme': log.critical("You have not set the login details correctly! Exiting...") sys.exit(2) @@ -61,4 +61,3 @@ if __name__ == '__main__': world.networkobjects[network] = classes.Irc(network, proto) world.started.set() log.info("loaded plugins: %s", world.plugins) - diff --git a/world.py b/world.py index 7b34d3f..825a43b 100644 --- a/world.py +++ b/world.py @@ -2,6 +2,7 @@ from collections import defaultdict import threading +import subprocess # Global variable to indicate whether we're being ran directly, or imported # for a testcase. @@ -16,3 +17,14 @@ schedulers = {} plugins = [] whois_handlers = [] started = threading.Event() + +version = "" +source = "https://github.com/GLolol/PyLink" # CHANGE THIS IF YOU'RE FORKING!! + +# Only run this once. +if version == "": + # Get version from Git tags. + try: + version = 'v' + subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip() + except: + log.exception('Failed to get version from "git describe --tags".') From 3c8da4934abb0e21793d38e03b116173d257d28d Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 11:55:22 -0700 Subject: [PATCH 60/70] pylink: always chdir to the PyLink root directory --- pylink | 1 + 1 file changed, 1 insertion(+) diff --git a/pylink b/pylink index 213bf16..1d6d81c 100755 --- a/pylink +++ b/pylink @@ -5,6 +5,7 @@ import os import sys # This must be done before conf imports, so we get the real conf instead of testing one. +os.chdir(os.path.dirname(__file__)) import world world.testing = False From 82d129fed2951e450ae4efed555dd1dc93e158f8 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 19 Sep 2015 11:55:29 -0700 Subject: [PATCH 61/70] world: fix 0f26848 (we can't use log here) --- world.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/world.py b/world.py index 825a43b..92e09d1 100644 --- a/world.py +++ b/world.py @@ -26,5 +26,5 @@ if version == "": # Get version from Git tags. try: version = 'v' + subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip() - except: - log.exception('Failed to get version from "git describe --tags".') + except Exception as e: + print('ERROR: Failed to get version from "git describe --tags": %s: %s' % (type(e).__name__, e)) From f38958995c947834ee841162364c3f2ade97bd89 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 11:25:45 -0700 Subject: [PATCH 62/70] Store opertypes with spaces instead of underscores, and only change them back when sending OPERTYPE Another "Janus sucks" commit - it should be always using underscores when sending OPERTYPE, but it doesn't... --- coreplugin.py | 2 +- plugins/relay.py | 8 ++++---- protocols/inspircd.py | 11 +++++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/coreplugin.py b/coreplugin.py index 4e9ce27..81af294 100644 --- a/coreplugin.py +++ b/coreplugin.py @@ -79,7 +79,7 @@ def handle_whois(irc, source, command, args): # only if they have umode +o. if ('o', None) in user.modes: if hasattr(user, 'opertype'): - opertype = user.opertype.replace("_", " ") + opertype = user.opertype else: opertype = "IRC Operator" # Let's be gramatically correct. diff --git a/plugins/relay.py b/plugins/relay.py index 71affc2..8a87c0f 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -194,12 +194,11 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): # If an opertype exists for the user, add " (remote)" # for the relayed clone, so that it shows in whois. # Janus does this too. :) - # OPERTYPE uses underscores instead of spaces, FYI. log.debug('(%s) relay.getRemoteUser: setting OPERTYPE of client for %r to %s', irc.name, user, userobj.opertype) - opertype = userobj.opertype + '_(remote)' + opertype = userobj.opertype + ' (remote)' else: - opertype = 'IRC_Operator_(remote)' + opertype = 'IRC Operator (remote)' # Set hideoper on remote opers, to prevent inflating # /lusers and various /stats hideoper_mode = remoteirc.umodes.get('hideoper') @@ -587,7 +586,8 @@ world.whois_handlers.append(relayWhoisHandler) def handle_operup(irc, numeric, command, args): newtype = args['text'] + '_(remote)' for netname, user in relayusers[(irc.name, numeric)].items(): - log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s', irc.name, user, netname, newtype) + log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s', + irc.name, user, netname, newtype) remoteirc = world.networkobjects[netname] remoteirc.users[user].opertype = newtype utils.add_hook(handle_operup, 'PYLINK_CLIENT_OPERED') diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 7a8086c..88828a4 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -54,7 +54,7 @@ class InspIRCdProtocol(TS6BaseProtocol): modes=raw_modes, ip=ip, realname=realname, realhost=realhost)) if ('o', None) in modes or ('+o', None) in modes: - self._operUp(uid, opertype=opertype or 'IRC_Operator') + self._operUp(uid, opertype=opertype or 'IRC Operator') return u def joinClient(self, client, channel): @@ -143,17 +143,17 @@ class InspIRCdProtocol(TS6BaseProtocol): and the change will be reflected here.""" userobj = self.irc.users[target] try: - otype = opertype or userobj.opertype or 'IRC_Operator' + otype = opertype or userobj.opertype or 'IRC Operator' except AttributeError: log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!', self.irc.name, target, userobj.nick) # whatever, this is non-standard anyways. - otype = 'IRC_Operator' + otype = 'IRC Operator' assert otype, "Tried to send an empty OPERTYPE!" log.debug('(%s) Sending OPERTYPE from %s to oper them up.', self.irc.name, target) userobj.opertype = otype - self._send(target, 'OPERTYPE %s' % otype) + self._send(target, 'OPERTYPE %s' % otype.replace(" ", "_")) def _sendModes(self, numeric, target, modes, ts=None): """Internal function to send mode changes from a PyLink client/server.""" @@ -163,7 +163,6 @@ class InspIRCdProtocol(TS6BaseProtocol): if ('+o', None) in modes and not utils.isChannel(target): # https://github.com/inspself.ircd/inspself.ircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28 # Servers need a special command to set umode +o on people. - # Why isn't this documented anywhere, InspIRCd? self._operUp(target) utils.applyModes(self.irc, target, modes) joinedmodes = utils.joinModes(modes) @@ -547,7 +546,7 @@ class InspIRCdProtocol(TS6BaseProtocol): # command sent for it. # <- :70MAAAAAB OPERTYPE Network_Owner omode = [('+o', None)] - self.irc.users[numeric].opertype = opertype = args[0] + self.irc.users[numeric].opertype = opertype = args[0].replace("_", " ") utils.applyModes(self.irc, numeric, omode) # OPERTYPE is essentially umode +o and metadata in one command; # we'll call that too. From f4f89878735d1a5879d98f86dc1edbc1244dde1b Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 11:55:51 -0700 Subject: [PATCH 63/70] example conf: add full "netname" in server configuration --- config.yml.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config.yml.example b/config.yml.example index c638978..45754f9 100644 --- a/config.yml.example +++ b/config.yml.example @@ -44,6 +44,9 @@ servers: recvpass: "abcd" sendpass: "abcd" + # The full network name, used by plugins. + netname: "yournet" + # Hostname we will use to connect to the remote server hostname: "pylink.yournet" @@ -97,6 +100,7 @@ servers: sendpass: "abcd" hostname: "pylink.example.com" sid: "8PY" + netname: "some network" # Leave this as an empty list if you don't want to join any channels. channels: [] From 4c45533ecbbb115a77861949c73cb9f0e1dac8f6 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 12:11:28 -0700 Subject: [PATCH 64/70] coreplugin: send the RIGHT server desc in WHOIS replies --- coreplugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coreplugin.py b/coreplugin.py index 81af294..eaf2d3d 100644 --- a/coreplugin.py +++ b/coreplugin.py @@ -74,7 +74,7 @@ def handle_whois(irc, source, command, args): f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans))) # 312: sends the server the target is on, and its server description. f(server, 312, source, "%s %s :%s" % (nick, irc.servers[server].name, - irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'])) + irc.servers[server].desc)) # 313: sends a string denoting the target's operator privilege, # only if they have umode +o. if ('o', None) in user.modes: From 7b2d1d8129f96f146de744d4daed788418ac774c Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 12:11:41 -0700 Subject: [PATCH 65/70] relay: use the network's full name in spawning servers --- plugins/relay.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index 8a87c0f..458a0dc 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -148,7 +148,10 @@ def getRemoteSid(irc, remoteirc): sid = relayservers[irc.name][remoteirc.name] except KeyError: try: - sid = irc.proto.spawnServer('%s.relay' % remoteirc.name) + sid = irc.proto.spawnServer('%s.relay' % remoteirc.name, + desc="PyLink Relay network - %s" % + (remoteirc.serverdata.get('netname')\ + or remoteirc.name)) except ValueError: # Network not initialized yet. log.exception('(%s) Failed to spawn server for %r:', irc.name, remoteirc.name) From f110ac15b05c84eec78cc6db120a2ff00bd818f6 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 13:21:04 -0700 Subject: [PATCH 66/70] coreplugin: Protect against forced deopers Closes #67. --- coreplugin.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/coreplugin.py b/coreplugin.py index eaf2d3d..fbed342 100644 --- a/coreplugin.py +++ b/coreplugin.py @@ -4,22 +4,22 @@ import utils from log import log import world -# Handle KILLs sent to the PyLink client and respawn def handle_kill(irc, source, command, args): + """Handle KILLs to the main PyLink client, respawning it as needed.""" if args['target'] == irc.pseudoclient.uid: irc.spawnMain() utils.add_hook(handle_kill, 'KILL') -# Handle KICKs to the PyLink client, rejoining the channels def handle_kick(irc, source, command, args): + """Handle KICKs to the main PyLink client, rejoining channels as needed.""" kicked = args['target'] channel = args['channel'] if kicked == irc.pseudoclient.uid: irc.proto.joinClient(irc.pseudoclient.uid, channel) utils.add_hook(handle_kick, 'KICK') -# Handle commands sent to the PyLink client (PRIVMSG) def handle_commands(irc, source, command, args): + """Handle commands sent to the PyLink client (PRIVMSG).""" if args['target'] == irc.pseudoclient.uid: text = args['text'].strip() cmd_args = text.split(' ') @@ -39,8 +39,8 @@ def handle_commands(irc, source, command, args): irc.msg(source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e))) utils.add_hook(handle_commands, 'PRIVMSG') -# Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd). def handle_whois(irc, source, command, args): + """Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd).""" target = args['target'] user = irc.users.get(target) if user is None: @@ -109,3 +109,14 @@ def handle_whois(irc, source, command, args): # 318: End of WHOIS. f(server, 318, source, "%s :End of /WHOIS list" % nick) utils.add_hook(handle_whois, 'WHOIS') + +def handle_mode(irc, source, command, args): + """Protect against forced deoper attempts.""" + target = args['target'] + modes = args['modes'] + # If the sender is not a PyLink client, and the target IS a protected + # client, revert any forced deoper attempts. + if utils.isInternalClient(irc, target) and not utils.isInternalClient(irc, source): + if ('-o', None) in modes and not utils.isManipulatableClient(irc, target): + irc.proto.modeServer(irc.sid, target, {('+o', None)}) +utils.add_hook(handle_mode, 'MODE') From 16d8a2212a5ec2a3fbe8c1fec4ec9c624883029d Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 16:55:04 -0700 Subject: [PATCH 67/70] coreplugin: protect the main PyLink client too, even though it's technically manipulatable --- coreplugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coreplugin.py b/coreplugin.py index fbed342..2420ef2 100644 --- a/coreplugin.py +++ b/coreplugin.py @@ -117,6 +117,6 @@ def handle_mode(irc, source, command, args): # If the sender is not a PyLink client, and the target IS a protected # client, revert any forced deoper attempts. if utils.isInternalClient(irc, target) and not utils.isInternalClient(irc, source): - if ('-o', None) in modes and not utils.isManipulatableClient(irc, target): + if ('-o', None) in modes and (target == irc.pseudoclient.uid or not utils.isManipulatableClient(irc, target)): irc.proto.modeServer(irc.sid, target, {('+o', None)}) utils.add_hook(handle_mode, 'MODE') From f2b6d1a3ad748ba25a270f114bc361801851b14e Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 16:56:24 -0700 Subject: [PATCH 68/70] relay: use two locks (one for clients, one for servers) to prevent lock ups on start --- plugins/relay.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/relay.py b/plugins/relay.py index 458a0dc..8a3dd38 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -23,6 +23,7 @@ dbname += '.db' relayusers = defaultdict(dict) relayservers = defaultdict(dict) spawnlocks = defaultdict(threading.RLock) +spawnlocks_servers = defaultdict(threading.RLock) savecache = ExpiringDict(max_len=5, max_age_seconds=10) killcache = ExpiringDict(max_len=5, max_age_seconds=10) @@ -143,7 +144,7 @@ def getPrefixModes(irc, remoteirc, channel, user, mlist=None): def getRemoteSid(irc, remoteirc): """Gets the remote server SID representing remoteirc on irc, spawning it if it doesn't exist.""" - with spawnlocks[irc.name]: + with spawnlocks_servers[irc.name]: try: sid = relayservers[irc.name][remoteirc.name] except KeyError: From 7e12ec9e5e42bc18acb101ba337f26c09dda9159 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 18:13:39 -0700 Subject: [PATCH 69/70] relay: don't prepend sender prefixes more than once (for servers and clients not on any relay networks) --- plugins/relay.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/relay.py b/plugins/relay.py index 8a3dd38..8000343 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -775,8 +775,7 @@ def handle_kick(irc, source, command, args): # Propogate the kick! if real_kicker: log.debug('(%s) Relay kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan,real_kicker, kicker, irc.name) - remoteirc.proto.kickClient(real_kicker, - remotechan, real_target, text) + remoteirc.proto.kickClient(real_kicker, remotechan, real_target, args['text']) else: # Kick originated from a server, or the kicker isn't in any # common channels with the target relay network. @@ -787,9 +786,9 @@ def handle_kick(irc, source, command, args): kname = irc.servers[kicker].name else: kname = irc.users.get(kicker).nick - text = "(%s/%s) %s" % (kname, irc.name, text) + text = "(%s/%s) %s" % (kname, irc.name, args['text']) except AttributeError: - text = "(@%s) %s" % (irc.name, text) + text = "(@%s) %s" % (irc.name, args['text']) remoteirc.proto.kickServer(rsid, remotechan, real_target, text) # If the target isn't on any channels, quit them. From e92f35018fe32181d6dd9be489e44a0309825001 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 20 Sep 2015 18:32:43 -0700 Subject: [PATCH 70/70] TS6SIDGenerator: take an IRC object, and skip any SIDs that are currently in use --- protocols/inspircd.py | 2 +- protocols/ts6.py | 2 +- utils.py | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 88828a4..8f94c1a 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -24,7 +24,7 @@ class InspIRCdProtocol(TS6BaseProtocol): self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE', 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'} - self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) + self.sidgen = utils.TS6SIDGenerator(self.irc) self.uidgen = {} def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), diff --git a/protocols/ts6.py b/protocols/ts6.py index dc010b4..352043a 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -17,7 +17,7 @@ class TS6Protocol(TS6BaseProtocol): super(TS6Protocol, self).__init__(irc) self.casemapping = 'rfc1459' self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} - self.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) + self.sidgen = utils.TS6SIDGenerator(self.irc) self.uidgen = {} def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), diff --git a/utils.py b/utils.py index 06b1904..9b5cdc8 100644 --- a/utils.py +++ b/utils.py @@ -56,8 +56,12 @@ class TS6SIDGenerator(): "6##" would give: 600, 601, 602, ... 60Y, 60Z, 610, 611, ... 6ZZ (1296 total results) """ - def __init__(self, query): - self.query = list(query) + def __init__(self, irc): + self.irc = irc + try: + self.query = query = list(irc.serverdata["sidrange"]) + except KeyError: + raise RuntimeError('(%s) "sidrange" is missing from your server configuration block!' % irc.name) self.iters = self.query.copy() self.output = self.query.copy() self.allowedchars = {} @@ -93,8 +97,9 @@ class TS6SIDGenerator(): self.increment(pos-1) def next_sid(self): + while ''.join(self.output) in self.irc.servers: + self.increment() sid = ''.join(self.output) - self.increment() return sid def add_cmd(func, name=None):