From c3695c94194510cc53ec6da06dfe60fc4416430a Mon Sep 17 00:00:00 2001 From: James McCoy Date: Thu, 26 Mar 2015 00:11:36 -0400 Subject: [PATCH] ircutils: Add formatWhois function Parsing through the various WHOIS replies to build a formatted string isn't a trivial task, especially since there is some privacy related information. Consolidate this handling into a single function so there's one place to fix bugs. Also fix an issue with people putting (unterminated) formatted text into the "realname" field of their IRC client (c.f., ProgVal/Limnoria#1083). Signed-off-by: James McCoy --- plugins/Network/plugin.py | 86 ++----------------------------- plugins/Relay/plugin.py | 63 ++-------------------- src/ircutils.py | 106 +++++++++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 142 deletions(-) diff --git a/plugins/Network/plugin.py b/plugins/Network/plugin.py index 8ab5cdf48..cc9fbff50 100644 --- a/plugins/Network/plugin.py +++ b/plugins/Network/plugin.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher -# Copyright (c) 2010, James McCoy +# Copyright (c) 2010,2015 James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -158,87 +158,9 @@ class Network(callbacks.Plugin): if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] - hostmask = '@'.join(d['311'].args[2:4]) - user = d['311'].args[-1] - if '319' in d: - channels = d['319'].args[-1].split() - ops = [] - voices = [] - normal = [] - halfops = [] - for channel in channels: - origchan = channel - channel = channel.lstrip('@%+~!') - # UnrealIRCd uses & for user modes and disallows it as a - # channel-prefix, flying in the face of the RFC. Have to - # handle this specially when processing WHOIS response. - testchan = channel.lstrip('&') - if testchan != channel and irc.isChannel(testchan): - channel = testchan - diff = len(channel) - len(origchan) - modes = origchan[:diff] - chan = irc.state.channels.get(channel) - # The user is in a channel the bot is in, so the ircd may have - # responded with otherwise private data. - if chan: - # Skip channels the callee isn't in. This helps prevents - # us leaking information when the channel is +s or the - # target is +i - if replyMsg.nick not in chan.users: - continue - # Skip +s channels the target is in only if the reply isn't - # being sent to that channel - if 's' in chan.modes and \ - not ircutils.strEqual(replyMsg.args[0], channel): - continue - if not modes: - normal.append(channel) - elif utils.iter.any(lambda c: c in modes,('@', '&', '~', '!')): - ops.append(channel[1:]) - elif utils.iter.any(lambda c: c in modes, ('%',)): - halfops.append(channel[1:]) - elif utils.iter.any(lambda c: c in modes, ('+',)): - voices.append(channel[1:]) - L = [] - if ops: - L.append(format('is an op on %L', ops)) - if halfops: - L.append(format('is a halfop on %L', halfops)) - if voices: - L.append(format('is voiced on %L', voices)) - if normal: - if L: - L.append(format('is also on %L', normal)) - else: - L.append(format('is on %L', normal)) - else: - L = ['isn\'t on any non-secret channels'] - channels = format('%L', L) - if '317' in d: - idle = utils.timeElapsed(d['317'].args[2]) - signon = time.strftime(conf.supybot.reply.format.time(), - time.localtime(float(d['317'].args[3]))) - else: - idle = '' - signon = '' - if '312' in d: - server = d['312'].args[2] - else: - server = '' - if '301' in d: - away = ' %s is away: %s.' % (nick, d['301'].args[2]) - else: - away = '' - if '320' in d: - if d['320'].args[2]: - identify = ' identified' - else: - identify = '' - else: - identify = '' - s = '%s (%s) has been%s on server %s since %s (idle for %s) and ' \ - '%s.%s' % (user, hostmask, identify, server, signon, idle, - channels, away) + d['318'] = msg + s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, + channel=replyMsg.args[0]) replyIrc.reply(s) del self._whois[(irc, loweredNick)] diff --git a/plugins/Relay/plugin.py b/plugins/Relay/plugin.py index 203f8ecf3..c1bf26403 100644 --- a/plugins/Relay/plugin.py +++ b/plugins/Relay/plugin.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher -# Copyright (c) 2010, James McCoy +# Copyright (c) 2010,2015 James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -196,64 +196,9 @@ class Relay(callbacks.Plugin): if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] - hostmask = '@'.join(d['311'].args[2:4]) - user = d['311'].args[-1] - if '319' in d: - channels = d['319'].args[-1].split() - ops = [] - voices = [] - normal = [] - halfops = [] - for channel in channels: - if channel.startswith('@'): - ops.append(channel[1:]) - elif channel.startswith('%'): - halfops.append(channel[1:]) - elif channel.startswith('+'): - voices.append(channel[1:]) - else: - normal.append(channel) - L = [] - if ops: - L.append(format('is an op on %L', ops)) - if halfops: - L.append(format('is a halfop on %L', halfups)) - if voices: - L.append(format('is voiced on %L', voices)) - if normal: - if L: - L.append(format('is also on %L', normal)) - else: - L.append(format('is on %L', normal)) - else: - L = ['isn\'t on any non-secret channels'] - channels = format('%L', L) - if '317' in d: - idle = utils.timeElapsed(d['317'].args[2]) - signon = time.strftime(conf.supybot.reply.format.time(), - time.localtime(float(d['317'].args[3]))) - else: - idle = '' - signon = '' - if '312' in d: - server = d['312'].args[2] - else: - server = '' - if '301' in d: - away = format(' %s is away: %s.', nick, d['301'].args[2]) - else: - away = '' - if '320' in d: - if d['320'].args[2]: - identify = ' identified' - else: - identify = '' - else: - identify = '' - s = format('%s (%s) has been%s on server %s since %s (idle for %s) ' - 'and %s.%s', - user, hostmask, identify, server, signon, idle, - channels, away) + d['318'] = msg + s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, + channel=replyMsg.args[0]) replyIrc.reply(s) del self._whois[(irc, loweredNick)] diff --git a/src/ircutils.py b/src/ircutils.py index c107a16d7..b234ed0f7 100644 --- a/src/ircutils.py +++ b/src/ircutils.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2009,2011, James McCoy +# Copyright (c) 2009,2011,2015 James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -338,6 +338,110 @@ def stripFormatting(s): s = stripUnderline(s) return s.replace('\x0f', '').replace('\x0F', '') +_containsFormattingRe = re.compile(r'[\x02\x03\x16\x1f]') +def formatWhois(irc, replies, caller='', channel=''): + """Returns a string describing the target of a WHOIS command. + + Arguments are: + * irc: the irclib.Irc object on which the replies was received + + * replies: a dict mapping the reply codes ('311', '312', etc.) to their + corresponding ircmsg.IrcMsg + + * caller: an optional nick specifying who requested the whois information + + * channel: an optional channel specifying where the reply will be sent + + If provided, caller and channel will be used to avoid leaking information + that the caller/channel shouldn't be privy to. + """ + hostmask = '@'.join(replies['311'].args[2:4]) + nick = replies['318'].args[1] + user = replies['311'].args[-1] + if _containsFormattingRe.search(user) and user[-1] != '\x0f': + # For good measure, disable any formatting + user = '%s\x0f' % user + if '319' in replies: + channels = replies['319'].args[-1].split() + ops = [] + voices = [] + normal = [] + halfops = [] + for chan in channels: + origchan = chan + chan = chan.lstrip('@%+~!') + # UnrealIRCd uses & for user modes and disallows it as a + # channel-prefix, flying in the face of the RFC. Have to + # handle this specially when processing WHOIS response. + testchan = chan.lstrip('&') + if testchan != chan and irc.isChannel(testchan): + chan = testchan + diff = len(chan) - len(origchan) + modes = origchan[:diff] + chanState = irc.state.channels.get(chan) + # The user is in a channel the bot is in, so the ircd may have + # responded with otherwise private data. + if chanState: + # Skip channels the callee isn't in. This helps prevents + # us leaking information when the channel is +s or the + # target is +i + if caller not in chanState.users: + continue + # Skip +s channels the target is in only if the reply isn't + # being sent to that channel + if 's' in chanState.modes and \ + not ircutils.strEqual(channel or '', chan): + continue + if not modes: + normal.append(chan) + elif utils.iter.any(lambda c: c in modes,('@', '&', '~', '!')): + ops.append(chan[1:]) + elif utils.iter.any(lambda c: c in modes, ('%',)): + halfops.append(chan[1:]) + elif utils.iter.any(lambda c: c in modes, ('+',)): + voices.append(chan[1:]) + L = [] + if ops: + L.append(format('is an op on %L', ops)) + if halfops: + L.append(format('is a halfop on %L', halfops)) + if voices: + L.append(format('is voiced on %L', voices)) + if normal: + if L: + L.append(format('is also on %L', normal)) + else: + L.append(format('is on %L', normal)) + else: + L = ['isn\'t on any non-secret channels'] + channels = format('%L', L) + if '317' in replies: + idle = utils.timeElapsed(replies['317'].args[2]) + signon = utils.str.timestamp(float(replies['317'].args[3])) + else: + idle = '' + signon = '' + if '312' in replies: + server = replies['312'].args[2] + else: + server = '' + if '301' in replies: + away = ' %s is away: %s.' % (nick, replies['301'].args[2]) + else: + away = '' + if '320' in replies: + if replies['320'].args[2]: + identify = ' identified' + else: + identify = '' + else: + identify = '' + s = utils.str.format('%s (%s) has been%s on server %s since %s ' + '(idle for %s) and %s.%s', + user, hostmask, identify, server, signon, idle, + channels, away) + return s + class FormatContext(object): def __init__(self): self.reset()