moth/devel/devel-server.py

235 lines
6.7 KiB
Python
Raw Normal View History

2017-10-30 11:25:58 -06:00
#!/usr/bin/python3
import asyncio
2018-09-28 12:15:38 -06:00
import cgitb
2016-10-22 10:35:55 -06:00
import html
from aiohttp import web
2016-10-22 10:35:55 -06:00
import io
2018-10-02 19:21:54 -06:00
import json
import mimetypes
import moth
import logging
2016-10-16 19:52:09 -06:00
import os
import pathlib
import random
2016-10-22 10:35:55 -06:00
import shutil
import socketserver
2016-10-18 09:34:06 -06:00
import sys
import traceback
2018-09-28 12:15:38 -06:00
import mothballer
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),
)
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, ""])
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),
)
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")
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)
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),
)
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")
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)
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
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
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="MOTH puzzle development server")
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")
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"
)
args = parser.parse_args()
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)
web.run_app(app, host=addr, port=port)