2016-10-17 23:02:05 -06:00
|
|
|
#!/usr/bin/env python3
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-10-15 21:47:50 -06:00
|
|
|
import glob
|
2016-10-22 10:35:55 -06:00
|
|
|
import html
|
2016-10-14 22:26:47 -06:00
|
|
|
import http.server
|
2016-10-22 10:35:55 -06:00
|
|
|
import io
|
2016-10-14 22:26:47 -06:00
|
|
|
import mistune
|
2016-10-20 11:32:21 -06:00
|
|
|
import moth
|
2016-10-16 19:52:09 -06:00
|
|
|
import os
|
2016-10-14 22:26:47 -06:00
|
|
|
import pathlib
|
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
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
try:
|
|
|
|
from http.server import HTTPStatus
|
|
|
|
except ImportError:
|
2016-10-17 15:37:11 -06:00
|
|
|
class HTTPStatus:
|
2016-10-22 10:35:55 -06:00
|
|
|
OK = (200, 'OK', 'Request fulfilled, document follows')
|
|
|
|
NOT_FOUND = (404, 'Not Found', 'Nothing matches the given URI')
|
|
|
|
INTERNAL_SERVER_ERROR = (500, 'Internal Server Error', 'Server got itself in trouble')
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-11-28 15:17:12 -07:00
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
|
2016-10-17 23:02:05 -06:00
|
|
|
# XXX: This will eventually cause a problem. Do something more clever here.
|
|
|
|
seed = 1
|
|
|
|
|
2016-10-17 20:28:24 -06:00
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
def page(title, body):
|
|
|
|
return """<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
2016-10-24 21:09:04 -06:00
|
|
|
<title>{title}</title>
|
2016-10-20 11:49:43 -06:00
|
|
|
<link rel="stylesheet" href="/files/src/www/res/style.css">
|
2016-10-14 22:26:47 -06:00
|
|
|
</head>
|
|
|
|
<body>
|
2016-10-24 21:09:04 -06:00
|
|
|
<h1>{title}</h1>
|
2016-10-16 20:32:00 -06:00
|
|
|
<div id="preview" class="terminal">
|
2016-10-24 21:09:04 -06:00
|
|
|
{body}
|
2016-10-14 22:26:47 -06:00
|
|
|
</div>
|
|
|
|
</body>
|
2016-10-24 21:09:04 -06:00
|
|
|
</html>""".format(title=title, body=body)
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
def mdpage(body):
|
2016-10-15 21:47:50 -06:00
|
|
|
try:
|
|
|
|
title, _ = body.split('\n', 1)
|
|
|
|
except ValueError:
|
|
|
|
title = "Result"
|
|
|
|
title = title.lstrip("#")
|
|
|
|
title = title.strip()
|
2016-11-28 15:17:12 -07:00
|
|
|
return page(title, mistune.markdown(body, escape=False))
|
2016-10-14 22:26:47 -06:00
|
|
|
|
|
|
|
|
|
|
|
class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
|
|
|
pass
|
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
|
2016-10-18 09:34:06 -06:00
|
|
|
class MothHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
|
def handle_one_request(self):
|
|
|
|
try:
|
|
|
|
super().handle_one_request()
|
|
|
|
except:
|
|
|
|
tbtype, value, tb = sys.exc_info()
|
|
|
|
tblist = traceback.format_tb(tb, None) + traceback.format_exception_only(tbtype, value)
|
2016-10-22 10:35:55 -06:00
|
|
|
payload = ("Traceback (most recent call last)\n" +
|
|
|
|
"".join(tblist[:-1]) +
|
|
|
|
tblist[-1]).encode('utf-8')
|
|
|
|
self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
|
|
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
self.send_header("Content-Length", payload)
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(payload)
|
2016-10-18 09:34:06 -06:00
|
|
|
|
2016-10-14 22:26:47 -06:00
|
|
|
def do_GET(self):
|
|
|
|
if self.path == "/":
|
|
|
|
self.serve_front()
|
2016-10-15 21:47:50 -06:00
|
|
|
elif self.path.startswith("/puzzles"):
|
|
|
|
self.serve_puzzles()
|
|
|
|
elif self.path.startswith("/files"):
|
2016-10-22 10:35:55 -06:00
|
|
|
self.serve_file(self.translate_path(self.path))
|
2016-10-14 22:26:47 -06:00
|
|
|
else:
|
|
|
|
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
|
|
|
|
|
|
|
def translate_path(self, path):
|
|
|
|
if path.startswith('/files'):
|
|
|
|
path = path[7:]
|
|
|
|
return super().translate_path(path)
|
|
|
|
|
|
|
|
def serve_front(self):
|
2016-10-22 10:35:55 -06:00
|
|
|
body = """
|
2016-10-14 22:26:47 -06:00
|
|
|
MOTH Development Server Front Page
|
|
|
|
====================
|
|
|
|
|
|
|
|
Yo, it's the front page.
|
|
|
|
There's stuff you can do here:
|
|
|
|
|
|
|
|
* [Available puzzles](/puzzles)
|
|
|
|
* [Raw filesystem view](/files/)
|
2016-10-21 14:38:19 -06:00
|
|
|
* [Documentation](/files/docs/)
|
|
|
|
* [Instructions](/files/docs/devel-server.md) for using this server
|
2016-10-14 22:26:47 -06:00
|
|
|
|
|
|
|
If you use this development server to run a contest,
|
|
|
|
you are a fool.
|
|
|
|
"""
|
2016-10-22 10:35:55 -06:00
|
|
|
payload = mdpage(body).encode('utf-8')
|
|
|
|
self.send_response(HTTPStatus.OK)
|
|
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
|
|
self.send_header("Content-Length", len(payload))
|
|
|
|
self.end_headers()
|
|
|
|
self.wfile.write(payload)
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-10-15 21:47:50 -06:00
|
|
|
def serve_puzzles(self):
|
2016-10-22 10:35:55 -06:00
|
|
|
body = io.StringIO()
|
2016-10-16 20:32:00 -06:00
|
|
|
path = self.path.rstrip('/')
|
|
|
|
parts = path.split("/")
|
2016-10-22 10:35:55 -06:00
|
|
|
title = None
|
2016-10-23 14:55:35 -06:00
|
|
|
fpath = None
|
|
|
|
points = None
|
2016-10-22 10:35:55 -06:00
|
|
|
cat = None
|
2016-10-23 14:55:35 -06:00
|
|
|
puzzle = None
|
2016-10-22 10:35:55 -06:00
|
|
|
|
|
|
|
try:
|
|
|
|
fpath = os.path.join("puzzles", parts[2])
|
2016-10-23 14:55:35 -06:00
|
|
|
points = int(parts[3])
|
2016-10-22 10:35:55 -06:00
|
|
|
except:
|
|
|
|
pass
|
2016-10-18 20:11:20 -06:00
|
|
|
|
2016-10-23 14:55:35 -06:00
|
|
|
if fpath:
|
|
|
|
cat = moth.Category(fpath, seed)
|
|
|
|
if points:
|
2016-11-17 14:24:01 -07:00
|
|
|
puzzle = cat.puzzle(points)
|
2016-10-23 14:55:35 -06:00
|
|
|
|
2016-10-22 10:35:55 -06:00
|
|
|
if not cat:
|
|
|
|
title = "Puzzle Categories"
|
|
|
|
body.write("<ul>")
|
2016-10-24 21:09:04 -06:00
|
|
|
for i in sorted(glob.glob(os.path.join("puzzles", "*", ""))):
|
2016-10-22 10:35:55 -06:00
|
|
|
body.write('<li><a href="{}">{}</a></li>'.format(i, i))
|
|
|
|
body.write("</ul>")
|
|
|
|
elif not puzzle:
|
2016-10-16 19:52:09 -06:00
|
|
|
# List all point values in a category
|
2016-10-22 10:35:55 -06:00
|
|
|
title = "Puzzles in category `{}`".format(parts[2])
|
|
|
|
body.write("<ul>")
|
2016-10-17 23:02:05 -06:00
|
|
|
for points in cat.pointvals:
|
2016-10-23 14:55:35 -06:00
|
|
|
body.write('<li><a href="/puzzles/{cat}/{points}">puzzles/{cat}/{points}</a></li>'.format(cat=parts[2], points=points))
|
2016-10-22 10:35:55 -06:00
|
|
|
body.write("</ul>")
|
2016-10-23 14:55:35 -06:00
|
|
|
elif len(parts) == 4:
|
2016-10-22 10:35:55 -06:00
|
|
|
# Serve up a puzzle
|
|
|
|
title = "{} puzzle {}".format(parts[2], parts[3])
|
|
|
|
body.write("<h2>Body</h2>")
|
|
|
|
body.write(puzzle.html_body())
|
2016-10-23 14:55:35 -06:00
|
|
|
body.write("<h2>Files</h2>")
|
|
|
|
body.write("<ul>")
|
|
|
|
for name in puzzle.files:
|
2016-10-24 20:51:39 -06:00
|
|
|
body.write('<li><a href="/puzzles/{cat}/{points}/{filename}">{filename}</a></li>'
|
2016-10-23 14:55:35 -06:00
|
|
|
.format(cat=parts[2], points=puzzle.points, filename=name))
|
|
|
|
body.write("</ul>")
|
2016-10-22 10:35:55 -06:00
|
|
|
body.write("<h2>Answers</h2>")
|
|
|
|
body.write("<ul>")
|
2016-10-24 21:09:04 -06:00
|
|
|
assert puzzle.answers, 'No answers defined'
|
2016-10-22 10:35:55 -06:00
|
|
|
for a in puzzle.answers:
|
|
|
|
body.write("<li><code>{}</code></li>".format(html.escape(a)))
|
|
|
|
body.write("</ul>")
|
2016-10-23 14:55:35 -06:00
|
|
|
body.write("<h2>Author</h2><p>{}</p>".format(puzzle.author))
|
|
|
|
body.write("<h2>Summary</h2><p>{}</p>".format(puzzle.summary))
|
2016-11-28 15:17:12 -07:00
|
|
|
if puzzle.logs:
|
|
|
|
body.write("<h2>Debug Log</h2>")
|
|
|
|
body.write('<ul class="log">')
|
|
|
|
for l in puzzle.logs:
|
|
|
|
body.write("<li>{}</li>".format(html.escape(l)))
|
|
|
|
body.write("</ul>")
|
2016-10-18 20:11:20 -06:00
|
|
|
elif len(parts) == 5:
|
2016-10-22 10:35:55 -06:00
|
|
|
# Serve up a puzzle file
|
|
|
|
try:
|
|
|
|
pfile = puzzle.files[parts[4]]
|
|
|
|
except KeyError:
|
|
|
|
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
|
|
|
return
|
|
|
|
ctype = self.guess_type(pfile.name)
|
|
|
|
self.send_response(HTTPStatus.OK)
|
|
|
|
self.send_header("Content-Type", ctype)
|
|
|
|
self.end_headers()
|
|
|
|
shutil.copyfileobj(pfile.stream, self.wfile)
|
|
|
|
return
|
2016-10-18 20:11:20 -06:00
|
|
|
|
2016-10-22 10:35:55 -06:00
|
|
|
payload = page(title, body.getvalue()).encode('utf-8')
|
2016-10-19 15:02:38 -06:00
|
|
|
self.send_response(HTTPStatus.OK)
|
2016-10-22 10:35:55 -06:00
|
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
|
|
self.send_header("Content-Length", len(payload))
|
2016-10-19 15:02:38 -06:00
|
|
|
self.end_headers()
|
2016-10-22 10:35:55 -06:00
|
|
|
self.wfile.write(payload)
|
2016-10-19 15:02:38 -06:00
|
|
|
|
2016-10-22 10:35:55 -06:00
|
|
|
def serve_file(self, path):
|
|
|
|
lastmod = None
|
|
|
|
fspath = pathlib.Path(path)
|
2016-10-18 20:11:20 -06:00
|
|
|
|
2016-10-22 10:35:55 -06:00
|
|
|
if fspath.is_dir():
|
|
|
|
ctype = "text/html; charset=utf-8"
|
|
|
|
payload = self.list_directory(path)
|
|
|
|
# it sends headers but not body
|
|
|
|
shutil.copyfileobj(payload, self.wfile)
|
2016-10-14 22:26:47 -06:00
|
|
|
else:
|
2016-10-22 10:35:55 -06:00
|
|
|
ctype = self.guess_type(path)
|
2016-10-14 22:26:47 -06:00
|
|
|
try:
|
2016-10-22 10:35:55 -06:00
|
|
|
payload = fspath.read_bytes()
|
2016-10-14 22:26:47 -06:00
|
|
|
except OSError:
|
|
|
|
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
2016-10-22 10:35:55 -06:00
|
|
|
return
|
2016-10-23 14:55:35 -06:00
|
|
|
if path.endswith(".md"):
|
2016-10-22 10:35:55 -06:00
|
|
|
ctype = "text/html; charset=utf-8"
|
|
|
|
content = mdpage(payload.decode('utf-8'))
|
|
|
|
payload = content.encode('utf-8')
|
|
|
|
try:
|
|
|
|
fs = fspath.stat()
|
|
|
|
lastmod = self.date_time_string(fs.st_mtime)
|
|
|
|
except:
|
|
|
|
pass
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-10-17 15:37:11 -06:00
|
|
|
self.send_response(HTTPStatus.OK)
|
2016-10-22 10:35:55 -06:00
|
|
|
self.send_header("Content-Type", ctype)
|
|
|
|
self.send_header("Content-Length", len(payload))
|
|
|
|
if lastmod:
|
|
|
|
self.send_header("Last-Modified", lastmod)
|
2016-10-14 22:26:47 -06:00
|
|
|
self.end_headers()
|
2016-10-22 10:35:55 -06:00
|
|
|
self.wfile.write(payload)
|
2016-10-14 22:26:47 -06:00
|
|
|
|
2016-10-17 19:58:51 -06:00
|
|
|
|
2016-10-17 23:20:48 -06:00
|
|
|
def run(address=('localhost', 8080)):
|
2016-10-14 22:26:47 -06:00
|
|
|
httpd = ThreadingServer(address, MothHandler)
|
2016-10-17 23:20:48 -06:00
|
|
|
print("=== Listening on http://{}:{}/".format(address[0], address[1]))
|
2016-10-14 22:26:47 -06:00
|
|
|
httpd.serve_forever()
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
run()
|