Skip to content

Commit

Permalink
SECURITY: Strip "\x01" out of commands being sent to the server. Only…
Browse files Browse the repository at this point in the history
… things excluded are the .write for the owner and .me for admins

Hard coded in a few IRC codes for jenni not to report to #jenni-log
Made slight improvement on speed when raw logging is turned on.
Added full paths for raw logging.
Moved around safe() in irc.py to make it useful in several locations.
Optimized throttling for jenni and reporting to #jenni-log (the throttling for each runs in two different stacks)
Updated Copyright and header documentation for 'jenni' and bot.py
Added new option in the default.py configuration file to have the ability to turn on/off raw logging in log/raw.log
Added ability for 'owner' only to send RAW irc commands to the server. (.write)
Fixed indentation in admin.py
Updated path for unobot.py, fixes #53
  • Loading branch information
myano committed Oct 7, 2012
1 parent 20eaf04 commit 4dae575
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 86 deletions.
11 changes: 8 additions & 3 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
"""
bot.py - Jenni IRC Bot
Copyright 2008, Sean B. Palmer, inamidst.com
Modified by: Michael Yanovich
Licensed under the Eiffel Forum License 2.
http://inamidst.com/phenny/
More info:
* Jenni: https://github.com/myano/jenni/
* Phenny: http://inamidst.com/phenny/
"""

import time, sys, os, re, threading, imp
Expand All @@ -24,7 +27,9 @@ class Jenni(irc.Bot):
def __init__(self, config):
if hasattr(config, "logchan_pm"): lc_pm = config.logchan_pm
else: lc_pm = None
args = (config.nick, config.name, config.channels, config.password, lc_pm)
if hasattr(config, "logging"): logging = config.logging
else: logging = False
args = (config.nick, config.name, config.channels, config.password, lc_pm, logging)
irc.Bot.__init__(self, *args)
self.config = config
self.doc = {}
Expand Down Expand Up @@ -118,7 +123,7 @@ def sub(pattern, self=self):

if not hasattr(func, 'rate'):
if hasattr(func, 'commands'):
func.rate = 20
func.rate = 0
else:
func.rate = 0

Expand Down
129 changes: 79 additions & 50 deletions irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""
irc.py - A Utility IRC Bot
Copyright 2008, Sean B. Palmer, inamidst.com
Modified by: Michael Yanovich
Licensed under the Eiffel Forum License 2.
More info:
Expand All @@ -13,6 +14,9 @@
import socket, asyncore, asynchat
import os, codecs

IRC_CODES = ('251', '252', '254', '255', '265', '266', '250', '333', '353', '366', '372', '375', '376', 'QUIT', 'NICK')
cwd = os.getcwd()

class Origin(object):
source = re.compile(r'([^!]*)!?([^@]*)@?(.*)')

Expand All @@ -28,20 +32,20 @@ def __init__(self, bot, source, args):
self.sender = mappings.get(target, target)

def create_logdir():
try: os.mkdir("logs")
try: os.mkdir(cwd + "/logs")
except Exception, e:
print >> sys.stderr, 'There was a problem creating the logs directory.'
print >> sys.stderr, e.__class__, str(e)
print >> sys.stderr, 'Please fix this and then run jenni again.'
sys.exit(1)

def check_logdir():
if not os.path.isdir("logs"):
if not os.path.isdir(cwd + "/logs"):
create_logdir()

def log_raw(line):
check_logdir()
f = codecs.open("logs/raw.log", 'a', encoding='utf-8')
f = codecs.open(cwd + "/logs/raw.log", 'a', encoding='utf-8')
f.write(str(time.time()) + "\t")
temp = line.replace('\n', '')
try:
Expand All @@ -56,7 +60,7 @@ def log_raw(line):
f.close()

class Bot(asynchat.async_chat):
def __init__(self, nick, name, channels, password=None, logchan_pm=None):
def __init__(self, nick, name, channels, password=None, logchan_pm=None, logging=False):
asynchat.async_chat.__init__(self)
self.set_terminator('\n')
self.buffer = ''
Expand All @@ -67,43 +71,56 @@ def __init__(self, nick, name, channels, password=None, logchan_pm=None):
self.password = password

self.verbose = True
self.channels = channels or []
self.stack = []
self.channels = channels or list()
self.stack = list()
self.stack_log = list()
self.logchan_pm = logchan_pm
self.logging = logging

import threading
self.sending = threading.RLock()

# def push(self, *args, **kargs):
# asynchat.async_chat.push(self, *args, **kargs)

def __write(self, args, text=None):
def __write(self, args, text=None, raw=False):
# print '%r %r %r' % (self, args, text)
try:
if text is not None:
# 510 because CR and LF count too, as nyuszika7h points out
temp = (' '.join(args) + ' :' + text)[:510] + '\r\n'
else:
temp = ' '.join(args)[:510] + '\r\n'
log_raw(temp)
self.push(temp)
if raw:
temp = ' '.join(args)[:510] + " :" + text + '\r\n'
self.push(temp)
elif not raw:
if text:
# 510 because CR and LF count too, as nyuszika7h points out
temp = (' '.join(args) + ' :' + text)[:510] + '\r\n'
else:
temp = ' '.join(args)[:510] + '\r\n'
self.push(temp)
if self.logging:
log_raw(temp)
except IndexError:
print "INDEXERROR", text
#pass

def write(self, args, text=None):
# This is a safe version of __write
def safe(input):
input = input.replace('\n', '')
input = input.replace('\r', '')
return input.encode('utf-8')
def write(self, args, text=None, raw=False):
try:
args = [safe(arg) for arg in args]
args = [self.safe(arg, u=True) for arg in args]
if text is not None:
text = safe(text)
self.__write(args, text)
text = self.safe(text, u=True)
if raw:
self.__write(args, text, raw)
else:
self.__write(args, text)
except Exception, e: pass

def safe(self, input, u=False):
if input:
input = input.replace('\n', '')
input = input.replace('\r', '')
if u:
input = input.encode('utf-8')
return input

def run(self, host, port=6667):
self.initiate_connect(host, port)

Expand All @@ -130,13 +147,15 @@ def handle_close(self):
print >> sys.stderr, 'Closed!'

def collect_incoming_data(self, data):
if data:
log_raw(data)
if hasattr(self, "logchan_pm") and self.logchan_pm and ("PRIVMSG" in data or "NOTICE" in data):
if len(data.split()) > 1:
if "#" not in data.split()[2]:
self.msg(self.logchan_pm, data)
self.buffer += data
if data:
if self.logchan_pm:
dlist = data.split()
if len(dlist) >= 3:
if "#" not in dlist[2] and dlist[1].strip() not in IRC_CODES:
self.msg(self.logchan_pm, data, True)
if self.logging:
log_raw(data)

def found_terminator(self):
line = self.buffer
Expand All @@ -163,7 +182,7 @@ def found_terminator(self):
def dispatch(self, origin, args):
pass

def msg(self, recipient, text):
def msg(self, recipient, text, log=False, x=False):
self.sending.acquire()

# Cf. http://swhack.com/logs/2006-03-01#T19-43-25
Expand All @@ -176,30 +195,40 @@ def msg(self, recipient, text):
except UnicodeEncodeError, e:
return

if not x:
text = text.replace('\x01', '')

# No messages within the last 3 seconds? Go ahead!
# Otherwise, wait so it's been at least 0.8 seconds + penalty
if self.stack:
elapsed = time.time() - self.stack[-1][0]
if elapsed < 3:
penalty = float(max(0, len(text) - 50)) / 70
wait = 0.8 + penalty
if elapsed < wait:
time.sleep(wait - elapsed)
def wait(sk, txt):
if sk:
elapsed = time.time() - sk[-1][0]
if elapsed < 3:
penalty = float(max(0, len(txt) - 50)) / 70
wait = 0.8 + penalty
if elapsed < wait:
time.sleep(wait - elapsed)
if log:
wait(self.stack_log, text)
else:
wait(self.stack, text)

# Loop detection
messages = [m[1] for m in self.stack[-8:]]
if messages.count(text) >= 5:
text = '...'
if messages.count('...') >= 3:
self.sending.release()
return

def safe(input):
input = input.replace('\n', '')
return input.replace('\r', '')
self.__write(('PRIVMSG', safe(recipient)), safe(text))
self.stack.append((time.time(), text))
if not log:
messages = [m[1] for m in self.stack[-8:]]
if messages.count(text) >= 5:
text = '...'
if messages.count('...') >= 3:
self.sending.release()
return

self.__write(('PRIVMSG', self.safe(recipient)), self.safe(text))
if log:
self.stack_log.append((time.time(), text))
else:
self.stack.append((time.time(), text))
self.stack = self.stack[-10:]
self.stack_log = self.stack_log[-10:]

self.sending.release()

Expand Down Expand Up @@ -239,5 +268,5 @@ def main():
# bot.run('irc.freenode.net')
print __doc__

if __name__=="__main__":
if __name__ == "__main__":
main()
8 changes: 7 additions & 1 deletion jenni
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
"""
jenni - An IRC Bot
Copyright 2008, Sean B. Palmer, inamidst.com
Modified by: Michael Yanovich
Licensed under the Eiffel Forum License 2.
http://inamidst.com/phenny/
More info:
* Jenni: https://github.com/myano/jenni/
* Phenny: http://inamidst.com/phenny/
Note: DO NOT EDIT THIS FILE.
Run ./jenni, then edit ~/.jenni/default.py
Expand Down Expand Up @@ -42,6 +45,9 @@ def create_default_config(fn):
# But admin.py is disabled by default, as follows:
exclude = ['admin', 'adminchannel', 'ai', 'resp', 'school']
# Enable raw logging of everything jenni sees
logging = False
# Block modules from specific channels
# To not block anything for a channel, just don't mention it
excludes = {
Expand Down
75 changes: 44 additions & 31 deletions modules/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,55 @@
import os

def join(jenni, input):
"""Join the specified channel. This is an admin-only command."""
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
if input.admin:
channel, key = input.group(1), input.group(2)
if not key:
jenni.write(['JOIN'], channel)
else: jenni.write(['JOIN', channel, key])
"""Join the specified channel. This is an admin-only command."""
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
if input.admin:
channel, key = input.group(1), input.group(2)
if not key:
jenni.write(['JOIN'], channel)
else: jenni.write(['JOIN', channel, key])
join.rule = r'\.join (#\S+)(?: *(\S+))?'
join.priority = 'low'
join.example = '.join #example or .join #example key'

def part(jenni, input):
"""Part the specified channel. This is an admin-only command."""
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
if input.admin:
jenni.write(['PART'], input.group(2))
"""Part the specified channel. This is an admin-only command."""
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
if input.admin:
jenni.write(['PART'], input.group(2))
part.commands = ['part']
part.priority = 'low'
part.example = '.part #example'

def quit(jenni, input):
"""Quit from the server. This is an owner-only command."""
# Can only be done in privmsg by the owner
if input.sender.startswith('#'): return
if input.owner:
jenni.write(['QUIT'])
__import__('os')._exit(0)
"""Quit from the server. This is an owner-only command."""
# Can only be done in privmsg by the owner
if input.sender.startswith('#'): return
if input.owner:
jenni.write(['QUIT'])
__import__('os')._exit(0)
quit.commands = ['quit']
quit.priority = 'low'

def msg(jenni, input):
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
a, b = input.group(2), input.group(3)
if (not a) or (not b): return
if input.admin:
jenni.msg(a, b)
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
a, b = input.group(2), input.group(3)
if (not a) or (not b): return
if input.admin:
jenni.msg(a, b)
msg.rule = (['msg'], r'(#?\S+) (.+)')
msg.priority = 'low'

def me(jenni, input):
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
if input.admin:
msg = '\x01ACTION %s\x01' % input.group(3)
jenni.msg(input.group(2), msg)
# Can only be done in privmsg by an admin
if input.sender.startswith('#'): return
if input.admin:
if input.group(2) and input.group(3):
msg = '\x01ACTION %s\x01' % input.group(3)
jenni.msg(input.group(2), msg, x=True)
me.rule = (['me'], r'(#?\S+) (.*)')
me.priority = 'low'

Expand Down Expand Up @@ -175,6 +176,18 @@ def blocks(jenni, input):
blocks.priority = 'low'
blocks.thread = False

def write_raw(jenni, input):
if not input.owner: return
txt = input.bytes[7:]
jenni.reply(txt)
txt = txt.encode('utf-8')
a = txt.split(":")
jenni.reply(str(a))
jenni.write([a[0].strip()],a[1].strip(),True)
write_raw.commands = ['write']
write_raw.priority = 'high'
write_raw.thread = False

if __name__ == '__main__':
print __doc__.strip()
print __doc__.strip()

2 changes: 1 addition & 1 deletion modules/unobot.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

# Remember to change these 3 lines or nothing will work
CHANNEL = '##uno'
SCOREFILE = "/home/jenni/dev/unoscores.txt"
SCOREFILE = "/home/jenni/jenni/unoscores.txt"
# Only the owner (starter of the game) can call .unostop to stop the game.
# But this calls for a way to allow others to stop it after the game has been idle for a while.
# After this set time, anyone can stop the game via .unostop
Expand Down

0 comments on commit 4dae575

Please sign in to comment.