diff --git a/plugins/Internet/plugin.py b/plugins/Internet/plugin.py index 490c9165c..de94afcd9 100644 --- a/plugins/Internet/plugin.py +++ b/plugins/Internet/plugin.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2003-2005, Jeremiah Fincher -# Copyright (c) 2010, James Vega +# Copyright (c) 2010-2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -53,7 +53,7 @@ class Internet(callbacks.Plugin): irc.reply(hostname) else: try: - ip = socket.gethostbyname(host) + ip = socket.getaddrinfo(host, None)[0][4][0] if ip == '64.94.110.11': # Verisign sucks! irc.reply('Host not found.') else: @@ -149,12 +149,22 @@ class Internet(callbacks.Plugin): Returns the hexadecimal IP for that IP. """ - quads = ip.split('.') ret = "" - for quad in quads: - i = int(quad) - ret += '%02x' % i - irc.reply(ret.upper()) + if utils.net.isIPV4(ip): + quads = ip.split('.') + for quad in quads: + i = int(quad) + ret += '%02X' % i + else: + octets = ip.split(':') + for octet in octets: + if octet: + i = int(octet, 16) + ret += '%04X' % i + else: + missing = (8 - len(octets)) * 4 + ret += '0' * missing + irc.reply(ret) hexip = wrap(hexip, ['ip']) diff --git a/plugins/Misc/plugin.py b/plugins/Misc/plugin.py index a1412f32e..a3b560d4a 100644 --- a/plugins/Misc/plugin.py +++ b/plugins/Misc/plugin.py @@ -58,31 +58,34 @@ class Misc(callbacks.Plugin): assert not msg.repliedTo, 'repliedTo msg in Misc.invalidCommand.' assert self is irc.callbacks[-1], 'Misc isn\'t last callback.' self.log.debug('Misc.invalidCommand called (tokens %s)', tokens) - # First, we check for invalidCommand floods. This is rightfully done - # here since this will be the last invalidCommand called, and thus it - # will only be called if this is *truly* an invalid command. - maximum = conf.supybot.abuse.flood.command.invalid.maximum() - self.invalidCommands.enqueue(msg) - if self.invalidCommands.len(msg) > maximum and \ - conf.supybot.abuse.flood.command.invalid() and \ - not ircdb.checkCapability(msg.prefix, 'owner'): - punishment = conf.supybot.abuse.flood.command.invalid.punishment() - banmask = '*!%s@%s' % (msg.user, msg.host) - self.log.info('Ignoring %s for %s seconds due to an apparent ' - 'invalid command flood.', banmask, punishment) - if tokens and tokens[0] == 'Error:': - self.log.warning('Apparent error loop with another Supybot ' - 'observed. Consider ignoring this bot ' - 'permanently.') - ircdb.ignores.add(banmask, time.time() + punishment) - if conf.supybot.abuse.flood.command.invalid.notify(): - irc.reply('You\'ve given me %s invalid commands within the last ' - 'minute; I\'m now ignoring you for %s.' % - (maximum, - utils.timeElapsed(punishment, seconds=False))) - return - # Now, for normal handling. channel = msg.args[0] + # Only bother with the invaildCommand flood handling if it's actually + # enabled + if conf.supybot.abuse.flood.command.invalid(): + # First, we check for invalidCommand floods. This is rightfully done + # here since this will be the last invalidCommand called, and thus it + # will only be called if this is *truly* an invalid command. + maximum = conf.supybot.abuse.flood.command.invalid.maximum() + banmasker = conf.supybot.protocols.irc.banmask.makeBanmask + self.invalidCommands.enqueue(msg) + if self.invalidCommands.len(msg) > maximum and \ + not ircdb.checkCapability(msg.prefix, 'owner'): + penalty = conf.supybot.abuse.flood.command.invalid.punishment() + banmask = banmasker(msg.prefix) + self.log.info('Ignoring %s for %s seconds due to an apparent ' + 'invalid command flood.', banmask, penalty) + if tokens and tokens[0] == 'Error:': + self.log.warning('Apparent error loop with another Supybot ' + 'observed. Consider ignoring this bot ' + 'permanently.') + ircdb.ignores.add(banmask, time.time() + penalty) + if conf.supybot.abuse.flood.command.invalid.notify(): + irc.reply('You\'ve given me %s invalid commands within ' + 'the last minute; I\'m now ignoring you for %s.' % + (maximum, + utils.timeElapsed(penalty, seconds=False))) + return + # Now, for normal handling. if conf.get(conf.supybot.reply.whenNotCommand, channel): if len(tokens) >= 2: cb = irc.getCallback(tokens[0]) diff --git a/plugins/RSS/plugin.py b/plugins/RSS/plugin.py index c5058d4ca..d23dabe72 100644 --- a/plugins/RSS/plugin.py +++ b/plugins/RSS/plugin.py @@ -262,8 +262,14 @@ class RSS(callbacks.Plugin): def _getConverter(self, feed): toText = utils.web.htmlToText if 'encoding' in feed: - return lambda s: toText(s).strip().encode(feed['encoding'], - 'replace') + def conv(s): + # encode() first so there implicit encoding doesn't happen in + # other functions when unicode and bytestring objects are used + # together + s = s.encode(feed['encoding'], 'replace') + s = toText(s).strip() + return s + return conv else: return lambda s: toText(s).strip() diff --git a/plugins/Seen/plugin.py b/plugins/Seen/plugin.py index 4051e0994..fba29f59d 100644 --- a/plugins/Seen/plugin.py +++ b/plugins/Seen/plugin.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher -# Copyright (c) 2010, James Vega +# Copyright (c) 2010-2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -66,7 +66,7 @@ class SeenDB(plugins.ChannelUserDB): def seenWildcard(self, channel, nick): nicks = ircutils.IrcSet() - nickRe = re.compile('.*'.join(nick.split('*')), re.I) + nickRe = re.compile('^%s$' % '.*'.join(nick.split('*')), re.I) for (searchChan, searchNick) in self.keys(): #print 'chan: %s ... nick: %s' % (searchChan, searchNick) if isinstance(searchNick, int): @@ -75,11 +75,8 @@ class SeenDB(plugins.ChannelUserDB): # are keyed by nick-string continue if ircutils.strEqual(searchChan, channel): - try: - s = nickRe.match(searchNick).group() - except AttributeError: - continue - nicks.add(s) + if nickRe.search(searchNick) is not None: + nicks.add(searchNick) L = [[nick, self.seen(channel, nick)] for nick in nicks] def negativeTime(x): return -x[1][0] diff --git a/plugins/Services/plugin.py b/plugins/Services/plugin.py index e5882573b..1d17a8085 100644 --- a/plugins/Services/plugin.py +++ b/plugins/Services/plugin.py @@ -244,7 +244,7 @@ class Services(callbacks.Plugin): # You have been unbanned from (oftc) irc.sendMsg(networkGroup.channels.join(channel)) elif 'isn\'t registered' in s: - self.log.warning('Received "%s isn\'t registered" from ChanServ %', + self.log.warning('Received "%s isn\'t registered" from ChanServ %s', channel, on) elif 'this channel has been registered' in s: self.log.debug('Got "Registered channel" from ChanServ %s.', on) diff --git a/plugins/String/plugin.py b/plugins/String/plugin.py index d6b4c0dbe..ce4c2ef06 100644 --- a/plugins/String/plugin.py +++ b/plugins/String/plugin.py @@ -79,7 +79,12 @@ class String(callbacks.Plugin): . """ try: - irc.reply(text.decode(encoding).encode('utf-8')) + s = text.decode(encoding) + # Not all encodings decode to a unicode object. Only encode those + # that do. + if isinstance(s, unicode): + s = s.encode('utf-8') + irc.reply(s) except LookupError: irc.errorInvalid('encoding', encoding) except binascii.Error: diff --git a/scripts/supybot-test b/scripts/supybot-test index 2bec1826e..c0a500713 100644 --- a/scripts/supybot-test +++ b/scripts/supybot-test @@ -2,6 +2,7 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher +# Copyright (c) 2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -122,9 +123,10 @@ if __name__ == '__main__': parser.add_option('-c', '--clean', action='store_true', default=False, dest='clean', help='Cleans the various data/conf/logs' 'directories before running tests.') - parser.add_option('-t', '--timeout', action='store', type='int', + parser.add_option('-t', '--timeout', action='store', type='float', dest='timeout', - help='Sets the timeout for tests to return responses.') + help='Sets the timeout, in seconds, for tests to return ' + 'responses.') parser.add_option('-v', '--verbose', action='store_true', default=False, help='Sets the verbose flag, logging extra information ' 'about each test that runs.') diff --git a/src/conf.py b/src/conf.py index 54141d5a8..963cedfac 100644 --- a/src/conf.py +++ b/src/conf.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2008-2009, James Vega +# Copyright (c) 2008-2009,2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -1034,7 +1034,7 @@ registerGlobalValue(supybot, 'defaultIgnore', class IP(registry.String): """Value must be a valid IP.""" def setValue(self, v): - if v and not (utils.net.isIP(v) or utils.net.isIPV6(v)): + if v and not utils.net.isIP(v): self.error() else: registry.String.setValue(self, v) diff --git a/src/drivers/Socket.py b/src/drivers/Socket.py index 6db605a59..ff9e59146 100644 --- a/src/drivers/Socket.py +++ b/src/drivers/Socket.py @@ -46,6 +46,13 @@ import supybot.drivers as drivers import supybot.schedule as schedule from supybot.utils.iter import imap +try: + import ssl +except ImportError: + drivers.log.debug('ssl module is not available, ' + 'cannot connect to SSL servers.') + ssl = None + class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): def __init__(self, irc): self.irc = irc @@ -61,12 +68,7 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): self.writeCheckTime = None self.nextReconnectTime = None self.resetDelay() - # Only connect to non-SSL servers - if self.networkGroup.get('ssl').value: - drivers.log.error('The Socket driver can not connect to SSL ' - 'servers. Try the Twisted driver instead.') - else: - self.connect() + self.connect() def getDelay(self): ret = self.currentDelay @@ -139,6 +141,12 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): self.irc.feedMsg(msg) except socket.timeout: pass + except ssl.SSLError, e: + if e.args[0] == 'The read operation timed out': + pass + else: + self._handleSocketError(e) + return except socket.error, e: self._handleSocketError(e) return @@ -163,6 +171,14 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): drivers.log.connect(self.currentServer) try: self.conn = utils.net.getSocket(server[0]) + if self.networkGroup.get('ssl').value: + if ssl: + self.plainconn = self.conn + self.conn = ssl.wrap_socket(self.conn) + else: + drivers.log.error('ssl module not available, ' + 'cannot connect to SSL servers.') + return vhost = conf.supybot.protocols.irc.vhost() self.conn.bind((vhost, 0)) except socket.error, e: diff --git a/src/irclib.py b/src/irclib.py index ffded1652..4f8f73ff8 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -405,10 +405,10 @@ class IrcState(IrcCommandDispatcher): """Handles parsing the 004 reply Supported user and channel modes are cached""" - # msg.args = [nick, server, ircd-version, umodes, modes, + # msg.args = [server, ircd-version, umodes, modes, # modes that require arguments? (non-standard)] - self.supported['umodes'] = msg.args[3] - self.supported['chanmodes'] = msg.args[4] + self.supported['umodes'] = msg.args[2] + self.supported['chanmodes'] = msg.args[3] _005converters = utils.InsensitivePreservingDict({ 'modes': int, @@ -929,14 +929,14 @@ class Irc(IrcCommandDispatcher): # Let's reset nicks in case we had to use a weird one. self.alternateNicks = conf.supybot.nick.alternates()[:] umodes = conf.supybot.protocols.irc.umodes() - supported = self.supported.get('umodes') + supported = self.state.supported.get('umodes') if umodes: addSub = '+' if umodes[0] in '+-': (addSub, umodes) = (umodes[0], umodes[1:]) if supported: - umodes = filter(lamda m: m in supported, umodes) - umodes = ''.join(addSub, umodes) + umodes = ''.join([m for m in umodes if m in supported]) + umodes = ''.join([addSub, umodes]) log.info('Sending user modes to %s: %s', self.network, umodes) self.sendMsg(ircmsgs.mode(self.nick, umodes)) do377 = do422 = do376 diff --git a/src/ircutils.py b/src/ircutils.py index c7b5408df..e2eb137eb 100644 --- a/src/ircutils.py +++ b/src/ircutils.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2009, James Vega +# Copyright (c) 2009,2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -187,7 +187,7 @@ def banmask(hostmask): """ assert isUserHostmask(hostmask) host = hostFromHostmask(hostmask) - if utils.net.isIP(host): + if utils.net.isIPV4(host): L = host.split('.') L[-1] = '*' return '*!*@' + '.'.join(L) @@ -196,7 +196,7 @@ def banmask(hostmask): L[-1] = '*' return '*!*@' + ':'.join(L) else: - if '.' in host: + if len(host.split('.')) > 2: # If it is a subdomain return '*!*@*%s' % host[host.find('.'):] else: return '*!*@' + host @@ -459,8 +459,8 @@ def replyTo(msg): def dccIP(ip): """Converts an IP string to the DCC integer form.""" - assert utils.net.isIP(ip), \ - 'argument must be a string ip in xxx.xxx.xxx.xxx format.' + assert utils.net.isIPV4(ip), \ + 'argument must be a string ip in xxx.yyy.zzz.www format.' i = 0 x = 256**3 for quad in ip.split('.'): diff --git a/src/test.py b/src/test.py index 50dad08a7..a9f716264 100644 --- a/src/test.py +++ b/src/test.py @@ -1,5 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher +# Copyright (c) 2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -54,6 +55,8 @@ network = True # This is the global list of suites that are to be run. suites = [] +timeout = 10 + originalCallbacksGetHelp = callbacks.getHelp lastGetHelp = 'x' * 1000 def cachingGetHelp(method, name=None, doc=None): @@ -110,12 +113,12 @@ class PluginTestCase(SupyTestCase): """Subclass this to write a test case for a plugin. See plugins/Plugin/test.py for an example. """ - timeout = 10 plugins = None cleanConfDir = True cleanDataDir = True config = {} def __init__(self, methodName='runTest'): + self.timeout = timeout originalRunTest = getattr(self, methodName) def runTest(self): run = True diff --git a/src/utils/net.py b/src/utils/net.py index fa78fdcc3..ffe8c2005 100644 --- a/src/utils/net.py +++ b/src/utils/net.py @@ -1,5 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher +# Copyright (c) 2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -44,7 +45,7 @@ def getSocket(host): """ addrinfo = socket.getaddrinfo(host, None) host = addrinfo[0][4][0] - if isIP(host): + if isIPV4(host): return socket.socket(socket.AF_INET, socket.SOCK_STREAM) elif isIPV6(host): return socket.socket(socket.AF_INET6, socket.SOCK_STREAM) @@ -52,16 +53,27 @@ def getSocket(host): raise socket.error, 'Something wonky happened.' def isIP(s): - """Returns whether or not a given string is an IPV4 address. + """Returns whether or not a given string is an IP address. >>> isIP('255.255.255.255') 1 - >>> isIP('abc.abc.abc.abc') + >>> isIP('::1') + 0 + """ + return isIPV4(s) or isIPV6(s) + +def isIPV4(s): + """Returns whether or not a given string is an IPV4 address. + + >>> isIPV4('255.255.255.255') + 1 + + >>> isIPV4('abc.abc.abc.abc') 0 """ try: - return bool(socket.inet_aton(s)) + return bool(socket.inet_pton(socket.AF_INET, s)) except socket.error: return False diff --git a/test/test_irclib.py b/test/test_irclib.py index 52dc7e258..bda696be6 100644 --- a/test/test_irclib.py +++ b/test/test_irclib.py @@ -290,6 +290,13 @@ class IrcStateTestCase(SupyTestCase): state.addMsg(self.irc, ircmsgs.IrcMsg(':irc.inet.tele.dk 005 adkwbot WALLCHOPS KNOCK EXCEPTS INVEX MODES=4 MAXCHANNELS=20 MAXBANS=beI:100 MAXTARGETS=4 NICKLEN=9 TOPICLEN=120 KICKLEN=90 :are supported by this server')) self.assertEqual(state.supported['maxbans'], 100) + def testSupportedUmodes(self): + state = irclib.IrcState() + state.addMsg(self.irc, ircmsgs.IrcMsg(':charm.oftc.net 004 charm.oftc.net hybrid-7.2.2+oftc1.6.8 CDGPRSabcdfgiklnorsuwxyz biklmnopstveI bkloveI')) + self.assertEqual(state.supported['umodes'], 'CDGPRSabcdfgiklnorsuwxyz') + self.assertEqual(state.supported['chanmodes'], + 'biklmnopstveI') + def testEmptyTopic(self): state = irclib.IrcState() state.addMsg(self.irc, ircmsgs.topic('#foo')) diff --git a/test/test_ircutils.py b/test/test_ircutils.py index 6dc11e21d..b0fca5261 100644 --- a/test/test_ircutils.py +++ b/test/test_ircutils.py @@ -233,6 +233,10 @@ class FunctionsTestCase(SupyTestCase): msg.prefix), '%r didn\'t match %r' % (msg.prefix, banmask)) self.assertEqual(ircutils.banmask('foobar!user@host'), '*!*@host') + self.assertEqual(ircutils.banmask('foobar!user@host.tld'), + '*!*@host.tld') + self.assertEqual(ircutils.banmask('foobar!user@sub.host.tld'), + '*!*@*.host.tld') self.assertEqual(ircutils.banmask('foo!bar@2001::'), '*!*@2001::*') def testSeparateModes(self): diff --git a/test/test_utils.py b/test/test_utils.py index c4efb4979..da94678e6 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2009, James Vega +# Copyright (c) 2009,2011, James Vega # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -501,11 +501,9 @@ class NetTest(SupyTestCase): isIP = utils.net.isIP self.failIf(isIP('a.b.c')) self.failIf(isIP('256.0.0.0')) - self.failUnless(isIP('127.1')) self.failUnless(isIP('0.0.0.0')) self.failUnless(isIP('100.100.100.100')) - # This test is too flaky to bother with. - # self.failUnless(utils.isIP('255.255.255.255')) + self.failUnless(isIP('255.255.255.255')) def testIsIPV6(self): f = utils.net.isIPV6