Somewhat working game

This commit adds:

* Registration
* Puzzler cgi script
* Several puzzles
* Style sheet for HTML
This commit is contained in:
Neale Pickett 2009-09-02 10:15:54 -06:00
parent d638a3be0c
commit 5f39aff5de
24 changed files with 417 additions and 29 deletions

21
ctf.css Normal file
View File

@ -0,0 +1,21 @@
body {
background: #000;
color: #0f0;
}
.readme {
background: #444;
}
a:link {
color: #ff0;
}
a:visited {
color: #880;
}
a:hover {
color: #000;
background: #ff0;
}
.error {
color: #000;
background: #f00;
}

View File

@ -9,6 +9,7 @@ import hmac
import optparse import optparse
import points import points
import pointscli import pointscli
import teams
import traceback import traceback
key = b'My First Shared Secret (tm)' key = b'My First Shared Secret (tm)'
@ -67,7 +68,7 @@ class Submitter(asyncore.dispatcher):
def set_flag(self, cat, team): def set_flag(self, cat, team):
now = int(time.time()) now = int(time.time())
team = team or points.house team = team or teams.house
if self.flags.get(cat) != team: if self.flags.get(cat) != team:
self.flags[cat] = team self.flags[cat] = team

33
game.py
View File

@ -6,6 +6,7 @@ import asynchat
import socket import socket
import traceback import traceback
import time import time
import teams
from errno import EPIPE from errno import EPIPE
@ -25,7 +26,6 @@ class Listener(asyncore.dispatcher):
self.listen(4) self.listen(4)
self.player_factory = player_factory self.player_factory = player_factory
self.manager = manager self.manager = manager
self.last_beat = 0
def handle_accept(self): def handle_accept(self):
conn, addr = self.accept() conn, addr = self.accept()
@ -34,9 +34,7 @@ class Listener(asyncore.dispatcher):
# has a reference to it for as long as it's open. # has a reference to it for as long as it's open.
def readable(self): def readable(self):
now = time.time() self.manager.heartbeat(time.time())
if now > self.last_beat + pulse:
self.manager.heartbeat(now)
return True return True
@ -89,11 +87,25 @@ class Manager:
self.lobby = set() self.lobby = set()
self.contestants = [] self.contestants = []
self.last_beat = 0 self.last_beat = 0
self.timers = set()
def heartbeat(self, now): def heartbeat(self, now):
# Called by listener to beat heart """Called by listener to beat heart."""
for game in list(self.games):
game.heartbeat(now) now = time.time()
if now > self.last_beat + pulse:
for game in list(self.games):
game.heartbeat(now)
for event in self.timers:
when, cb = event
if now >= when:
self.timers.remove(event)
cb()
def add_timer(self, when, cb):
"""Add a timed callback."""
self.timers.add((when, cb))
def enter_lobby(self, player): def enter_lobby(self, player):
self.lobby.add(player) self.lobby.add(player)
@ -248,13 +260,14 @@ class Player(asynchat.async_chat):
cmd, args = val[0].lower(), val[1:] cmd, args = val[0].lower(), val[1:]
if cmd == 'login': if cmd == 'login':
if not self.name: if self.name:
# XXX Check password self.err('Already logged in.')
elif teams.chkpasswd(args[0], args[1]):
self.name = args[0] self.name = args[0]
self.write('Welcome to the fray, %s.' % self.name) self.write('Welcome to the fray, %s.' % self.name)
self.manager.enter_lobby(self) self.manager.enter_lobby(self)
else: else:
self.err('Already logged in.') self.err('Invalid password.')
elif cmd == '^': elif cmd == '^':
# Send to manager # Send to manager
ret = self.manager.player_cmd(args) ret = self.manager.player_cmd(args)

30
games/crypto/scytale.py Executable file
View File

@ -0,0 +1,30 @@
#! /usr/bin/env python3
import sys
import random
primes = [2, 3, 5, 7, 11, 13, 17, 19]
letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
data = sys.stdin.read().strip()
jumble = ''.join(data.split())
lj = len(jumble)
below = (0, 0)
above = (lj, 2)
for i in primes:
for j in primes:
m = i * j
if (m < lj) and (m > below[0] * below[1]):
below = (i, j)
elif (m >= lj) and (m < (above[0] * above[1])):
above = (i, j)
for i in range(lj, (above[0] * above[1])):
jumble += random.choice(letters)
out = []
for i in range(above[0]):
for j in range(above[1]):
out.append(jumble[j*above[0] + i])
print(''.join(out))

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Capture The Flag</title>
<link rel="stylesheet" href="ctf.css" type="text/css" />
</head>
<body>
<h1>Capture The Flag</h1>
<ol>
<li><a href="register.cgi">Register</a> your team</li>
<li><a href="scoreboard.cgi">Scoreboard</a></li>
</ol>
<p>
Some challenges are <a href="puzzler.cgi">puzzles</a>. Some are
sitting on the network; you must find these yourself!
</p>
</body>
</html>

View File

@ -4,9 +4,7 @@ import socket
import hmac import hmac
import struct import struct
import io import io
import teams
## Name of the house team
house = 'dirtbags'
## ##
## Authentication ## Authentication

View File

@ -6,10 +6,17 @@ import points
import socket import socket
import time import time
def submit(sock, cat, team, score): def makesock(host):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((host, 6667))
return s
def submit(cat, team, score, sock=None):
if not sock:
sock = makesock('cfl-sunray1')
begin = time.time() begin = time.time()
mark = int(begin) mark = int(begin)
req = points.encode_request(mark, cat, team, score) req = points.encode_request(1, mark, cat, team, score)
while True: while True:
sock.send(req) sock.send(req)
r, w, x = select.select([sock], [], [], begin + 2 - time.time()) r, w, x = select.select([sock], [], [], begin + 2 - time.time())
@ -17,12 +24,12 @@ def submit(sock, cat, team, score):
break break
b = sock.recv(500) b = sock.recv(500)
try: try:
when, cat_, txt = points.decode_response(b) id, txt = points.decode_response(b)
except ValueError: except ValueError:
# Ignore invalid packets # Ignore invalid packets
continue continue
if (when != mark) or (cat_ != cat): if id != 1:
# Ignore wrong timestamp # Ignore wrong ID
continue continue
if txt == 'OK': if txt == 'OK':
return return
@ -30,11 +37,6 @@ def submit(sock, cat, team, score):
raise ValueError(txt) raise ValueError(txt)
def makesock(host):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((host, 6667))
return s
def main(): def main():
p = optparse.OptionParser(usage='%prog CATEGORY TEAM SCORE') p = optparse.OptionParser(usage='%prog CATEGORY TEAM SCORE')
p.add_option('-s', '--host', dest='host', default='localhost', p.add_option('-s', '--host', dest='host', default='localhost',
@ -50,7 +52,7 @@ def main():
s = makesock(opts.host) s = makesock(opts.host)
try: try:
submit(s, cat, team, score) submit(cat, team, score, sock=s)
except ValueError as err: except ValueError as err:
print(err) print(err)
raise raise

View File

@ -34,11 +34,11 @@ class MyHandler(asyncore.dispatcher):
team = team or house team = team or house
# Replays can happen legitimately. # Replays can happen legitimately.
if not (id in self.acked): if not ((peer, id) in self.acked):
if not (now - 2 < when <= now): if not (now - 2 < when <= now):
return self.respond(peer, id, 'Your clock is off') return self.respond(peer, id, 'Your clock is off')
self.store.add((when, cat, team, score)) self.store.add((when, cat, team, score))
self.acked.add(id) self.acked.add((peer, id))
self.respond(peer, id, 'OK') self.respond(peer, id, 'OK')

176
puzzler.cgi Executable file
View File

@ -0,0 +1,176 @@
#! /usr/bin/env python3
import cgitb; cgitb.enable()
import cgi
import os
import fcntl
import re
import sys
import pointscli
import teams
cat_re = re.compile(r'^[a-z]+$')
points_re = re.compile(r'^[0-9]+$')
def dbg(*vals):
print('Content-type: text/plain\n\n')
print(*vals)
points_by_cat = {}
points_by_team = {}
try:
for line in open('puzzler.dat'):
line = line.strip()
cat, team, pts = line.split('\t')
pts = int(pts)
points_by_cat[cat] = max(points_by_cat.get(cat, 0), pts)
points_by_team.setdefault((team, cat), set()).add(pts)
except IOError:
pass
f = cgi.FieldStorage()
cat = f.getfirst('c')
points = f.getfirst('p')
team = f.getfirst('t')
passwd = f.getfirst('w')
key = f.getfirst('k')
verboten = ['key', 'index.html']
def start_html(title):
print('''Content-type: text/html
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%s</title>
<link rel="stylesheet" href="ctf.css" type="text/css" />
</head>
<body>
<h1>%s</h1>
''' % (title, title))
def end_html():
print('</body></html>')
def safe_join(*args):
safe = []
for a in args:
if not a:
return None
else:
a = a.replace('..', '')
a = a.replace('/', '')
safe.append(a)
ret = '/'.join(safe)
if os.path.exists(ret):
return ret
def dump_file(fn):
f = open(fn, 'rb')
while True:
d = f.read(4096)
if not d:
break
sys.stdout.buffer.write(d)
def show_cats():
start_html('Categories')
print('<ul>')
for p in sorted(os.listdir('puzzles')):
print('<li><a href="puzzler.cgi?c=%s">%s</a></li>' % (p, p))
print('</ul>')
end_html()
def show_puzzles(cat, cat_dir):
start_html('Open in %s' % cat)
opened = points_by_cat.get(cat, 0)
puzzles = sorted([int(v) for v in os.listdir(cat_dir)])
if puzzles:
print('<ul>')
opened = max(opened, puzzles[0])
for p in puzzles:
if p <= opened:
print('<li><a href="puzzler.cgi?c=%s&p=%d">%d</a></li>' % (cat, p, p))
print('</ul>')
else:
print('<p>None (someone is slacking)</p>')
end_html()
def show_puzzle(cat, points, points_dir):
# Show puzzle in cat for points
start_html('%s for %s' % (cat, points))
fn = os.path.join(points_dir, 'index.html')
if os.path.exists(fn):
print('<div class="readme">')
dump_file(fn)
print('</div>')
print('<ul>')
for fn in sorted(os.listdir(points_dir)):
if fn.endswith('~') or fn.startswith('.') or fn in verboten:
continue
print('<li><a href="puzzler.cgi?c=%s&p=%s&f=%s">%s</a></li>' % (cat, points, fn, fn))
print('</ul>')
print('<form action="puzzler.cgi" method="post">')
print('<input type="hidden" name="c" value="%s" />' % cat)
print('<input type="hidden" name="p" value="%s" />' % points)
print('Team: <input name="t" /><br />')
print('Password: <input type="password" name="w" /><br />')
print('Key: <input name="k" /><br />')
print('<input type="submit" />')
print('</form>')
end_html()
def win(cat, team, points):
start_html('Winner!')
points = int(points)
pointscli.submit(cat, team, points)
end_html()
f = open('puzzler.dat', 'a')
fctnl.lockf(f, LOCK_EX)
f.write('%s\t%s\t%d\n' % (cat, team, points))
def main():
cat_dir = safe_join('puzzles', cat)
points_dir = safe_join('puzzles', cat, points)
if not cat_dir:
# Show categories
show_cats()
elif not points_dir:
# Show available puzzles in category
show_puzzles(cat, cat_dir)
elif not (team and passwd and key):
fn = f.getfirst('f')
if fn in verboten:
fn = None
fn = safe_join('puzzles', cat, points, fn)
if fn:
# Provide a file from this directory
print('Content-type: application/octet-stream')
print()
dump_file(fn)
else:
show_puzzle(cat, points, points_dir)
else:
thekey = open('%s/key' % points_dir).read().strip()
if not teams.chkpasswd(team, passwd):
start_html('Wrong password')
end_html()
elif key != thekey:
show_puzzle(cat, points, points_dir)
elif points_by_team.get((team, cat)):
start_html('Greedy greedy')
end_html()
else:
win(cat, team, points)
main()

1
puzzles/bletchey/100/key Normal file
View File

@ -0,0 +1 @@
antediluvian

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

@ -0,0 +1 @@
tkftsuiuqvaheohrnsnuoleyriod"eic"

1
puzzles/bletchey/200/key Normal file
View File

@ -0,0 +1 @@
unequivocal

View File

@ -0,0 +1 @@
27586126814341379597440261571645814840581961154587430529221052323

1
puzzles/bletchey/250/key Normal file
View File

@ -0,0 +1 @@
DB1663<3

View File

@ -0,0 +1,13 @@
<p>Kolejne modele Panzerfausta, odpowiednio: 60, 100, 150, różnił kaliber głowicy i wielkość ładunku miotającego. Konstrukcja i mechanizm nie ulegał istotnym zmianom, z racji wzrastania zasięgu broni modyfikacjom ulegały nastawy celowników. Jedynie we wzorze 150 wprowadzono (a był to już początek 1945 roku) wielokrotne użycie wyrzutni rurowej. Osiągnięto to przez umieszczenie ładunku miotającego w głowicy oraz przez wzmocnienie rury. W wyniku problemu z transportem model ów nie wszedł do walki. Model 250 (o teoretycznym zasięgu 250 m) z racji zakończenia wojny nie opuścił desek kreślarskich nigdy nie wchodząc nawet w fazę prototypową.</p>
<pre>(61, 4)
(47, 8)
(19, 4)
(37, 1)
(51, 3)
(67, 5)
(9, 2)
(26, 1)
(2, 2)
(26, 3)
(50, 2)</pre>

1
puzzles/bletchey/300/key Normal file
View File

@ -0,0 +1 @@
jako561962

Binary file not shown.

View File

@ -0,0 +1 @@
31 9 15 26 14 23 14 6 18 5 12 18 5 2 16 27 7 10 11 5 13 31 17 17 6 2 26 26 10 21 10 8 20 4

View File

@ -0,0 +1 @@
journals.uchicago

1
puzzles/bletchey/500/key Normal file
View File

@ -0,0 +1 @@
xez.3nt

62
register.cgi Executable file
View File

@ -0,0 +1,62 @@
#! /usr/bin/env python3
import cgitb; cgitb.enable()
import cgi
import teams
import fcntl
import string
print('Content-type: text/html')
print()
f = cgi.FieldStorage()
team = f.getfirst('team', '')
pw = f.getfirst('pw')
confirm_pw = f.getfirst('confirm_pw')
html = string.Template('''<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Team Registration</title>
<link rel="stylesheet" href="ctf.css" type="text/css" />
</head>
<body>
<h1>Team Registration</h1>
<form method="post" action="register.cgi">
<fieldset>
<label>Desired Team Team:</label>
<input type="text" name="team" />
<span class="error">$team_error</span><br />
<label>Password:</label>
<input type="password" name="pw" /> <br />
<label>Confirm Password:</label>
<input type="password" name="confirm_pw" />
<span class="error">$pw_match_error</span><br />
<input type="submit" value="Register" />
</fieldset>
</form>
</body>
</html>
''')
if not (team and pw and confirm_pw): #If we're starting from the beginning?
html = html.substitute(team_error='',
pw_match_error='')
elif teams.exists(team):
html = html.substitute(team_error='Team team already taken',
pw_match_error='')
elif pw != confirm_pw:
html = html.substitute(team_error='',
pw_match_error='Passwords do not match')
else:
teams.add(team, pw)
html = 'Team registered.'
print(html)

View File

@ -17,9 +17,10 @@ print('''<?xml version="1.0" encoding="UTF-8" ?>
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head> <head>
<title>yo mom</title> <title>CTF Scoreboard</title>
<link rel="stylesheet" href="ctf.css" type="text/css" />
</head> </head>
<body style="background: black; color: white;"> <body>
<h1>Scoreboard</h1> <h1>Scoreboard</h1>
''') ''')
print('<table>') print('<table>')

40
teams.py Executable file
View File

@ -0,0 +1,40 @@
#! /usr/bin/env python3
import fcntl
house = 'dirtbags'
teams = None
def build_teams():
global teams
teams = {}
try:
f = open('passwd')
for line in f:
team, passwd = line.strip().split('\t')
teams[team] = passwd
except IOError:
pass
def chkpasswd(team, passwd):
if teams is None:
build_teams()
if teams.get(team) == passwd:
return True
else:
return False
def exists(team):
if teams is None:
build_teams()
if team == house:
return True
return team in teams
def add(team, passwd):
f = open('passwd', 'a')
fcntl.lockf(f, fcntl.LOCK_EX)
f.seek(0, 2)
f.write('%s\t%s\n' % (team, passwd))