Merge branch 'v3.4_devel' of https://github.com/dirtbags/moth into 81-support-downloading-open-content

This commit is contained in:
John Donaldson 2019-11-08 19:28:37 +00:00
commit dce4e7ea32
5 changed files with 41 additions and 185 deletions

9
CHANGELOG.md Normal file
View File

@ -0,0 +1,9 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- A changelog

View File

@ -14,6 +14,7 @@ import os
import pathlib
import random
import shutil
import socketserver
import sys
import traceback
import mothballer
@ -25,14 +26,8 @@ from http import HTTPStatus
sys.dont_write_bytecode = True # Don't write .pyc files
try:
ThreadingHTTPServer = http.server.ThreadingHTTPServer
except AttributeError:
import socketserver
class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
class MothServer(ThreadingHTTPServer):
class MothServer(socketserver.ForkingMixIn, http.server.HTTPServer):
def __init__(self, server_address, RequestHandlerClass):
super().__init__(server_address, RequestHandlerClass)
self.args = {}
@ -270,12 +265,24 @@ if __name__ == '__main__':
'--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(":")
addr = parts[0] or "0.0.0.0"
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=logging.INFO)
logging.basicConfig(level=log_level)
server = MothServer((addr, port), MothRequestHandler)
server.args["base_url"] = args.base

View File

@ -8,6 +8,7 @@ import hashlib
import html
import io
import importlib.machinery
import logging
import mistune
import os
import random
@ -19,6 +20,8 @@ import yaml
messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
LOGGER = logging.getLogger(__name__)
def djb2hash(str):
h = 5381
for c in str.encode("utf-8"):
@ -28,6 +31,7 @@ def djb2hash(str):
@contextlib.contextmanager
def pushd(newdir):
curdir = os.getcwd()
LOGGER.debug("Attempting to chdir from %s to %s" % (curdir, newdir))
os.chdir(newdir)
# Force a copy of the old path, instead of just a reference
@ -48,6 +52,7 @@ def pushd(newdir):
del(sys.modules[module])
sys.path = old_path
LOGGER.debug("Changing directory back from %s to %s" % (newdir, curdir))
os.chdir(curdir)
@ -154,8 +159,10 @@ class Puzzle:
stream.seek(0)
if header == "yaml":
LOGGER.info("Puzzle is YAML-formatted")
self.read_yaml_header(stream)
elif header == "moth":
LOGGER.info("Puzzle is MOTH-formatted")
self.read_moth_header(stream)
for line in stream:
@ -190,6 +197,7 @@ class Puzzle:
self.handle_header_key(key, val)
def handle_header_key(self, key, val):
LOGGER.debug("Handling key: %s, value: %s", key, val)
if key == 'author':
self.authors.append(val)
elif key == 'summary':
@ -213,6 +221,7 @@ class Puzzle:
parts = shlex.split(val)
name = parts[0]
hidden = False
LOGGER.debug("Attempting to open %s", os.path.abspath(name))
stream = open(name, 'rb')
try:
name = parts[1]

View File

@ -1,169 +0,0 @@
#!/usr/bin/env python3
import argparse
import binascii
import glob
import hashlib
import io
import json
import logging
import moth
import os
import shutil
import string
import sys
import tempfile
import zipfile
def write_kv_pairs(ziphandle, filename, kv):
""" Write out a sorted map to file
:param ziphandle: a zipfile object
:param filename: The filename to write within the zipfile object
:param kv: the map to write out
:return:
"""
filehandle = io.StringIO()
for key in sorted(kv.keys()):
if type(kv[key]) == type([]):
for val in kv[key]:
filehandle.write("%s %s\n" % (key, val))
else:
filehandle.write("%s %s\n" % (key, kv[key]))
filehandle.seek(0)
ziphandle.writestr(filename, filehandle.read())
def escape(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def generate_html(ziphandle, puzzle, puzzledir, category, points, authors, files):
html_content = io.StringIO()
file_content = io.StringIO()
if files:
file_content.write(
''' <section id="files">
<h2>Associated files:</h2>
<ul>
''')
for fn in files:
file_content.write(' <li><a href="{fn}">{efn}</a></li>\n'.format(fn=fn, efn=escape(fn)))
file_content.write(
''' </ul>
</section>
''')
scripts = ['<script src="{}"></script>'.format(s) for s in puzzle.scripts]
html_content.write(
'''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>{category} {points}</title>
<link rel="stylesheet" href="../../style.css">
{scripts}
</head>
<body>
<h1>{category} for {points} points</h1>
<section id="readme">
{body} </section>
{file_content} <section id="form">
<form id="puzzler" action="../../cgi-bin/puzzler.cgi" method="get" accept-charset="utf-8" autocomplete="off">
<input type="hidden" name="c" value="{category}">
<input type="hidden" name="p" value="{points}">
<div>Team hash:<input name="t" size="8"></div>
<div>Answer:<input name="a" id="answer" size="20"></div>
<input type="submit" value="submit">
</form>
</section>
<address>Puzzle by <span class="authors" data-handle="{authors}">{authors}</span></address>
</body>
</html>'''.format(
category=category,
points=points,
body=puzzle.html_body(),
file_content=file_content.getvalue(),
authors=', '.join(authors),
scripts='\n'.join(scripts),
)
)
ziphandle.writestr(os.path.join(puzzledir, 'index.html'), html_content.getvalue())
def build_category(categorydir, outdir):
zipfileraw = tempfile.NamedTemporaryFile(delete=False)
zf = zipfile.ZipFile(zipfileraw, 'x')
category_seed = binascii.b2a_hex(os.urandom(20))
puzzles_dict = {}
secrets = {}
categoryname = os.path.basename(categorydir.strip(os.sep))
seedfn = os.path.join("category_seed.txt")
zipfilename = os.path.join(outdir, "%s.mb" % categoryname)
logging.info("Building {} from {}".format(zipfilename, categorydir))
if os.path.exists(zipfilename):
# open and gather some state
existing = zipfile.ZipFile(zipfilename, 'r')
try:
category_seed = existing.open(seedfn).read().strip()
except:
pass
existing.close()
logging.debug("Using PRNG seed {}".format(category_seed))
zf.writestr(seedfn, category_seed)
cat = moth.Category(categorydir, category_seed)
mapping = {}
answers = {}
summary = {}
for puzzle in cat:
logging.info("Processing point value {}".format(puzzle.points))
hashmap = hashlib.sha1(category_seed)
hashmap.update(str(puzzle.points).encode('utf-8'))
puzzlehash = hashmap.hexdigest()
mapping[puzzle.points] = puzzlehash
answers[puzzle.points] = puzzle.answers
summary[puzzle.points] = puzzle.summary
puzzledir = os.path.join('content', puzzlehash)
files = []
for fn, f in puzzle.files.items():
if f.visible:
files.append(fn)
payload = f.stream.read()
zf.writestr(os.path.join(puzzledir, fn), payload)
puzzledict = {
'authors': puzzle.authors,
'hashes': puzzle.hashes(),
'files': files,
'body': puzzle.html_body(),
}
puzzlejson = json.dumps(puzzledict)
zf.writestr(os.path.join(puzzledir, 'puzzle.json'), puzzlejson)
generate_html(zf, puzzle, puzzledir, categoryname, puzzle.points, puzzle.get_authors(), files)
write_kv_pairs(zf, 'map.txt', mapping)
write_kv_pairs(zf, 'answers.txt', answers)
write_kv_pairs(zf, 'summaries.txt', summary)
# clean up
zf.close()
shutil.move(zipfileraw.name, zipfilename)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Build a category package')
parser.add_argument('outdir', help='Output directory')
parser.add_argument('categorydirs', nargs='+', help='Directory of category source')
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG)
for categorydir in args.categorydirs:
build_category(categorydir, args.outdir)

File diff suppressed because one or more lines are too long