2020-03-01 12:44:21 -07:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
|
|
|
import cgitb
|
|
|
|
import html
|
|
|
|
import cgi
|
|
|
|
import http.server
|
|
|
|
import io
|
|
|
|
import json
|
|
|
|
import mimetypes
|
|
|
|
import moth
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import pathlib
|
|
|
|
import random
|
|
|
|
import shutil
|
|
|
|
import socketserver
|
|
|
|
import sys
|
|
|
|
import traceback
|
|
|
|
import mothballer
|
|
|
|
import parse
|
|
|
|
import urllib.parse
|
|
|
|
import posixpath
|
|
|
|
from http import HTTPStatus
|
|
|
|
|
|
|
|
|
|
|
|
sys.dont_write_bytecode = True # Don't write .pyc files
|
|
|
|
|
|
|
|
|
|
|
|
class MothServer(socketserver.ForkingMixIn, http.server.HTTPServer):
|
|
|
|
def __init__(self, server_address, RequestHandlerClass):
|
|
|
|
super().__init__(server_address, RequestHandlerClass)
|
|
|
|
self.args = {}
|
|
|
|
|
|
|
|
|
|
|
|
class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
|
endpoints = []
|
|
|
|
|
|
|
|
def __init__(self, request, client_address, server):
|
|
|
|
self.directory = str(server.args["theme_dir"])
|
|
|
|
try:
|
|
|
|
super().__init__(request, client_address, server, directory=server.args["theme_dir"])
|
|
|
|
except TypeError:
|
|
|
|
super().__init__(request, client_address, server)
|
|
|
|
|
2020-01-21 08:26:22 -07:00
|
|
|
# Why isn't this the default?!
|
|
|
|
def guess_type(self, path):
|
|
|
|
mtype, encoding = mimetypes.guess_type(path)
|
|
|
|
if encoding:
|
|
|
|
return "%s; encoding=%s" % (mtype, encoding)
|
|
|
|
else:
|
|
|
|
return mtype
|
2020-03-01 12:44:21 -07:00
|
|
|
|
|
|
|
# Backport from Python 3.7
|
|
|
|
def translate_path(self, path):
|
|
|
|
# I guess we just hope that some other thread doesn't call getcwd
|
|
|
|
getcwd = os.getcwd
|
|
|
|
os.getcwd = lambda: self.directory
|
|
|
|
ret = super().translate_path(path)
|
|
|
|
os.getcwd = getcwd
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
|
|
def get_puzzle(self):
|
|
|
|
category = self.req.get("cat")
|
|
|
|
points = int(self.req.get("points"))
|
|
|
|
catpath = str(self.server.args["puzzles_dir"].joinpath(category))
|
|
|
|
cat = moth.Category(catpath, self.seed)
|
|
|
|
puzzle = cat.puzzle(points)
|
|
|
|
return puzzle
|
|
|
|
|
|
|
|
|
|
|
|
def send_json(self, obj):
|
|
|
|
self.send_response(200)
|
|
|
|
self.send_header("Content-Type", "application/json")
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(json.dumps(obj).encode("utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_register(self):
|
|
|
|
# Everybody eats when they come to my house
|
|
|
|
ret = {
|
|
|
|
"status": "success",
|
|
|
|
"data": {
|
|
|
|
"short": "You win",
|
|
|
|
"description": "Welcome to the development server, you wily hacker you"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.send_json(ret)
|
|
|
|
endpoints.append(('/{seed}/register', handle_register))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_answer(self):
|
|
|
|
for f in ("cat", "points", "answer"):
|
|
|
|
self.req[f] = self.fields.getfirst(f)
|
|
|
|
puzzle = self.get_puzzle()
|
|
|
|
ret = {
|
|
|
|
"status": "success",
|
|
|
|
"data": {
|
|
|
|
"short": "",
|
2020-03-10 14:28:50 -06:00
|
|
|
"description": "%r was not in list of answers" % self.req.get("answer")
|
2020-03-01 12:44:21 -07:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.req.get("answer") in puzzle.answers:
|
|
|
|
ret["data"]["description"] = "Answer is correct"
|
|
|
|
self.send_json(ret)
|
|
|
|
endpoints.append(('/{seed}/answer', handle_answer))
|
|
|
|
|
|
|
|
|
|
|
|
def puzzlelist(self):
|
|
|
|
puzzles = {}
|
|
|
|
for p in self.server.args["puzzles_dir"].glob("*"):
|
|
|
|
if not p.is_dir() or p.match(".*"):
|
|
|
|
continue
|
|
|
|
catName = p.parts[-1]
|
|
|
|
cat = moth.Category(str(p), self.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(self.server.args["puzzles_dir"]))
|
|
|
|
|
|
|
|
return puzzles
|
|
|
|
|
|
|
|
|
|
|
|
# XXX: Remove this (redundant) when we've upgraded the bundled theme (probably v3.6 and beyond)
|
|
|
|
def handle_puzzlelist(self):
|
|
|
|
self.send_json(self.puzzlelist())
|
|
|
|
endpoints.append(('/{seed}/puzzles.json', handle_puzzlelist))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_state(self):
|
|
|
|
resp = {
|
|
|
|
"Config": {
|
|
|
|
"Devel": True,
|
|
|
|
},
|
|
|
|
"Puzzles": self.puzzlelist(),
|
|
|
|
"Messages": "<p><b>[MOTH Development Server]</b> Participant broadcast messages would go here.</p>",
|
|
|
|
}
|
|
|
|
self.send_json(resp)
|
|
|
|
endpoints.append(('/{seed}/state', handle_state))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_puzzle(self):
|
|
|
|
puzzle = self.get_puzzle()
|
|
|
|
|
|
|
|
obj = puzzle.package()
|
|
|
|
obj["answers"] = puzzle.answers
|
|
|
|
obj["hint"] = puzzle.hint
|
|
|
|
obj["summary"] = puzzle.summary
|
|
|
|
obj["logs"] = puzzle.logs
|
2020-01-29 16:55:10 -07:00
|
|
|
obj["format"] = puzzle._source_format
|
2020-03-01 12:44:21 -07:00
|
|
|
|
|
|
|
self.send_json(obj)
|
|
|
|
endpoints.append(('/{seed}/content/{cat}/{points}/puzzle.json', handle_puzzle))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_puzzlefile(self):
|
|
|
|
puzzle = self.get_puzzle()
|
|
|
|
|
|
|
|
try:
|
|
|
|
file = puzzle.files[self.req["filename"]]
|
|
|
|
except KeyError:
|
|
|
|
self.send_error(
|
|
|
|
HTTPStatus.NOT_FOUND,
|
|
|
|
"File Not Found: %s" % self.req["filename"],
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
self.send_response(200)
|
|
|
|
self.send_header("Content-Type", mimetypes.guess_type(file.name))
|
|
|
|
self.end_headers()
|
|
|
|
shutil.copyfileobj(file.stream, self.wfile)
|
|
|
|
endpoints.append(("/{seed}/content/{cat}/{points}/{filename}", handle_puzzlefile))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_mothballer(self):
|
|
|
|
category = self.req.get("cat")
|
|
|
|
|
|
|
|
try:
|
|
|
|
catdir = self.server.args["puzzles_dir"].joinpath(category)
|
|
|
|
mb = mothballer.package(category, str(catdir), self.seed)
|
|
|
|
except Exception as ex:
|
|
|
|
logging.exception(ex)
|
|
|
|
self.send_response(500)
|
|
|
|
self.send_header("Content-Type", "text/html; charset=\"utf-8\"")
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(cgitb.html(sys.exc_info()).encode("utf-8"))
|
|
|
|
return
|
|
|
|
|
|
|
|
self.send_response(200)
|
|
|
|
self.send_header("Content-Type", "application/octet_stream")
|
|
|
|
self.end_headers()
|
|
|
|
shutil.copyfileobj(mb, self.wfile)
|
|
|
|
endpoints.append(("/{seed}/mothballer/{cat}.mb", handle_mothballer))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_index(self):
|
|
|
|
seed = random.getrandbits(32)
|
|
|
|
self.send_response(307)
|
|
|
|
self.send_header("Location", "%s/" % seed)
|
|
|
|
self.send_header("Content-Type", "text/html")
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(b"Your browser was supposed to redirect you to <a href=\"%i/\">here</a>." % seed)
|
|
|
|
endpoints.append((r"/", handle_index))
|
|
|
|
|
|
|
|
|
|
|
|
def handle_theme_file(self):
|
|
|
|
self.path = "/" + self.req.get("path", "")
|
|
|
|
super().do_GET()
|
|
|
|
endpoints.append(("/{seed}/", handle_theme_file))
|
|
|
|
endpoints.append(("/{seed}/{path}", handle_theme_file))
|
|
|
|
|
|
|
|
|
|
|
|
def do_GET(self):
|
|
|
|
self.fields = cgi.FieldStorage(
|
|
|
|
fp=self.rfile,
|
|
|
|
headers=self.headers,
|
|
|
|
environ={
|
|
|
|
"REQUEST_METHOD": self.command,
|
|
|
|
"CONTENT_TYPE": self.headers["Content-Type"],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
url = urllib.parse.urlparse(self.path)
|
|
|
|
for pattern, function in self.endpoints:
|
|
|
|
result = parse.parse(pattern, url.path)
|
|
|
|
if result:
|
|
|
|
self.req = result.named
|
|
|
|
seed = self.req.get("seed", "random")
|
|
|
|
if seed == "random":
|
|
|
|
self.seed = random.getrandbits(32)
|
|
|
|
else:
|
|
|
|
self.seed = int(seed)
|
|
|
|
return function(self)
|
|
|
|
super().do_GET()
|
|
|
|
|
|
|
|
def do_POST(self):
|
|
|
|
self.do_GET()
|
|
|
|
|
|
|
|
def do_HEAD(self):
|
|
|
|
self.send_error(
|
|
|
|
HTTPStatus.NOT_IMPLEMENTED,
|
|
|
|
"Unsupported method (%r)" % self.command,
|
|
|
|
)
|
|
|
|
|
|
|
|
# I don't fully understand why you can't do this inside the class definition.
|
|
|
|
MothRequestHandler.extensions_map[".mjs"] = "application/ecmascript"
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="MOTH puzzle development server")
|
|
|
|
parser.add_argument(
|
|
|
|
'--puzzles', default='puzzles',
|
|
|
|
help="Directory containing your puzzles"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--theme', default='theme',
|
|
|
|
help="Directory containing theme files")
|
|
|
|
parser.add_argument(
|
2020-07-24 13:42:28 -06:00
|
|
|
'--bind', default=":8080",
|
2020-03-01 12:44:21 -07:00
|
|
|
help="Bind to ip:port"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--base', default="",
|
|
|
|
help="Base URL to this server, for reverse proxy setup"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"-v", "--verbose",
|
|
|
|
action="count",
|
|
|
|
default=1, # Leave at 1, for now, to maintain current default behavior
|
|
|
|
help="Include more verbose logging. Use multiple flags to increase level",
|
|
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
parts = args.bind.split(":")
|
2020-07-24 13:42:28 -06:00
|
|
|
addr = parts[0]
|
2020-03-01 12:44:21 -07:00
|
|
|
port = int(parts[1])
|
|
|
|
if args.verbose >= 2:
|
|
|
|
log_level = logging.DEBUG
|
|
|
|
elif args.verbose == 1:
|
|
|
|
log_level = logging.INFO
|
|
|
|
else:
|
|
|
|
log_level = logging.WARNING
|
|
|
|
|
|
|
|
logging.basicConfig(level=log_level)
|
|
|
|
|
2020-01-21 08:26:22 -07:00
|
|
|
mimetypes.add_type("application/javascript", ".mjs")
|
|
|
|
|
2020-03-01 12:44:21 -07:00
|
|
|
server = MothServer((addr, port), MothRequestHandler)
|
|
|
|
server.args["base_url"] = args.base
|
|
|
|
server.args["puzzles_dir"] = pathlib.Path(args.puzzles)
|
|
|
|
server.args["theme_dir"] = args.theme
|
|
|
|
|
|
|
|
logging.info("Listening on %s:%d", addr, port)
|
|
|
|
server.serve_forever()
|