This commit is contained in:
Neale Pickett 2019-07-29 20:40:55 +00:00
commit dab09db585
11 changed files with 139 additions and 22 deletions

View File

@ -16,6 +16,5 @@ COPY devel /app/
COPY example-puzzles /puzzles/
COPY theme /theme/
WORKDIR /moth/
ENTRYPOINT [ "python3", "/app/devel-server.py" ]
CMD [ "--bind", "0.0.0.0:8080", "--puzzles", "/puzzles", "--theme", "/theme" ]

View File

@ -23,8 +23,14 @@ and comes with a JavaScript-based scoreboard to display team rankings.
Running a Development Server
============================
To use example puzzles
docker run --rm -it -p 8080:8080 dirtbags/moth-devel
or, to use your own puzzles
docker run --rm -it -p 8080:8080 -v /path/to/puzzles:/puzzles:ro dirtbags/moth-devel
And point a browser to http://localhost:8080/ (or whatever host is running the server).
The development server includes a number of Python libraries that we have found useful in writing puzzles.

View File

@ -1 +1 @@
3.1-rc2
3.3

16
contrib/smash Executable file
View File

@ -0,0 +1,16 @@
#! /bin/sh
## Run two of these to trigger the race condition from
BASEURL=http://localhost:8080
URL=$BASEURL/answer
while true; do
curl \
-X POST \
-F "cat=byobf" \
-F "points=10" \
-F "id=test" \
-F "answer=6" \
$URL
done

View File

@ -14,18 +14,25 @@ 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
try:
ThreadingHTTPServer = http.server.ThreadingHTTPServer
except AttributeError:
import socketserver
class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
class MothServer(http.server.ThreadingHTTPServer):
class MothServer(ThreadingHTTPServer):
def __init__(self, server_address, RequestHandlerClass):
super().__init__(server_address, RequestHandlerClass)
self.args = {}
@ -35,13 +42,28 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
endpoints = []
def __init__(self, request, client_address, server):
super().__init__(request, client_address, server, directory=server.args["theme_dir"])
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)
# 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"))
cat = moth.Category(self.server.args["puzzles_dir"].joinpath(category), self.seed)
catpath = str(self.server.args["puzzles_dir"].joinpath(category))
cat = moth.Category(catpath, self.seed)
puzzle = cat.puzzle(points)
return puzzle
@ -75,7 +97,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
if not p.is_dir() or p.match(".*"):
continue
catName = p.parts[-1]
cat = moth.Category(p, self.seed)
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:
@ -255,8 +277,6 @@ if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
mydir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))
server = MothServer((addr, port), MothRequestHandler)
server.args["base_url"] = args.base
server.args["puzzles_dir"] = pathlib.Path(args.puzzles)

View File

@ -12,6 +12,7 @@ import os
import random
import string
import tempfile
import shlex
messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
@ -112,7 +113,7 @@ class Puzzle:
elif key == 'name':
pass
elif key == 'file':
parts = val.split()
parts = shlex.split(val)
name = parts[0]
hidden = False
stream = open(name, 'rb')
@ -264,10 +265,10 @@ class Puzzle:
def html_body(self):
"""Format and return the markdown for the puzzle body."""
return mistune.markdown(self.get_body(), escape=False)
def package(self, answers=False):
"""Return a dict packaging of the puzzle."""
files = [fn for fn,f in self.files.items() if f.visible]
return {
'authors': self.authors,

View File

@ -5,6 +5,7 @@ Being in this list is voluntary. Add your name when you contribute code.
* Paul Ferrell
* Shannon Steinfadt
* John Donaldson
* 3ch01c
Word List
---------

View File

@ -17,7 +17,14 @@ function helperUpdateAnswer(event) {
values.push(c.value)
}
}
value = values.join(",")
if (e.classList.contains("sort")) {
values.sort()
}
let join = e.dataset.join
if (join === undefined) {
join = ","
}
value = values.join(join)
}
// First make any adjustments to the value
@ -33,8 +40,42 @@ function helperUpdateAnswer(event) {
answer.dispatchEvent(new InputEvent("input"))
}
function helperRemoveInput(e) {
let item = e.target.parentElement
let container = item.parentElement
item.remove()
var event = new Event("input")
container.dispatchEvent(event)
}
function helperExpandInputs(e) {
let item = e.target.parentElement
let container = item.parentElement
let template = container.firstElementChild
let newElement = template.cloneNode(true)
// Add remove button
let remove = document.createElement("button")
remove.innerText = ""
remove.title = "Remove this input"
remove.addEventListener("click", helperRemoveInput)
newElement.appendChild(remove)
// Zero it out, otherwise whatever's in first element is copied too
newElement.querySelector("input").value = ""
container.insertBefore(newElement, item)
var event = new Event("input")
container.dispatchEvent(event)
}
function helperActivate(e) {
e.addEventListener("input", helperUpdateAnswer)
for (let exp of e.querySelectorAll(".expand")) {
exp.addEventListener("click", helperExpandInputs)
}
}
function helperInit(event) {

View File

@ -17,8 +17,13 @@ This is just a demonstration page.
You will probably only want one of these in a page,
to avoid confusing people.
Timestamp
<input type="datetime-local" class="answer">
RFC3339 Timestamp
<div class="answer" data-join="">
<input type="date">
<input type="hidden" value="T">
<input type="time" step="1">
<input type="hidden" value="Z">
</div>
All lower-case letters
<input class="answer lower">
@ -31,6 +36,12 @@ Multiple concatenated values
<input>
</div>
Free input, sorted, concatenated values
<ul class="answer lower sort">
<li><input></li>
<li><button class="expand" title="Add another input"></button><l/i>
</ul>
Select from an ordered list of options
<ul class="answer">
<li><input type="checkbox" value="horn">Horns</li>

View File

@ -11,6 +11,7 @@ import (
"os"
"path"
"strings"
"sync"
"time"
)
@ -21,12 +22,13 @@ type Instance struct {
ThemeDir string
AttemptInterval time.Duration
categories map[string]*Mothball
update chan bool
jPuzzleList []byte
jPointsLog []byte
nextAttempt map[string]time.Time
mux *http.ServeMux
categories map[string]*Mothball
update chan bool
jPuzzleList []byte
jPointsLog []byte
nextAttempt map[string]time.Time
nextAttemptMutex *sync.RWMutex
mux *http.ServeMux
}
func (ctx *Instance) Initialize() error {
@ -42,6 +44,7 @@ func (ctx *Instance) Initialize() error {
ctx.categories = map[string]*Mothball{}
ctx.update = make(chan bool, 10)
ctx.nextAttempt = map[string]time.Time{}
ctx.nextAttemptMutex = new(sync.RWMutex)
ctx.mux = http.NewServeMux()
ctx.BindHandlers()
@ -129,8 +132,15 @@ func (ctx *Instance) ThemePath(parts ...string) string {
func (ctx *Instance) TooFast(teamId string) bool {
now := time.Now()
ctx.nextAttemptMutex.RLock()
next, _ := ctx.nextAttempt[teamId]
ctx.nextAttemptMutex.RUnlock()
ctx.nextAttemptMutex.Lock()
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
ctx.nextAttemptMutex.Unlock()
return now.Before(next)
}
@ -212,7 +222,10 @@ func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.Read
}
func (ctx *Instance) ValidTeamId(teamId string) bool {
ctx.nextAttemptMutex.RLock()
_, ok := ctx.nextAttempt[teamId]
ctx.nextAttemptMutex.RUnlock()
return ok
}

View File

@ -186,19 +186,28 @@ func (ctx *Instance) readTeams() {
now := time.Now()
added := 0
for k, _ := range newList {
if _, ok := ctx.nextAttempt[k]; !ok {
ctx.nextAttemptMutex.RLock()
_, ok := ctx.nextAttempt[k]
ctx.nextAttemptMutex.RUnlock()
if !ok {
ctx.nextAttemptMutex.Lock()
ctx.nextAttempt[k] = now
ctx.nextAttemptMutex.Unlock()
added += 1
}
}
// For any removed team IDs, remove them
removed := 0
ctx.nextAttemptMutex.Lock() // XXX: This could be less of a cludgel
for k, _ := range ctx.nextAttempt {
if _, ok := newList[k]; !ok {
delete(ctx.nextAttempt, k)
}
}
ctx.nextAttemptMutex.Unlock()
if (added > 0) || (removed > 0) {
log.Printf("Team IDs updated: %d added, %d removed", added, removed)