#!/usr/bin/env python3 import glob import html import http.server import io import mistune import moth import os import pathlib import shutil import socketserver import sys import traceback try: from http.server import HTTPStatus except ImportError: class HTTPStatus: 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') # XXX: This will eventually cause a problem. Do something more clever here. seed = 1 def page(title, body): return """ {title}

{title}

{body}
""".format(title=title, body=body) def mdpage(body): try: title, _ = body.split('\n', 1) except ValueError: title = "Result" title = title.lstrip("#") title = title.strip() return page(title, mistune.markdown(body)) class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): pass 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) 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) def do_GET(self): if self.path == "/": self.serve_front() elif self.path.startswith("/puzzles"): self.serve_puzzles() elif self.path.startswith("/files"): self.serve_file(self.translate_path(self.path)) 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): body = """ 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/) * [Documentation](/files/docs/) * [Instructions](/files/docs/devel-server.md) for using this server If you use this development server to run a contest, you are a fool. """ 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) def serve_puzzles(self): body = io.StringIO() path = self.path.rstrip('/') parts = path.split("/") title = None fpath = None points = None cat = None puzzle = None try: fpath = os.path.join("puzzles", parts[2]) points = int(parts[3]) except: pass if fpath: cat = moth.Category(fpath, seed) if points: puzzle = cat.puzzle(int(parts[3])) if not cat: title = "Puzzle Categories" body.write("") elif not puzzle: # List all point values in a category title = "Puzzles in category `{}`".format(parts[2]) body.write("") elif len(parts) == 4: # Serve up a puzzle title = "{} puzzle {}".format(parts[2], parts[3]) body.write("

Body

") body.write(puzzle.html_body()) body.write("

Files

") body.write("") body.write("

Answers

") body.write("") body.write("

Author

{}

".format(puzzle.author)) body.write("

Summary

{}

".format(puzzle.summary)) body.write("

Debug Log

") body.write('") elif len(parts) == 5: # 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 payload = page(title, body.getvalue()).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) def serve_file(self, path): lastmod = None fspath = pathlib.Path(path) 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) else: ctype = self.guess_type(path) try: payload = fspath.read_bytes() except OSError: self.send_error(HTTPStatus.NOT_FOUND, "File not found") return if path.endswith(".md"): 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 self.send_response(HTTPStatus.OK) self.send_header("Content-Type", ctype) self.send_header("Content-Length", len(payload)) if lastmod: self.send_header("Last-Modified", lastmod) self.end_headers() self.wfile.write(payload) def run(address=('localhost', 8080)): httpd = ThreadingServer(address, MothHandler) print("=== Listening on http://{}:{}/".format(address[0], address[1])) httpd.serve_forever() if __name__ == '__main__': run()