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

33
game.py
View File

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

View File

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

View File

@ -34,11 +34,11 @@ class MyHandler(asyncore.dispatcher):
team = team or house
# Replays can happen legitimately.
if not (id in self.acked):
if not ((peer, id) in self.acked):
if not (now - 2 < when <= now):
return self.respond(peer, id, 'Your clock is off')
self.store.add((when, cat, team, score))
self.acked.add(id)
self.acked.add((peer, id))
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">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>yo mom</title>
<title>CTF Scoreboard</title>
<link rel="stylesheet" href="ctf.css" type="text/css" />
</head>
<body style="background: black; color: white;">
<body>
<h1>Scoreboard</h1>
''')
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))