diff --git a/devel/devel-server.py b/devel/devel-server.py index 354f899..4db0b4a 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import asyncio +import cgitb import glob import html from aiohttp import web @@ -15,11 +16,12 @@ import shutil import socketserver import sys import traceback +import mothballer sys.dont_write_bytecode = True # Don't write .pyc files def mkseed(): - return bytes(random.choice(b'abcdef0123456789') for i in range(40)) + return bytes(random.choice(b'abcdef0123456789') for i in range(40)).decode('ascii') class Page: def __init__(self, title, depth=0): @@ -72,11 +74,17 @@ async def handle_front(request): return p.response(request) async def handle_puzzlelist(request): + seed = request.query.get("seed", mkseed()) p = Page("Puzzle Categories", 1) + p.write("

seed = {}

".format(seed)) p.write("") return p.response(request) @@ -137,7 +145,7 @@ async def handle_puzzle(request): return p.response(request) async def handle_puzzlefile(request): - seed = request.query.get("seed", mkseed()) + seed = request.query.get("seed", mkseed()).encode('ascii') category = request.match_info.get("category") points = int(request.match_info.get("points")) filename = request.match_info.get("filename") @@ -158,6 +166,25 @@ async def handle_puzzlefile(request): resp.body = file.stream.read() return resp +async def handle_mothballer(request): + seed = request.query.get("seed", mkseed()) + category = request.match_info.get("category") + + try: + catdir = os.path.join(request.app["puzzles_dir"], category) + mb = mothballer.package(category, catdir, seed) + except: + body = cgitb.html(sys.exc_info()) + resp = web.Response(text=body, content_type="text/html") + return resp + + mb_buf = mb.read() + resp = web.Response( + body=mb_buf, + headers={"Content-Disposition": "attachment; filename={}.mb".format(category)}, + content_type="application/octet_stream", + ) + return resp if __name__ == '__main__': import argparse @@ -192,5 +219,6 @@ if __name__ == '__main__': app.router.add_route("GET", "/puzzles/{category}/", handle_category) app.router.add_route("GET", "/puzzles/{category}/{points}/", handle_puzzle) app.router.add_route("GET", "/puzzles/{category}/{points}/{filename}", handle_puzzlefile) + app.router.add_route("GET", "/mothballer/{category}", handle_mothballer) app.router.add_static("/files/", mydir, show_index=True) web.run_app(app, host=addr, port=port) diff --git a/devel/mothballer.py b/devel/mothballer.py new file mode 100755 index 0000000..f0799b1 --- /dev/null +++ b/devel/mothballer.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 + +import argparse +import binascii +import hashlib +import io +import json +import logging +import moth +import os +import shutil +import tempfile +import zipfile + +SEEDFN = "SEED" + + +def write_kv_pairs(ziphandle, filename, kv): + """ Write out a sorted map to file + :param ziphandle: a zipfile object + :param filename: The filename to write within the zipfile object + :param kv: the map to write out + :return: + """ + filehandle = io.StringIO() + for key in sorted(kv.keys()): + if isinstance(kv[key], list): + for val in kv[key]: + filehandle.write("%s %s\n" % (key, val)) + else: + filehandle.write("%s %s\n" % (key, kv[key])) + filehandle.seek(0) + ziphandle.writestr(filename, filehandle.read()) + + +def escape(s): + return s.replace('&', '&').replace('<', '<').replace('>', '>') + + +def generate_html(ziphandle, puzzle, puzzledir, category, points, authors, files): + html_content = io.StringIO() + file_content = io.StringIO() + if files: + file_content.write( +'''
+

Associated files:

+ +
+''') + scripts = [''.format(s) for s in puzzle.scripts] + + html_content.write( +''' + + + + + {category} {points} + + {scripts} + + +

{category} for {points} points

+
+{body}
+{file_content}
+
+ + +
Team hash:
+
Answer:
+ +
+
+
Puzzle by {authors}
+ +'''.format( + category=category, + points=points, + body=puzzle.html_body(), + file_content=file_content.getvalue(), + authors=', '.join(authors), + scripts='\n'.join(scripts), + ) + ) + ziphandle.writestr(os.path.join(puzzledir, 'index.html'), html_content.getvalue()) + + +def build_category(categorydir, outdir): + category_seed = binascii.b2a_hex(os.urandom(20)) + + categoryname = os.path.basename(categorydir.strip(os.sep)) + zipfilename = os.path.join(outdir, "%s.mb" % categoryname) + logging.info("Building {} from {}".format(zipfilename, categorydir)) + + if os.path.exists(zipfilename): + # open and gather some state + existing = zipfile.ZipFile(zipfilename, 'r') + try: + category_seed = existing.open(SEEDFN).read().strip() + except Exception: + pass + existing.close() + logging.debug("Using PRNG seed {}".format(category_seed)) + + zipfileraw = tempfile.NamedTemporaryFile(delete=False) + mothball = package(categoryname, categorydir, category_seed) + shutil.copyfileobj(mothball, zipfileraw) + zipfileraw.close() + shutil.move(zipfileraw.name, zipfilename) + + +# Returns a file-like object containing the contents of the new zip file +def package(categoryname, categorydir, seed): + zfraw = io.BytesIO() + zf = zipfile.ZipFile(zfraw, 'x') + zf.writestr("category_seed.txt", seed) + + cat = moth.Category(categorydir, seed) + mapping = {} + answers = {} + summary = {} + for puzzle in cat: + logging.info("Processing point value {}".format(puzzle.points)) + + hashmap = hashlib.sha1(seed.encode('utf-8')) + hashmap.update(str(puzzle.points).encode('utf-8')) + puzzlehash = hashmap.hexdigest() + + mapping[puzzle.points] = puzzlehash + answers[puzzle.points] = puzzle.answers + summary[puzzle.points] = puzzle.summary + + puzzledir = os.path.join('content', puzzlehash) + files = [] + for fn, f in puzzle.files.items(): + if f.visible: + files.append(fn) + payload = f.stream.read() + zf.writestr(os.path.join(puzzledir, fn), payload) + + puzzledict = { + 'authors': puzzle.authors, + 'hashes': puzzle.hashes(), + 'files': files, + 'body': puzzle.html_body(), + } + puzzlejson = json.dumps(puzzledict) + zf.writestr(os.path.join(puzzledir, 'puzzle.json'), puzzlejson) + generate_html(zf, puzzle, puzzledir, categoryname, puzzle.points, puzzle.get_authors(), files) + + write_kv_pairs(zf, 'map.txt', mapping) + write_kv_pairs(zf, 'answers.txt', answers) + write_kv_pairs(zf, 'summaries.txt', summary) + + # clean up + zf.close() + zfraw.seek(0) + return zfraw + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Build a category package') + parser.add_argument('outdir', help='Output directory') + parser.add_argument('categorydirs', nargs='+', help='Directory of category source') + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + + for categorydir in args.categorydirs: + build_category(categorydir, args.outdir)