2009-10-05 14:33:30 -06:00
|
|
|
import badmath
|
|
|
|
import time
|
|
|
|
import os
|
|
|
|
import traceback
|
|
|
|
import pickle
|
|
|
|
from hashlib import sha256
|
|
|
|
|
2009-10-05 15:30:37 -06:00
|
|
|
try:
|
|
|
|
from ctf import irc
|
|
|
|
from ctf.flagd import Flagger
|
|
|
|
except:
|
|
|
|
import sys
|
|
|
|
sys.path.append('/home/pflarr/repos/gctf/')
|
2009-10-05 15:43:24 -06:00
|
|
|
from ctf.flagd import Flagger
|
|
|
|
from ctf import irc
|
2009-10-05 14:33:30 -06:00
|
|
|
|
|
|
|
class Gyopi(irc.Bot):
|
2009-10-05 15:43:24 -06:00
|
|
|
STATE_FN = 'badmath.state'
|
2009-10-05 14:33:30 -06:00
|
|
|
|
|
|
|
SALT = b'this is questionable.'
|
|
|
|
|
|
|
|
FLAG_DEFAULT = 'dirtbags'
|
|
|
|
MAX_ATTEMPT_RATE = 3
|
|
|
|
NOBODY = '\002[nobody]\002'
|
|
|
|
|
|
|
|
FLAG_HOST = b'ctf1.lanl.gov'
|
|
|
|
# FLAG_HOST = b'localhost'
|
|
|
|
|
2009-10-05 15:30:37 -06:00
|
|
|
def __init__(self, host, channels, dataPath, flagger):
|
|
|
|
irc.Bot.__init__(self, host, ['gyopi'], 'Gyopi', channels)
|
2009-10-05 14:33:30 -06:00
|
|
|
|
|
|
|
self._dataPath = dataPath
|
|
|
|
|
|
|
|
self._flag = flagger
|
|
|
|
|
|
|
|
try:
|
|
|
|
self._loadState()
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
self._lvl = 0
|
|
|
|
self._flag.set_flag( self.FLAG_DEFAULT )
|
|
|
|
|
2009-10-07 10:50:18 -06:00
|
|
|
self._tokens = []
|
2009-10-05 14:33:30 -06:00
|
|
|
self._lastAttempt = {}
|
|
|
|
self._affiliations = {}
|
|
|
|
self._newPuzzle()
|
|
|
|
|
|
|
|
def err(self, exception):
|
|
|
|
"""Save the traceback for later inspection"""
|
|
|
|
irc.Bot.err(self, exception)
|
|
|
|
t,v,tb = exception
|
|
|
|
info = []
|
|
|
|
while 1:
|
|
|
|
info.append('%s:%d(%s)' %
|
|
|
|
(os.path.basename(tb.tb_frame.f_code.co_filename),
|
|
|
|
tb.tb_lineno,
|
|
|
|
tb.tb_frame.f_code.co_name))
|
|
|
|
tb = tb.tb_next
|
|
|
|
if not tb:
|
|
|
|
break
|
|
|
|
del tb # just to be safe
|
|
|
|
infostr = '[' + '] ['.join(info) + ']'
|
|
|
|
self.last_tb = '%s %s %s' % (t, v, infostr)
|
|
|
|
print(self.last_tb)
|
|
|
|
|
2009-10-07 10:50:18 -06:00
|
|
|
def cmd_JOIN(self, sender, forum, addl):
|
2009-10-05 14:33:30 -06:00
|
|
|
"""On join, announce who has the flag."""
|
|
|
|
if sender.name() in self.nicks:
|
|
|
|
self._tellFlag(forum)
|
|
|
|
self._tellPuzzle(forum)
|
|
|
|
|
|
|
|
def _newPuzzle(self):
|
|
|
|
"""Create a new puzzle."""
|
|
|
|
self._key, self._puzzle, self._banned = badmath.mkPuzzle(self._lvl)
|
|
|
|
|
|
|
|
def _loadState(self):
|
|
|
|
"""Load the last state from the stateFile."""
|
|
|
|
statePath = os.path.join(self._dataPath, self.STATE_FN)
|
|
|
|
stateFile = open( statePath, 'br' )
|
|
|
|
state = pickle.load(stateFile)
|
|
|
|
self._lvl = state['lvl']
|
|
|
|
self._flag.set_flag( state['flag'] )
|
|
|
|
self._lastAttempt = state['lastAttempt']
|
|
|
|
self._affiliations = state['affiliations']
|
|
|
|
self._puzzle = state['puzzle']
|
|
|
|
self._key = state['key']
|
|
|
|
self._banned = state['banned']
|
|
|
|
self._tokens = state.get('tokens', [])
|
|
|
|
|
|
|
|
def _saveState(self):
|
|
|
|
"""Write the current state to file."""
|
|
|
|
state = {'lvl': self._lvl,
|
|
|
|
'flag': self._flag.flag,
|
|
|
|
'lastAttempt': self._lastAttempt,
|
|
|
|
'affiliations': self._affiliations,
|
|
|
|
'puzzle': self._puzzle,
|
|
|
|
'key': self._key,
|
|
|
|
'banned': self._banned,
|
|
|
|
'tokens': self._tokens}
|
|
|
|
|
|
|
|
# Do the write as an atomic move operation
|
|
|
|
statePath = os.path.join(self._dataPath, self.STATE_FN)
|
|
|
|
stateFile = open(statePath + '.tmp', 'wb')
|
|
|
|
pickle.dump(state, stateFile)
|
|
|
|
stateFile.close()
|
2009-10-05 15:30:37 -06:00
|
|
|
os.rename( statePath + '.tmp', statePath)
|
2009-10-05 14:33:30 -06:00
|
|
|
|
|
|
|
def _tellFlag(self, forum):
|
|
|
|
"""Announce who owns the flag."""
|
|
|
|
forum.msg('%s has the flag.' % (self._flag.flag))
|
|
|
|
|
|
|
|
def _tellPuzzle(self, forum):
|
|
|
|
"""Announce the current puzzle."""
|
2009-10-05 15:30:37 -06:00
|
|
|
forum.msg('Difficulty level is %d' % self._lvl)
|
2009-10-05 14:33:30 -06:00
|
|
|
forum.msg('The problem is: %s' % ' '.join( map(str, self._puzzle)))
|
|
|
|
|
|
|
|
def _getStations(self):
|
|
|
|
stations = {}
|
|
|
|
with open(os.path.join(STORAGE, 'stations.txt')) as file:
|
|
|
|
lines = file.readlines()
|
|
|
|
for line in lines:
|
|
|
|
try:
|
|
|
|
name, file = line.split(':')
|
|
|
|
except:
|
|
|
|
continue
|
|
|
|
stations[name] = file
|
|
|
|
|
|
|
|
return stations
|
|
|
|
|
|
|
|
def _giveToken(self, user, forum):
|
|
|
|
"""Hand a Jukebox token to the user."""
|
|
|
|
|
|
|
|
token = self._jukebox.mkToken(user)
|
|
|
|
|
|
|
|
forum.msg('You get a jukebox token: %s' % token)
|
|
|
|
forum.msg('Use this with the !set command to change the music.')
|
|
|
|
forum.msg('This token is specific to your user name, and is only '
|
|
|
|
'useable once.')
|
|
|
|
|
|
|
|
def _useToken(self, user, forum, token, station):
|
|
|
|
"""Use the given token, and change the current station to station."""
|
|
|
|
try:
|
|
|
|
station = int(station)
|
|
|
|
stations = self._getStations()
|
|
|
|
assert station in stations
|
|
|
|
except:
|
|
|
|
forum.msg('%s: Invalid Station (%s)' % station)
|
|
|
|
return
|
|
|
|
|
|
|
|
if token in self._tokens[user]:
|
|
|
|
self._tokens[user].remove(token)
|
|
|
|
|
|
|
|
|
2009-10-07 10:50:18 -06:00
|
|
|
def cmd_PRIVMSG(self, sender, forum, addl):
|
2009-10-05 14:33:30 -06:00
|
|
|
text = addl[0]
|
|
|
|
who = sender.name()
|
|
|
|
if text.startswith('!'):
|
2009-10-07 10:50:18 -06:00
|
|
|
parts = text[1:].split(' ', 1)
|
2009-10-05 14:33:30 -06:00
|
|
|
cmd = parts[0]
|
|
|
|
if len(parts) > 1:
|
|
|
|
args = parts[1]
|
|
|
|
else:
|
|
|
|
args = None
|
|
|
|
if cmd.startswith('r'):
|
|
|
|
# Register
|
|
|
|
if args:
|
|
|
|
self._affiliations[who] = args
|
|
|
|
team = self._affiliations.get(who, self.NOBODY)
|
|
|
|
forum.msg('%s is playing for %s' % (who, team))
|
|
|
|
elif cmd.startswith('w'):
|
|
|
|
forum.msg('Teams:')
|
|
|
|
for player in self._affiliations:
|
|
|
|
forum.msg('%s: %s' % (player, self._affiliations[player]))
|
|
|
|
elif cmd.startswith('embrace'):
|
|
|
|
# Embrace
|
|
|
|
forum.ctcp('ACTION', 'is devoid of emotion.')
|
|
|
|
elif cmd.startswith('f'):
|
|
|
|
# Flag
|
|
|
|
self._tellFlag(forum)
|
|
|
|
elif cmd.startswith('h'):
|
|
|
|
# Help
|
|
|
|
forum.msg('Goal: Help me with my math homework, FROM ANOTHER DIMENSION!')
|
2009-10-07 10:50:18 -06:00
|
|
|
forum.msg('Order of operations is always left to right.')
|
2009-10-05 15:30:37 -06:00
|
|
|
#forum.msg('Goal: The current winner gets to control the contest music.')
|
|
|
|
forum.msg('Commands: !help, !flag, !register [TEAM], !solve SOLUTION,!? EQUATION, !ops, !problem, !who')
|
2009-10-05 14:33:30 -06:00
|
|
|
elif cmd.startswith('prob'):
|
|
|
|
self._tellPuzzle(forum)
|
|
|
|
elif cmd.startswith('solve') and args:
|
|
|
|
# Solve
|
|
|
|
team = self._affiliations.get(who)
|
|
|
|
lastAttempt = time.time() - self._lastAttempt.get(team, 0)
|
|
|
|
answer = badmath.solve(self._key, self._puzzle)
|
|
|
|
try:
|
|
|
|
attempt = int(''.join(args).strip())
|
|
|
|
except:
|
|
|
|
forum.msg("%s: Answers are always integers.")
|
|
|
|
if not team:
|
|
|
|
forum.msg('%s: register first (!register TEAM).' % who)
|
|
|
|
elif self._flag.flag == team:
|
|
|
|
forum.msg('%s: Greedy, greedy.' % who)
|
|
|
|
elif lastAttempt < self.MAX_ATTEMPT_RATE:
|
|
|
|
forum.msg('%s: Wait at least %d seconds between attempts' %
|
|
|
|
(team, self.MAX_ATTEMPT_RATE))
|
|
|
|
elif answer == attempt:
|
|
|
|
self._flag.set_flag( team )
|
|
|
|
self._lvl = self._lvl + 1
|
|
|
|
self._tellFlag(forum)
|
|
|
|
self._newPuzzle()
|
|
|
|
self._tellPuzzle(forum)
|
|
|
|
# self._giveToken(who, sender)
|
|
|
|
self._saveState()
|
|
|
|
else:
|
|
|
|
forum.msg('%s: That is not correct.' % who)
|
|
|
|
|
|
|
|
# Test a simple one op command.
|
|
|
|
elif cmd.startswith('?'):
|
2009-10-07 10:50:18 -06:00
|
|
|
if not args:
|
|
|
|
forum.msg('%s: Give me an easier problem, and I\'ll '
|
|
|
|
'give you the answer.' % who)
|
|
|
|
return
|
|
|
|
|
2009-10-05 14:33:30 -06:00
|
|
|
try:
|
|
|
|
tokens = badmath.parse(''.join(args))
|
|
|
|
except (ValueError) as msg:
|
|
|
|
forum.msg('%s: %s' % (who, msg))
|
|
|
|
return
|
|
|
|
|
|
|
|
if len(tokens) > 3:
|
|
|
|
forum.msg('%s: You can only test one op at a time.' % who)
|
|
|
|
|
|
|
|
for num in self._banned:
|
|
|
|
if num in tokens:
|
|
|
|
forum.msg('%s: You can\'t test numbers in the '
|
|
|
|
'puzzle.' % who)
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
result = badmath.solve(self._key, tokens)
|
|
|
|
forum.msg('%s: %s -> %d' % (who, ''.join(args), result))
|
|
|
|
except:
|
|
|
|
forum.msg("%s: That doesn't work at all." % who)
|
|
|
|
|
|
|
|
elif cmd == 'birdzerk':
|
|
|
|
self._saveState()
|
|
|
|
|
|
|
|
elif cmd == 'traceback':
|
|
|
|
forum.msg(self.last_tb or 'No traceback')
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
import optparse
|
|
|
|
|
|
|
|
p = optparse.OptionParser()
|
2009-10-05 15:30:37 -06:00
|
|
|
p.add_option('-i', '--irc', dest='ircHost', default='localhost',
|
|
|
|
help='IRC Host to connect to.')
|
2009-10-05 14:33:30 -06:00
|
|
|
p.add_option('-f', '--flagd', dest='flagd', default='localhost',
|
2009-10-05 15:30:37 -06:00
|
|
|
help='Flag Server to connect to')
|
2009-10-05 14:33:30 -06:00
|
|
|
p.add_option('-p', '--password', dest='password',
|
|
|
|
default='badmath:::a41c6753210c0bdafd84b3b62d7d1666',
|
|
|
|
help='Flag server password')
|
|
|
|
p.add_option('-d', '--path', dest='path', default='/var/lib/badmath',
|
2009-10-05 15:30:37 -06:00
|
|
|
help='Path to where we can store state info.')
|
2009-10-07 10:50:18 -06:00
|
|
|
p.add_option('-c', '--channel', dest='channel', default='#badmath',
|
2009-10-05 15:30:37 -06:00
|
|
|
help='Which channel to join')
|
2009-10-05 14:33:30 -06:00
|
|
|
|
|
|
|
opts, args = p.parse_args()
|
2009-10-05 15:30:37 -06:00
|
|
|
channels = [opts.channel]
|
2009-10-05 14:33:30 -06:00
|
|
|
|
2009-10-05 15:30:37 -06:00
|
|
|
flagger = Flagger(opts.flagd, opts.password.encode('utf-8'))
|
|
|
|
gyopi = Gyopi((opts.ircHost, 6667), channels, opts.path, flagger)
|
2009-10-05 14:33:30 -06:00
|
|
|
irc.run_forever()
|