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 example-puzzles /puzzles/
COPY theme /theme/ COPY theme /theme/
WORKDIR /moth/
ENTRYPOINT [ "python3", "/app/devel-server.py" ] ENTRYPOINT [ "python3", "/app/devel-server.py" ]
CMD [ "--bind", "0.0.0.0:8080", "--puzzles", "/puzzles", "--theme", "/theme" ] 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 Running a Development Server
============================ ============================
To use example puzzles
docker run --rm -it -p 8080:8080 dirtbags/moth-devel 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). 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. 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 pathlib
import random import random
import shutil import shutil
import socketserver
import sys import sys
import traceback import traceback
import mothballer import mothballer
import parse import parse
import urllib.parse
import posixpath
from http import HTTPStatus from http import HTTPStatus
sys.dont_write_bytecode = True # Don't write .pyc files 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): def __init__(self, server_address, RequestHandlerClass):
super().__init__(server_address, RequestHandlerClass) super().__init__(server_address, RequestHandlerClass)
self.args = {} self.args = {}
@ -35,13 +42,28 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
endpoints = [] endpoints = []
def __init__(self, request, client_address, server): 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): def get_puzzle(self):
category = self.req.get("cat") category = self.req.get("cat")
points = int(self.req.get("points")) 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) puzzle = cat.puzzle(points)
return puzzle return puzzle
@ -75,7 +97,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
if not p.is_dir() or p.match(".*"): if not p.is_dir() or p.match(".*"):
continue continue
catName = p.parts[-1] 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] = [[i, str(i)] for i in cat.pointvals()]
puzzles[catName].append([0, ""]) puzzles[catName].append([0, ""])
if len(puzzles) <= 1: if len(puzzles) <= 1:
@ -255,8 +277,6 @@ if __name__ == '__main__':
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
mydir = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))
server = MothServer((addr, port), MothRequestHandler) server = MothServer((addr, port), MothRequestHandler)
server.args["base_url"] = args.base server.args["base_url"] = args.base
server.args["puzzles_dir"] = pathlib.Path(args.puzzles) server.args["puzzles_dir"] = pathlib.Path(args.puzzles)

View File

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

View File

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

View File

@ -17,7 +17,14 @@ function helperUpdateAnswer(event) {
values.push(c.value) 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 // First make any adjustments to the value
@ -33,8 +40,42 @@ function helperUpdateAnswer(event) {
answer.dispatchEvent(new InputEvent("input")) 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) { function helperActivate(e) {
e.addEventListener("input", helperUpdateAnswer) e.addEventListener("input", helperUpdateAnswer)
for (let exp of e.querySelectorAll(".expand")) {
exp.addEventListener("click", helperExpandInputs)
}
} }
function helperInit(event) { 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, You will probably only want one of these in a page,
to avoid confusing people. to avoid confusing people.
Timestamp RFC3339 Timestamp
<input type="datetime-local" class="answer"> <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 All lower-case letters
<input class="answer lower"> <input class="answer lower">
@ -31,6 +36,12 @@ Multiple concatenated values
<input> <input>
</div> </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 Select from an ordered list of options
<ul class="answer"> <ul class="answer">
<li><input type="checkbox" value="horn">Horns</li> <li><input type="checkbox" value="horn">Horns</li>

View File

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

View File

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