2017-10-30 11:25:58 -06:00
|
|
|
#!/usr/bin/python3
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2018-05-11 15:45:40 -06:00
|
|
|
import asyncio
|
2018-09-28 12:15:38 -06:00
|
|
|
import cgitb
|
2016-10-22 10:35:55 -06:00
|
|
|
import html
|
2018-05-11 15:45:40 -06:00
|
|
|
from aiohttp import web
|
2016-10-22 10:35:55 -06:00
|
|
|
import io
|
2018-10-02 19:21:54 -06:00
|
|
|
import json
|
2018-05-11 15:45:40 -06:00
|
|
|
import mimetypes
|
2016-10-20 11:32:21 -06:00
|
|
|
import moth
|
2018-05-11 15:45:40 -06:00
|
|
|
import logging
|
2016-10-16 19:52:09 -06:00
|
|
|
import os
|
2016-10-14 22:26:47 -06:00
|
|
|
import pathlib
|
2018-05-11 15:45:40 -06:00
|
|
|
import random
|
2016-10-22 10:35:55 -06:00
|
|
|
import shutil
|
2016-10-14 22:26:47 -06:00
|
|
|
import socketserver
|
2016-10-18 09:34:06 -06:00
|
|
|
import sys
|
|
|
|
import traceback
|
2018-09-28 12:15:38 -06:00
|
|
|
import mothballer
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2018-05-11 15:45:40 -06:00
|
|
|
sys.dont_write_bytecode = True # Don't write .pyc files
|
2018-10-10 09:26:44 -06:00
|
|
|
|
|
|
|
|
|
|
|
def get_seed(request):
|
|
|
|
seedstr = request.match_info.get("seed")
|
|
|
|
if seedstr == "random":
|
|
|
|
return random.getrandbits(32)
|
|
|
|
else:
|
|
|
|
return int(seedstr)
|
|
|
|
|
2019-02-22 19:09:38 -07:00
|
|
|
|
2019-02-23 12:04:42 -07:00
|
|
|
def get_puzzle(request, data=None):
|
2019-02-22 19:09:38 -07:00
|
|
|
seed = get_seed(request)
|
2019-02-23 12:04:42 -07:00
|
|
|
if not data:
|
|
|
|
data = request.match_info
|
|
|
|
category = data.get("cat")
|
|
|
|
points = int(data.get("points"))
|
|
|
|
filename = data.get("filename")
|
2019-02-22 19:09:38 -07:00
|
|
|
cat = moth.Category(request.app["puzzles_dir"].joinpath(category), seed)
|
|
|
|
puzzle = cat.puzzle(points)
|
|
|
|
return puzzle
|
|
|
|
|
2019-02-23 12:04:42 -07:00
|
|
|
async def handle_answer(request):
|
|
|
|
data = await request.post()
|
|
|
|
puzzle = get_puzzle(request, data)
|
|
|
|
ret = {
|
|
|
|
"status": "success",
|
|
|
|
"data": {
|
|
|
|
"short": "",
|
|
|
|
"description": "Provided answer was not in list of answers"
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if data.get("answer") in puzzle.answers:
|
|
|
|
ret["data"]["description"] = "Answer is correct"
|
|
|
|
return web.Response(
|
|
|
|
content_type="application/json",
|
|
|
|
body=json.dumps(ret),
|
|
|
|
)
|
|
|
|
|
2018-05-11 15:45:40 -06:00
|
|
|
|
|
|
|
async def handle_puzzlelist(request):
|
2018-10-10 09:26:44 -06:00
|
|
|
seed = get_seed(request)
|
2018-10-02 19:21:54 -06:00
|
|
|
puzzles = {
|
2018-10-09 16:05:02 -06:00
|
|
|
"__devel__": [[0, ""]],
|
2018-10-02 19:21:54 -06:00
|
|
|
}
|
|
|
|
for p in request.app["puzzles_dir"].glob("*"):
|
|
|
|
if not p.is_dir() or p.match(".*"):
|
|
|
|
continue
|
|
|
|
catName = p.parts[-1]
|
|
|
|
cat = moth.Category(p, seed)
|
|
|
|
puzzles[catName] = [[i, str(i)] for i in cat.pointvals()]
|
|
|
|
puzzles[catName].append([0, ""])
|
2018-10-10 18:03:52 -06:00
|
|
|
if len(puzzles) <= 1:
|
|
|
|
logging.warning("No directories found matching {}/*".format(request.app["puzzles_dir"]))
|
2018-10-02 19:21:54 -06:00
|
|
|
return web.Response(
|
|
|
|
content_type="application/json",
|
|
|
|
body=json.dumps(puzzles),
|
|
|
|
)
|
|
|
|
|
2018-05-11 15:45:40 -06:00
|
|
|
|
|
|
|
async def handle_puzzle(request):
|
2018-10-10 09:26:44 -06:00
|
|
|
seed = get_seed(request)
|
2019-02-23 12:04:42 -07:00
|
|
|
category = request.match_info.get("cat")
|
2018-05-11 15:45:40 -06:00
|
|
|
points = int(request.match_info.get("points"))
|
2018-10-02 19:21:54 -06:00
|
|
|
cat = moth.Category(request.app["puzzles_dir"].joinpath(category), seed)
|
2018-05-11 15:45:40 -06:00
|
|
|
puzzle = cat.puzzle(points)
|
2018-10-02 19:21:54 -06:00
|
|
|
|
|
|
|
obj = puzzle.package()
|
|
|
|
obj["answers"] = puzzle.answers
|
|
|
|
obj["hint"] = puzzle.hint
|
|
|
|
obj["summary"] = puzzle.summary
|
2018-10-16 08:52:29 -06:00
|
|
|
obj["logs"] = puzzle.logs
|
2018-10-02 19:21:54 -06:00
|
|
|
|
|
|
|
return web.Response(
|
|
|
|
content_type="application/json",
|
|
|
|
body=json.dumps(obj),
|
|
|
|
)
|
|
|
|
|
2018-05-11 15:45:40 -06:00
|
|
|
|
|
|
|
async def handle_puzzlefile(request):
|
2018-10-10 09:26:44 -06:00
|
|
|
seed = get_seed(request)
|
2019-02-23 12:04:42 -07:00
|
|
|
category = request.match_info.get("cat")
|
2018-05-11 15:45:40 -06:00
|
|
|
points = int(request.match_info.get("points"))
|
|
|
|
filename = request.match_info.get("filename")
|
2018-10-02 19:21:54 -06:00
|
|
|
cat = moth.Category(request.app["puzzles_dir"].joinpath(category), seed)
|
2018-05-11 15:45:40 -06:00
|
|
|
puzzle = cat.puzzle(points)
|
|
|
|
|
|
|
|
try:
|
|
|
|
file = puzzle.files[filename]
|
|
|
|
except KeyError:
|
|
|
|
return web.Response(status=404)
|
2018-10-02 19:21:54 -06:00
|
|
|
|
|
|
|
content_type, _ = mimetypes.guess_type(file.name)
|
|
|
|
return web.Response(
|
|
|
|
body=file.stream.read(), # Is there no way to pipe this, must we slurp the whole thing into memory?
|
|
|
|
content_type=content_type,
|
|
|
|
)
|
|
|
|
|
2018-09-28 12:15:38 -06:00
|
|
|
async def handle_mothballer(request):
|
2018-10-10 09:26:44 -06:00
|
|
|
seed = get_seed(request)
|
2019-02-23 12:04:42 -07:00
|
|
|
category = request.match_info.get("cat")
|
2018-09-28 12:15:38 -06:00
|
|
|
|
|
|
|
try:
|
2018-10-02 19:21:54 -06:00
|
|
|
catdir = request.app["puzzles_dir"].joinpath(category)
|
2018-09-28 12:15:38 -06:00
|
|
|
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
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2018-10-02 19:21:54 -06:00
|
|
|
|
|
|
|
async def handle_index(request):
|
|
|
|
seed = random.getrandbits(32)
|
|
|
|
body = """<!DOCTYPE html>
|
|
|
|
<html>
|
2019-02-22 19:09:38 -07:00
|
|
|
<head>
|
|
|
|
<title>Dev Server</title>
|
|
|
|
<script>
|
|
|
|
// Skip trying to log in
|
2019-02-23 12:04:42 -07:00
|
|
|
sessionStorage.setItem("id", "devel-server")
|
2019-02-22 19:09:38 -07:00
|
|
|
</script>
|
|
|
|
</head>
|
2018-10-02 19:21:54 -06:00
|
|
|
<body>
|
|
|
|
<h1>Dev Server</h1>
|
2019-02-22 19:09:38 -07:00
|
|
|
|
2018-10-02 19:21:54 -06:00
|
|
|
<p>
|
2019-02-22 19:09:38 -07:00
|
|
|
Pick a seed:
|
2018-10-10 09:26:44 -06:00
|
|
|
</p>
|
2019-02-22 19:09:38 -07:00
|
|
|
<ul>
|
|
|
|
<li><a href="{seed}/">{seed}</a>: a special seed I made just for you!</li>
|
|
|
|
<li><a href="random/">random</a>: will use a different seed every time you load a page (could be useful for debugging)</li>
|
|
|
|
<li>You can also hack your own seed into the URL, if you want to.</li>
|
|
|
|
</ul>
|
|
|
|
|
2018-10-10 09:26:44 -06:00
|
|
|
<p>
|
2019-02-22 19:09:38 -07:00
|
|
|
Puzzles can be generated from Python code: these puzzles can use a random number generator if they want.
|
|
|
|
The seed is used to create these random numbers.
|
|
|
|
</p>
|
|
|
|
|
|
|
|
<p>
|
|
|
|
We like to make a new seed for every contest,
|
|
|
|
and re-use that seed whenever we regenerate a category during an event
|
|
|
|
(say to fix a bug).
|
|
|
|
By using the same seed,
|
|
|
|
we make sure that all the dynamically-generated puzzles have the same values
|
|
|
|
in any new packages we build.
|
2018-10-10 09:26:44 -06:00
|
|
|
</p>
|
2018-10-02 19:21:54 -06:00
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
""".format(seed=seed)
|
|
|
|
return web.Response(
|
|
|
|
content_type="text/html",
|
|
|
|
body=body,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def handle_static(request):
|
2018-10-09 16:05:02 -06:00
|
|
|
themes = request.app["theme_dir"]
|
2018-10-02 19:21:54 -06:00
|
|
|
fn = request.match_info.get("filename")
|
|
|
|
if not fn:
|
2019-02-22 19:09:38 -07:00
|
|
|
fn = "index.html"
|
|
|
|
path = themes.joinpath(fn)
|
2018-10-09 16:05:02 -06:00
|
|
|
return web.FileResponse(path)
|
2018-10-02 19:21:54 -06:00
|
|
|
|
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
if __name__ == '__main__':
|
2016-12-01 16:20:04 -07:00
|
|
|
import argparse
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="MOTH puzzle development server")
|
2017-11-09 14:47:25 -07:00
|
|
|
parser.add_argument(
|
|
|
|
'--puzzles', default='puzzles',
|
|
|
|
help="Directory containing your puzzles"
|
|
|
|
)
|
2018-10-02 19:21:54 -06:00
|
|
|
parser.add_argument(
|
|
|
|
'--theme', default='theme',
|
|
|
|
help="Directory containing theme files")
|
2017-11-09 14:47:25 -07:00
|
|
|
parser.add_argument(
|
|
|
|
'--bind', default="127.0.0.1:8080",
|
|
|
|
help="Bind to ip:port"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--base', default="",
|
|
|
|
help="Base URL to this server, for reverse proxy setup"
|
|
|
|
)
|
2016-12-01 16:20:04 -07:00
|
|
|
args = parser.parse_args()
|
2018-05-11 15:45:40 -06:00
|
|
|
parts = args.bind.split(":")
|
|
|
|
addr = parts[0] or "0.0.0.0"
|
|
|
|
port = int(parts[1])
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
|
|
mydir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))
|
|
|
|
|
|
|
|
app = web.Application()
|
|
|
|
app["base_url"] = args.base
|
2018-10-02 19:21:54 -06:00
|
|
|
app["puzzles_dir"] = pathlib.Path(args.puzzles)
|
|
|
|
app["theme_dir"] = pathlib.Path(args.theme)
|
|
|
|
app.router.add_route("GET", "/", handle_index)
|
2019-02-23 12:04:42 -07:00
|
|
|
app.router.add_route("*", "/{seed}/answer", handle_answer)
|
2019-02-22 19:09:38 -07:00
|
|
|
app.router.add_route("*", "/{seed}/puzzles.json", handle_puzzlelist)
|
2019-02-23 12:04:42 -07:00
|
|
|
app.router.add_route("GET", "/{seed}/content/{cat}/{points}/puzzle.json", handle_puzzle)
|
|
|
|
app.router.add_route("GET", "/{seed}/content/{cat}/{points}/{filename}", handle_puzzlefile)
|
|
|
|
app.router.add_route("GET", "/{seed}/mothballer/{cat}", handle_mothballer)
|
2018-10-02 19:21:54 -06:00
|
|
|
app.router.add_route("GET", "/{seed}/{filename:.*}", handle_static)
|
2018-05-11 15:45:40 -06:00
|
|
|
web.run_app(app, host=addr, port=port)
|