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)
|
|
|
|
|
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, ""])
|
|
|
|
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)
|
2018-05-11 15:45:40 -06:00
|
|
|
category = request.match_info.get("category")
|
|
|
|
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
|
|
|
|
|
|
|
|
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)
|
2018-05-11 15:45:40 -06:00
|
|
|
category = request.match_info.get("category")
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
|
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)
|
2018-09-28 12:15:38 -06:00
|
|
|
category = request.match_info.get("category")
|
|
|
|
|
|
|
|
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>
|
|
|
|
<head><title>Dev Server</title></head>
|
|
|
|
<body>
|
|
|
|
<h1>Dev Server</h1>
|
|
|
|
<p>
|
2018-10-10 09:26:44 -06:00
|
|
|
You need to provide the contest seed in the URL.
|
|
|
|
If you don't have a contest seed in mind,
|
|
|
|
why not try <a href="{seed}/">{seed}</a>?
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
If you are chaotic,
|
|
|
|
you could even take your chances with a
|
|
|
|
<a href="random/">random seed</a> for every HTTP request.
|
|
|
|
This means generated files will get a different seed than the puzzle itself!
|
|
|
|
</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:
|
2018-10-09 16:05:02 -06:00
|
|
|
for fn in ("puzzle-list.html", "index.html"):
|
|
|
|
path = themes.joinpath(fn)
|
|
|
|
if path.exists():
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
path = themes.joinpath(fn)
|
|
|
|
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)
|
|
|
|
app.router.add_route("GET", "/{seed}/puzzles.json", handle_puzzlelist)
|
|
|
|
app.router.add_route("GET", "/{seed}/content/{category}/{points}/puzzle.json", handle_puzzle)
|
|
|
|
app.router.add_route("GET", "/{seed}/content/{category}/{points}/{filename}", handle_puzzlefile)
|
|
|
|
app.router.add_route("GET", "/{seed}/mothballer/{category}", handle_mothballer)
|
|
|
|
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)
|