mirror of https://github.com/dirtbags/moth.git
Merge branch 'devel' into v4
This commit is contained in:
commit
cedc8521ff
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve MOTH
|
||||||
|
labels: bug
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
<!-- Description of the issue -->
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
1. <!-- First Step -->
|
||||||
|
2. <!-- Second Step -->
|
||||||
|
3. <!-- and so on… -->
|
||||||
|
|
||||||
|
**Expected behavior:**
|
||||||
|
|
||||||
|
<!-- What you expect to happen -->
|
||||||
|
|
||||||
|
**Actual behavior:**
|
||||||
|
|
||||||
|
<!-- What actually happens -->
|
||||||
|
|
||||||
|
**Reproduces how often:**
|
||||||
|
|
||||||
|
<!-- What percentage of the time does it reproduce? -->
|
||||||
|
|
||||||
|
### Versions
|
||||||
|
|
||||||
|
<!-- What version of MOTH are you running? Is this happening in mothd or in moth-devel? Are you running in Docker or natively? Also, please include the OS and what version of the OS you're running. -->
|
||||||
|
|
||||||
|
### Additional Information
|
||||||
|
|
||||||
|
<!-- Any additional information, configuration or data that might be necessary to reproduce the issue. -->
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for MOTH
|
||||||
|
labels: enhancement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- One paragraph explanation of the feature. -->
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
<!-- Why are we doing this? What use cases does it support? What is the expected outcome? -->
|
||||||
|
|
||||||
|
## Describe alternatives you've considered
|
||||||
|
|
||||||
|
<!-- A clear and concise description of the alternative solutions you've considered. Be sure to explain why Atom's existing customizability isn't suitable for this feature. -->
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
|
||||||
|
<!-- Add any other context or screenshots about the feature request here. -->
|
|
@ -0,0 +1,12 @@
|
||||||
|
name: moth-devel Docker build
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-devel:
|
||||||
|
name: Build moth-devel
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Retrieve code
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
- name: Build mothd
|
||||||
|
run: docker build -f Dockerfile.moth-devel .
|
|
@ -0,0 +1,12 @@
|
||||||
|
name: Mothd Docker build
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-mothd:
|
||||||
|
name: Build mothd
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Retrieve code
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
- name: Build mothd
|
||||||
|
run: docker build -f Dockerfile.moth .
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -28,3 +28,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- Endpoints `/points.json`, `/puzzles.json`, and `/messages.html` (optional theme file) combine into `/state`
|
||||||
|
- No more `__devel__` category for dev server: this is now `.config.devel` in the `/state` endpoint
|
||||||
|
- Development server no longer serves a static `/` with links: it now redirects you to a randomly-generated seed URL
|
||||||
|
- Default theme modifications to handle all this
|
||||||
|
- Default theme now automatically "logs you in" with Team ID if it's getting state from the devel server
|
||||||
|
|
||||||
|
## [v3.5.1] - 2020-03-16
|
||||||
|
### Fixed
|
||||||
|
- Support insta-checking for legacy puzzles
|
||||||
|
|
||||||
|
## [v3.5.0] - 2020-03-13
|
||||||
|
### Changed
|
||||||
|
- We are now using SHA256 instead of djb2hash
|
||||||
|
### Added
|
||||||
|
- URL parameter to points.json to allow returning only the JSON for a single
|
||||||
|
team by its team id (e.g., points.json?id=abc123).
|
||||||
|
- A CONTRIBUTING.md to describe expectations when contributing to MOTH
|
||||||
|
- Include basic metadata in mothballs
|
||||||
|
- add_script_stream convenience function allows easy script addition to puzzle
|
||||||
|
- Autobuild Docker images to test buildability
|
||||||
|
- Extract and use X-Forwarded-For headers in mothd logging
|
||||||
|
- Mothballs can now specify `X-Answer-Pattern` header fields, which allow `*`
|
||||||
|
at the beginning, end, or both, of an answer. This is `X-` because we
|
||||||
|
are hoping to change how this works in the future.
|
||||||
|
### Fixed
|
||||||
|
- Handle cases where non-legacy puzzles don't have an `author` attribute
|
||||||
|
- Handle YAML-formatted file and script lists as expected
|
||||||
|
- YAML-formatted example puzzle actually works as expected
|
||||||
|
- points.log will now always be sorted chronologically
|
||||||
|
|
||||||
|
## [3.4.3] - 2019-11-20
|
||||||
|
### Fixed
|
||||||
|
- Made top-scoring teams full-width
|
||||||
|
|
||||||
|
## [3.4.2] - 2019-11-18
|
||||||
|
### Fixed
|
||||||
|
- Issue with multiple answers in devel server and YAML-format .moth
|
||||||
|
|
||||||
|
## [3.4.1] - 2019-11-17
|
||||||
|
### Fixed
|
||||||
|
- Scoreboard was double-counting points
|
||||||
|
|
||||||
|
## [3.4] - 2019-11-13
|
||||||
|
### Added
|
||||||
|
- A changelog
|
||||||
|
- Support for embedding Python libraries at the category or puzzle level
|
||||||
|
- Minimal PWA support to permit caching of currently-unlocked content
|
||||||
|
- Embedded graph in scoreboard
|
||||||
|
- Optional tracking of participant IDs
|
||||||
|
- New `notices.html` file for sending broadcast messages to players
|
||||||
|
### Changed
|
||||||
|
- Use native JS URL objects instead of wrangling everything by hand
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Contributing to MOTH
|
||||||
|
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||||
|
|
||||||
|
- Reporting a bug
|
||||||
|
- Discussing the current state of the code
|
||||||
|
- Submitting a fix
|
||||||
|
- Proposing new features
|
||||||
|
|
||||||
|
## We Develop with Github
|
||||||
|
We use github to host code, to track issues and feature requests, as well as accept pull requests.
|
||||||
|
|
||||||
|
## We Use [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow), So All Code Changes Happen Through Pull Requests
|
||||||
|
Pull requests are the best way to propose changes to the codebase (we use [Gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)). We actively welcome your pull requests:
|
||||||
|
|
||||||
|
1. Fork the repo and create your branch from `master`.
|
||||||
|
2. If you've added code that should be tested, add tests.
|
||||||
|
3. If you've changed APIs, update the documentation.
|
||||||
|
4. Ensure the test suite passes.
|
||||||
|
5. Make sure your code lints.
|
||||||
|
6. Update [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
7. Issue that pull request!
|
||||||
|
|
||||||
|
## We Deploy to a Variety of Architectures
|
||||||
|
MOTH is most often deployed using Docker, but we strive to ensure that it can easily be run outside of a Docker environment. Please ensure that and changes will not break or substantially alter Dockerized deployments and that, conversely, changes will not so substantially tie MOTH to Docker or particular Docker deployment that it becomes impractical to run MOTH anywhere but inside of Docker
|
||||||
|
|
||||||
|
## Any contributions you make will be under the MIT Software License
|
||||||
|
When you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||||
|
|
||||||
|
## Report bugs using Github's [issues](https://github.com/dirtbags/moth/issues)
|
||||||
|
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/dirtbags/moth/issues/new); it's that easy!
|
||||||
|
|
||||||
|
## Write bug reports with detail, background, and sample code
|
||||||
|
|
||||||
|
**Great Bug Reports** tend to have:
|
||||||
|
|
||||||
|
- A quick summary and/or background
|
||||||
|
- Steps to reproduce
|
||||||
|
- Be specific!
|
||||||
|
- Give sample code if you can.
|
||||||
|
- What you expected would happen
|
||||||
|
- What actually happens
|
||||||
|
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||||
|
|
||||||
|
## Use a Consistent Coding Style
|
||||||
|
|
||||||
|
### Go
|
||||||
|
* Run it through `gofmt`
|
||||||
|
|
||||||
|
### Javascript
|
||||||
|
* We use Javascript ASI
|
||||||
|
|
||||||
|
## References
|
||||||
|
This document was adapted from the open-source contribution guidelines from [https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62]
|
|
@ -7,4 +7,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o /mo
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /mothd /mothd
|
COPY --from=builder /mothd /mothd
|
||||||
COPY theme /theme
|
COPY theme /theme
|
||||||
|
COPY LICENSE.md /LICENSE
|
||||||
|
|
||||||
ENTRYPOINT [ "/mothd" ]
|
ENTRYPOINT [ "/mothd" ]
|
||||||
|
|
|
@ -16,6 +16,8 @@ RUN apk --no-cache add \
|
||||||
COPY devel /app/
|
COPY devel /app/
|
||||||
COPY example-puzzles /puzzles/
|
COPY example-puzzles /puzzles/
|
||||||
COPY theme /theme/
|
COPY theme /theme/
|
||||||
|
COPY LICENSE.md /LICENSE
|
||||||
|
COPY VERSION /VERSION
|
||||||
|
|
||||||
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" ]
|
||||||
|
|
25
README.md
25
README.md
|
@ -1,6 +1,14 @@
|
||||||
Dirtbags Monarch Of The Hill Server
|
Dirtbags Monarch Of The Hill Server
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
Master:
|
||||||
|
![](https://github.com/dirtbags/moth/workflows/Mothd%20Docker%20build/badge.svg?branch=master)
|
||||||
|
![](https://github.com/dirtbags/moth/workflows/moth-devel%20Docker%20build/badge.svg?branch=master)
|
||||||
|
|
||||||
|
Devel:
|
||||||
|
![](https://github.com/dirtbags/moth/workflows/Mothd%20Docker%20build/badge.svg?branch=devel)
|
||||||
|
![](https://github.com/dirtbags/moth/workflows/moth-devel%20Docker%20build/badge.svg?branch=devel)
|
||||||
|
|
||||||
This is a set of thingies to run our Monarch-Of-The-Hill contest,
|
This is a set of thingies to run our Monarch-Of-The-Hill contest,
|
||||||
which in the past has been called
|
which in the past has been called
|
||||||
"Tracer FIRE",
|
"Tracer FIRE",
|
||||||
|
@ -67,7 +75,7 @@ you can copy the example puzzles as a starting point:
|
||||||
|
|
||||||
Then launch the development server:
|
Then launch the development server:
|
||||||
|
|
||||||
$ python3 tools/devel-server.py
|
$ python3 devel/devel-server.py
|
||||||
|
|
||||||
Point a web browser at http://localhost:8080/
|
Point a web browser at http://localhost:8080/
|
||||||
and start hacking on things in your `puzzles` directory.
|
and start hacking on things in your `puzzles` directory.
|
||||||
|
@ -133,6 +141,18 @@ We sometimes to set `teamids.txt` to a bunch of random 8-digit hex values:
|
||||||
Remember that team IDs are essentially passwords.
|
Remember that team IDs are essentially passwords.
|
||||||
|
|
||||||
|
|
||||||
|
Enabling offline/PWA mode
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
If the file `state/export_manifest` is found, the server will expose the
|
||||||
|
endpoint `/current_manifest.json?id=<teamId>`. This endpoint will return
|
||||||
|
a list of all files, including static theme content and JSON and content
|
||||||
|
for currently-unlocked puzzles. This is used by the native PWA
|
||||||
|
implementation and `Cache` button on the index page to cache all of the
|
||||||
|
content necessary to display currently-open puzzles while offline.
|
||||||
|
Grading will be unavailable while offline. Some puzzles may not function
|
||||||
|
as expected while offline. A valid team ID must be provided.
|
||||||
|
|
||||||
Mothball Directory
|
Mothball Directory
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
@ -149,4 +169,7 @@ If you remove a mothball,
|
||||||
the category will vanish,
|
the category will vanish,
|
||||||
but points scored in that category won't!
|
but points scored in that category won't!
|
||||||
|
|
||||||
|
Contributing to MOTH
|
||||||
|
==================
|
||||||
|
|
||||||
|
Please read [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
|
@ -14,6 +14,24 @@ type Award struct {
|
||||||
Points int
|
Points int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AwardList []*Award
|
||||||
|
|
||||||
|
// Implement sort.Interface on AwardList
|
||||||
|
func (awards AwardList) Len() int {
|
||||||
|
return len(awards)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (awards AwardList) Less(i, j int) bool {
|
||||||
|
return awards[i].When.Before(awards[j].When)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (awards AwardList) Swap(i, j int) {
|
||||||
|
tmp := awards[i]
|
||||||
|
awards[i] = awards[j]
|
||||||
|
awards[j] = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func ParseAward(s string) (*Award, error) {
|
func ParseAward(s string) (*Award, error) {
|
||||||
ret := Award{}
|
ret := Award{}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAward(t *testing.T) {
|
func TestAward(t *testing.T) {
|
||||||
|
@ -38,3 +39,23 @@ func TestAward(t *testing.T) {
|
||||||
t.Error("Not throwing error on bad points")
|
t.Error("Not throwing error on bad points")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAwardList(t *testing.T) {
|
||||||
|
a, _ := ParseAward("1536958399 1a2b3c4d counting 1")
|
||||||
|
b, _ := ParseAward("1536958400 1a2b3c4d counting 1")
|
||||||
|
c, _ := ParseAward("1536958300 1a2b3c4d counting 1")
|
||||||
|
list := AwardList{a, b, c}
|
||||||
|
|
||||||
|
if sort.IsSorted(list) {
|
||||||
|
t.Error("Unsorted list thinks it's sorted")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Stable(list)
|
||||||
|
if (list[0] != c) || (list[1] != a) || (list[2] != b) {
|
||||||
|
t.Error("Sorting didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! sort.IsSorted(list) {
|
||||||
|
t.Error("Sorted list thinks it isn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -73,7 +76,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
pointstr := req.FormValue("points")
|
pointstr := req.FormValue("points")
|
||||||
answer := req.FormValue("answer")
|
answer := req.FormValue("answer")
|
||||||
|
|
||||||
if ! ctx.ValidTeamId(teamId) {
|
if !ctx.ValidTeamId(teamId) {
|
||||||
respond(
|
respond(
|
||||||
w, req, JSendFail,
|
w, req, JSendFail,
|
||||||
"Invalid team ID",
|
"Invalid team ID",
|
||||||
|
@ -150,9 +153,14 @@ func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx *Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
teamId, ok := req.URL.Query()["id"]
|
||||||
|
pointsLog := ctx.jPointsLog
|
||||||
|
if ok && len(teamId[0]) > 0 {
|
||||||
|
pointsLog = ctx.generatePointsLog(teamId[0])
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(ctx.jPointsLog)
|
w.Write(pointsLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) contentHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx *Instance) contentHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -217,6 +225,65 @@ func (ctx *Instance) staticHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
http.ServeContent(w, req, path, d.ModTime(), f)
|
http.ServeContent(w, req, path, d.ModTime(), f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) manifestHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if !ctx.Runtime.export_manifest {
|
||||||
|
http.Error(w, "Endpoint disabled", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamId := req.FormValue("id")
|
||||||
|
if _, err := ctx.TeamName(teamId); err != nil {
|
||||||
|
http.Error(w, "Must provide a valid team ID", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Method == http.MethodHead {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := make([]string, 0)
|
||||||
|
manifest = append(manifest, "puzzles.json")
|
||||||
|
manifest = append(manifest, "points.json")
|
||||||
|
|
||||||
|
// Pack up the theme files
|
||||||
|
theme_root_re := regexp.MustCompile(fmt.Sprintf("^%s/", ctx.ThemeDir))
|
||||||
|
filepath.Walk(ctx.ThemeDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() { // Only package up files
|
||||||
|
localized_path := theme_root_re.ReplaceAllLiteralString(path, "")
|
||||||
|
manifest = append(manifest, localized_path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Package up files for currently-unlocked puzzles in categories
|
||||||
|
for category_name, category := range ctx.categories {
|
||||||
|
if _, ok := ctx.MaxPointsUnlocked[category_name]; ok { // Check that the category is actually unlocked. This should never fail, probably
|
||||||
|
for _, file := range category.zf.File {
|
||||||
|
parts := strings.Split(file.Name, "/")
|
||||||
|
|
||||||
|
if parts[0] == "content" { // Only pick up content files, not thing like map.txt
|
||||||
|
for _, puzzlemap := range category.puzzlemap { // Figure out which puzzles are currently unlocked
|
||||||
|
if puzzlemap.Path == parts[1] && puzzlemap.Points <= ctx.MaxPointsUnlocked[category_name] {
|
||||||
|
|
||||||
|
manifest = append(manifest, path.Join("content", category_name, path.Join(parts[1:]...)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
manifest_json, _ := json.Marshal(manifest)
|
||||||
|
w.Write(manifest_json)
|
||||||
|
}
|
||||||
|
|
||||||
type FurtiveResponseWriter struct {
|
type FurtiveResponseWriter struct {
|
||||||
w http.ResponseWriter
|
w http.ResponseWriter
|
||||||
statusCode *int
|
statusCode *int
|
||||||
|
@ -242,10 +309,22 @@ func (ctx *Instance) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
|
||||||
w: wOrig,
|
w: wOrig,
|
||||||
statusCode: new(int),
|
statusCode: new(int),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientIP := r.RemoteAddr
|
||||||
|
|
||||||
|
if (ctx.UseXForwarded) {
|
||||||
|
forwardedIP := r.Header.Get("X-Forwarded-For")
|
||||||
|
forwardedIP = strings.Split(forwardedIP, ", ")[0]
|
||||||
|
|
||||||
|
if forwardedIP != "" {
|
||||||
|
clientIP = forwardedIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.mux.ServeHTTP(w, r)
|
ctx.mux.ServeHTTP(w, r)
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"%s %s %s %d\n",
|
"%s %s %s %d\n",
|
||||||
r.RemoteAddr,
|
clientIP,
|
||||||
r.Method,
|
r.Method,
|
||||||
r.URL,
|
r.URL,
|
||||||
*w.statusCode,
|
*w.statusCode,
|
||||||
|
@ -259,4 +338,5 @@ func (ctx *Instance) BindHandlers() {
|
||||||
ctx.mux.HandleFunc(ctx.Base+"/content/", ctx.contentHandler)
|
ctx.mux.HandleFunc(ctx.Base+"/content/", ctx.contentHandler)
|
||||||
ctx.mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
|
ctx.mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
|
||||||
ctx.mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
|
ctx.mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
|
||||||
|
ctx.mux.HandleFunc(ctx.Base+"/current_manifest.json", ctx.manifestHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RuntimeConfig struct {
|
||||||
|
export_manifest bool
|
||||||
|
}
|
||||||
|
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
Base string
|
Base string
|
||||||
MothballDir string
|
MothballDir string
|
||||||
|
@ -22,15 +26,18 @@ type Instance struct {
|
||||||
StateDir string
|
StateDir string
|
||||||
ThemeDir string
|
ThemeDir string
|
||||||
AttemptInterval time.Duration
|
AttemptInterval time.Duration
|
||||||
Debug bool
|
UseXForwarded bool
|
||||||
|
|
||||||
categories map[string]*Zipfs
|
Runtime RuntimeConfig
|
||||||
update chan bool
|
|
||||||
jPuzzleList []byte
|
categories map[string]*Mothball
|
||||||
jPointsLog []byte
|
MaxPointsUnlocked map[string]int
|
||||||
nextAttempt map[string]time.Time
|
update chan bool
|
||||||
nextAttemptMutex *sync.RWMutex
|
jPuzzleList []byte
|
||||||
mux *http.ServeMux
|
jPointsLog []byte
|
||||||
|
nextAttempt map[string]time.Time
|
||||||
|
nextAttemptMutex *sync.RWMutex
|
||||||
|
mux *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) Initialize() error {
|
func (ctx *Instance) Initialize() error {
|
||||||
|
@ -78,6 +85,7 @@ func (ctx *Instance) MaybeInitialize() {
|
||||||
os.Remove(ctx.StatePath("until"))
|
os.Remove(ctx.StatePath("until"))
|
||||||
os.Remove(ctx.StatePath("disabled"))
|
os.Remove(ctx.StatePath("disabled"))
|
||||||
os.Remove(ctx.StatePath("points.log"))
|
os.Remove(ctx.StatePath("points.log"))
|
||||||
|
|
||||||
os.RemoveAll(ctx.StatePath("points.tmp"))
|
os.RemoveAll(ctx.StatePath("points.tmp"))
|
||||||
os.RemoveAll(ctx.StatePath("points.new"))
|
os.RemoveAll(ctx.StatePath("points.new"))
|
||||||
os.RemoveAll(ctx.StatePath("teams"))
|
os.RemoveAll(ctx.StatePath("teams"))
|
||||||
|
@ -134,26 +142,27 @@ 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()
|
ctx.nextAttemptMutex.RLock()
|
||||||
next, _ := ctx.nextAttempt[teamId]
|
next, _ := ctx.nextAttempt[teamId]
|
||||||
ctx.nextAttemptMutex.RUnlock()
|
ctx.nextAttemptMutex.RUnlock()
|
||||||
|
|
||||||
ctx.nextAttemptMutex.Lock()
|
ctx.nextAttemptMutex.Lock()
|
||||||
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval)
|
||||||
ctx.nextAttemptMutex.Unlock()
|
ctx.nextAttemptMutex.Unlock()
|
||||||
|
|
||||||
return now.Before(next)
|
return now.Before(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) PointsLog() []*Award {
|
func (ctx *Instance) PointsLog(teamId string) AwardList {
|
||||||
var ret []*Award
|
awardlist := AwardList{}
|
||||||
|
|
||||||
fn := ctx.StatePath("points.log")
|
fn := ctx.StatePath("points.log")
|
||||||
|
|
||||||
f, err := os.Open(fn)
|
f, err := os.Open(fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Unable to open %s: %s", fn, err)
|
log.Printf("Unable to open %s: %s", fn, err)
|
||||||
return ret
|
return awardlist
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
|
@ -165,10 +174,13 @@ func (ctx *Instance) PointsLog() []*Award {
|
||||||
log.Printf("Skipping malformed award line %s: %s", line, err)
|
log.Printf("Skipping malformed award line %s: %s", line, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ret = append(ret, cur)
|
if len(teamId) > 0 && cur.TeamId != teamId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
awardlist = append(awardlist, cur)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return awardlist
|
||||||
}
|
}
|
||||||
|
|
||||||
// AwardPoints gives points to teamId in category.
|
// AwardPoints gives points to teamId in category.
|
||||||
|
@ -189,7 +201,7 @@ func (ctx *Instance) AwardPoints(teamId, category string, points int) error {
|
||||||
return fmt.Errorf("No registered team with this hash")
|
return fmt.Errorf("No registered team with this hash")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range ctx.PointsLog() {
|
for _, e := range ctx.PointsLog("") {
|
||||||
if a.Same(e) {
|
if a.Same(e) {
|
||||||
return fmt.Errorf("Points already awarded to this team in this category")
|
return fmt.Errorf("Points already awarded to this team in this category")
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,12 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PuzzleMap struct {
|
|
||||||
Points int
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *PuzzleMap) MarshalJSON() ([]byte, error) {
|
func (pm *PuzzleMap) MarshalJSON() ([]byte, error) {
|
||||||
if pm == nil {
|
if pm == nil {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
|
@ -33,7 +29,7 @@ func (pm *PuzzleMap) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
func (ctx *Instance) generatePuzzleList() {
|
func (ctx *Instance) generatePuzzleList() {
|
||||||
maxByCategory := map[string]int{}
|
maxByCategory := map[string]int{}
|
||||||
for _, a := range ctx.PointsLog() {
|
for _, a := range ctx.PointsLog("") {
|
||||||
if a.Points > maxByCategory[a.Category] {
|
if a.Points > maxByCategory[a.Category] {
|
||||||
maxByCategory[a.Category] = a.Points
|
maxByCategory[a.Category] = a.Points
|
||||||
}
|
}
|
||||||
|
@ -41,45 +37,29 @@ func (ctx *Instance) generatePuzzleList() {
|
||||||
|
|
||||||
ret := map[string][]PuzzleMap{}
|
ret := map[string][]PuzzleMap{}
|
||||||
for catName, mb := range ctx.categories {
|
for catName, mb := range ctx.categories {
|
||||||
mf, err := mb.Open("map.txt")
|
filtered_puzzlemap := make([]PuzzleMap, 0, 30)
|
||||||
if err != nil {
|
|
||||||
// File isn't in there
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer mf.Close()
|
|
||||||
|
|
||||||
pm := make([]PuzzleMap, 0, 30)
|
|
||||||
completed := true
|
completed := true
|
||||||
scanner := bufio.NewScanner(mf)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
var pointval int
|
for _, pm := range mb.puzzlemap {
|
||||||
var dir string
|
filtered_puzzlemap = append(filtered_puzzlemap, pm)
|
||||||
|
|
||||||
n, err := fmt.Sscanf(line, "%d %s", &pointval, &dir)
|
if pm.Points > maxByCategory[catName] {
|
||||||
if err != nil {
|
|
||||||
log.Printf("Parsing map for %s: %v", catName, err)
|
|
||||||
continue
|
|
||||||
} else if n != 2 {
|
|
||||||
log.Printf("Parsing map for %s: short read", catName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pm = append(pm, PuzzleMap{pointval, dir})
|
|
||||||
|
|
||||||
if pointval > maxByCategory[catName] {
|
|
||||||
completed = false
|
completed = false
|
||||||
|
maxByCategory[catName] = pm.Points
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if completed {
|
if completed {
|
||||||
pm = append(pm, PuzzleMap{0, ""})
|
filtered_puzzlemap = append(filtered_puzzlemap, PuzzleMap{0, ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
ret[catName] = pm
|
ret[catName] = filtered_puzzlemap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the unlocked points for use in other functions
|
||||||
|
ctx.MaxPointsUnlocked = maxByCategory
|
||||||
|
|
||||||
jpl, err := json.Marshal(ret)
|
jpl, err := json.Marshal(ret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Marshalling puzzles.js: %v", err)
|
log.Printf("Marshalling puzzles.js: %v", err)
|
||||||
|
@ -88,13 +68,13 @@ func (ctx *Instance) generatePuzzleList() {
|
||||||
ctx.jPuzzleList = jpl
|
ctx.jPuzzleList = jpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Instance) generatePointsLog() {
|
func (ctx *Instance) generatePointsLog(teamId string) []byte {
|
||||||
var ret struct {
|
var ret struct {
|
||||||
Teams map[string]string `json:"teams"`
|
Teams map[string]string `json:"teams"`
|
||||||
Points []*Award `json:"points"`
|
Points []*Award `json:"points"`
|
||||||
}
|
}
|
||||||
ret.Teams = map[string]string{}
|
ret.Teams = map[string]string{}
|
||||||
ret.Points = ctx.PointsLog()
|
ret.Points = ctx.PointsLog(teamId)
|
||||||
|
|
||||||
teamNumbersById := map[string]int{}
|
teamNumbersById := map[string]int{}
|
||||||
for nr, a := range ret.Points {
|
for nr, a := range ret.Points {
|
||||||
|
@ -114,9 +94,13 @@ func (ctx *Instance) generatePointsLog() {
|
||||||
jpl, err := json.Marshal(ret)
|
jpl, err := json.Marshal(ret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Marshalling points.js: %v", err)
|
log.Printf("Marshalling points.js: %v", err)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
ctx.jPointsLog = jpl
|
|
||||||
|
if len(teamId) == 0 {
|
||||||
|
ctx.jPointsLog = jpl
|
||||||
|
}
|
||||||
|
return jpl
|
||||||
}
|
}
|
||||||
|
|
||||||
// maintenance runs
|
// maintenance runs
|
||||||
|
@ -124,6 +108,9 @@ func (ctx *Instance) tidy() {
|
||||||
// Do they want to reset everything?
|
// Do they want to reset everything?
|
||||||
ctx.MaybeInitialize()
|
ctx.MaybeInitialize()
|
||||||
|
|
||||||
|
// Check set config
|
||||||
|
ctx.UpdateConfig()
|
||||||
|
|
||||||
// Refresh all current categories
|
// Refresh all current categories
|
||||||
for categoryName, mb := range ctx.categories {
|
for categoryName, mb := range ctx.categories {
|
||||||
if err := mb.Refresh(); err != nil {
|
if err := mb.Refresh(); err != nil {
|
||||||
|
@ -217,17 +204,26 @@ func (ctx *Instance) readTeams() {
|
||||||
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||||
// removing each points.new/ file as it goes.
|
// removing each points.new/ file as it goes.
|
||||||
func (ctx *Instance) collectPoints() {
|
func (ctx *Instance) collectPoints() {
|
||||||
logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
points := ctx.PointsLog("")
|
||||||
|
|
||||||
|
pointsFilename := ctx.StatePath("points.log")
|
||||||
|
pointsNewFilename := ctx.StatePath("points.log.new")
|
||||||
|
|
||||||
|
// Yo, this is delicate.
|
||||||
|
// If we have to return early, we must remove this file.
|
||||||
|
// If the file's written and we move it successfully,
|
||||||
|
// we need to remove all the little points files that built it.
|
||||||
|
newPoints, err := os.OpenFile(pointsNewFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Can't append to points log: %s", err)
|
log.Printf("Can't append to points log: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer logf.Close()
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(ctx.StatePath("points.new"))
|
files, err := ioutil.ReadDir(ctx.StatePath("points.new"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading packages: %s", err)
|
log.Printf("Error reading packages: %s", err)
|
||||||
}
|
}
|
||||||
|
removearino := make([]string, 0, len(files))
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
filename := ctx.StatePath("points.new", f.Name())
|
filename := ctx.StatePath("points.new", f.Name())
|
||||||
s, err := ioutil.ReadFile(filename)
|
s, err := ioutil.ReadFile(filename)
|
||||||
|
@ -242,7 +238,7 @@ func (ctx *Instance) collectPoints() {
|
||||||
}
|
}
|
||||||
|
|
||||||
duplicate := false
|
duplicate := false
|
||||||
for _, e := range ctx.PointsLog() {
|
for _, e := range points {
|
||||||
if award.Same(e) {
|
if award.Same(e) {
|
||||||
duplicate = true
|
duplicate = true
|
||||||
break
|
break
|
||||||
|
@ -252,13 +248,30 @@ func (ctx *Instance) collectPoints() {
|
||||||
if duplicate {
|
if duplicate {
|
||||||
log.Printf("Skipping duplicate points: %s", award.String())
|
log.Printf("Skipping duplicate points: %s", award.String())
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(logf, "%s\n", award.String())
|
points = append(points, award)
|
||||||
}
|
}
|
||||||
|
removearino = append(removearino, filename)
|
||||||
|
}
|
||||||
|
|
||||||
logf.Sync()
|
sort.Stable(points)
|
||||||
if err := os.Remove(filename); err != nil {
|
for _, point := range points {
|
||||||
log.Printf("Unable to remove %s: %s", filename, err)
|
fmt.Fprintln(newPoints, point.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newPoints.Close()
|
||||||
|
|
||||||
|
if err := os.Rename(pointsNewFilename, pointsFilename); err != nil {
|
||||||
|
log.Printf("Unable to move %s to %s: %s", pointsFilename, pointsNewFilename, err)
|
||||||
|
if err := os.Remove(pointsNewFilename); err != nil {
|
||||||
|
log.Printf("Also couldn't remove %s: %s", pointsNewFilename, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range removearino {
|
||||||
|
if err := os.Remove(filename); err != nil {
|
||||||
|
log.Printf("Unable to remove %s: %s", filename, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,6 +299,20 @@ func (ctx *Instance) isEnabled() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) UpdateConfig() {
|
||||||
|
// Handle export manifest
|
||||||
|
if _, err := os.Stat(ctx.StatePath("export_manifest")); err == nil {
|
||||||
|
if !ctx.Runtime.export_manifest {
|
||||||
|
log.Print("Enabling manifest export")
|
||||||
|
ctx.Runtime.export_manifest = true
|
||||||
|
}
|
||||||
|
} else if ctx.Runtime.export_manifest {
|
||||||
|
log.Print("Disabling manifest export")
|
||||||
|
ctx.Runtime.export_manifest = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// maintenance is the goroutine that runs a periodic maintenance task
|
// maintenance is the goroutine that runs a periodic maintenance task
|
||||||
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||||
for {
|
for {
|
||||||
|
@ -294,7 +321,7 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||||
ctx.readTeams()
|
ctx.readTeams()
|
||||||
ctx.collectPoints()
|
ctx.collectPoints()
|
||||||
ctx.generatePuzzleList()
|
ctx.generatePuzzleList()
|
||||||
ctx.generatePointsLog()
|
ctx.generatePointsLog("")
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.update:
|
case <-ctx.update:
|
||||||
|
|
|
@ -58,6 +58,12 @@ func main() {
|
||||||
20*time.Second,
|
20*time.Second,
|
||||||
"Time between maintenance tasks",
|
"Time between maintenance tasks",
|
||||||
)
|
)
|
||||||
|
flag.BoolVar(
|
||||||
|
&ctx.UseXForwarded,
|
||||||
|
"x-forwarded-for",
|
||||||
|
false,
|
||||||
|
"Emit IPs from the X-Forwarded-For header in logs, when available, instead of the source IP. Use this when running behind a load-balancer or proxy",
|
||||||
|
)
|
||||||
listen := flag.String(
|
listen := flag.String(
|
||||||
"listen",
|
"listen",
|
||||||
":8080",
|
":8080",
|
||||||
|
|
27
devel.sh
27
devel.sh
|
@ -1,27 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# Script to clone and start a development server
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -f tools/devel-server.py ]; then
|
|
||||||
cat <<EOM
|
|
||||||
This script is intended to be used to bootstrap a moth development server. It
|
|
||||||
looks like you're running the script from a moth repository working directory.
|
|
||||||
|
|
||||||
$ mkdir /tmp/moth
|
|
||||||
$ cd /tmp/moth
|
|
||||||
$ curl https://raw.githubusercontent.com/dirtbags/moth/master/devel.sh | sh
|
|
||||||
EOM
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -d puzzles ] || mkdir -p puzzles
|
|
||||||
[ -d moth/bin ] || git clone https://github.com/dirtbags/moth.git
|
|
||||||
|
|
||||||
cd moth
|
|
||||||
puzzles="$(readlink -e ../puzzles)"
|
|
||||||
ln -sf "${puzzles}" puzzles
|
|
||||||
|
|
||||||
printf "\n[+] Place puzzles at ${puzzles} ...\n"
|
|
||||||
python3 tools/devel-server.py
|
|
|
@ -1,6 +1,5 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import cgitb
|
import cgitb
|
||||||
import html
|
import html
|
||||||
import cgi
|
import cgi
|
||||||
|
@ -43,6 +42,13 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
super().__init__(request, client_address, server)
|
super().__init__(request, client_address, server)
|
||||||
|
|
||||||
|
# Why isn't this the default?!
|
||||||
|
def guess_type(self, path):
|
||||||
|
mtype, encoding = mimetypes.guess_type(path)
|
||||||
|
if encoding:
|
||||||
|
return "%s; encoding=%s" % (mtype, encoding)
|
||||||
|
else:
|
||||||
|
return mtype
|
||||||
|
|
||||||
# Backport from Python 3.7
|
# Backport from Python 3.7
|
||||||
def translate_path(self, path):
|
def translate_path(self, path):
|
||||||
|
@ -91,7 +97,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"short": "",
|
"short": "",
|
||||||
"description": "Provided answer was not in list of answers"
|
"description": "%r was not in list of answers" % self.req.get("answer")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +148,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
obj["hint"] = puzzle.hint
|
obj["hint"] = puzzle.hint
|
||||||
obj["summary"] = puzzle.summary
|
obj["summary"] = puzzle.summary
|
||||||
obj["logs"] = puzzle.logs
|
obj["logs"] = puzzle.logs
|
||||||
|
obj["format"] = puzzle._source_format
|
||||||
|
|
||||||
self.send_json(obj)
|
self.send_json(obj)
|
||||||
endpoints.append(('/{seed}/content/{cat}/{points}/puzzle.json', handle_puzzle))
|
endpoints.append(('/{seed}/content/{cat}/{points}/puzzle.json', handle_puzzle))
|
||||||
|
@ -252,7 +259,7 @@ if __name__ == '__main__':
|
||||||
'--theme', default='theme',
|
'--theme', default='theme',
|
||||||
help="Directory containing theme files")
|
help="Directory containing theme files")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--bind', default="127.0.0.1:8080",
|
'--bind', default=":8080",
|
||||||
help="Bind to ip:port"
|
help="Bind to ip:port"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -267,7 +274,7 @@ if __name__ == '__main__':
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
parts = args.bind.split(":")
|
parts = args.bind.split(":")
|
||||||
addr = parts[0] or "0.0.0.0"
|
addr = parts[0]
|
||||||
port = int(parts[1])
|
port = int(parts[1])
|
||||||
if args.verbose >= 2:
|
if args.verbose >= 2:
|
||||||
log_level = logging.DEBUG
|
log_level = logging.DEBUG
|
||||||
|
@ -278,6 +285,8 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
logging.basicConfig(level=log_level)
|
logging.basicConfig(level=log_level)
|
||||||
|
|
||||||
|
mimetypes.add_type("application/javascript", ".mjs")
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -22,14 +22,12 @@ messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def djb2hash(str):
|
def sha256hash(str):
|
||||||
h = 5381
|
return hashlib.sha256(str.encode("utf-8")).hexdigest()
|
||||||
for c in str.encode("utf-8"):
|
|
||||||
h = ((h * 33) + c) & 0xffffffff
|
|
||||||
return h
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def pushd(newdir):
|
def pushd(newdir):
|
||||||
|
newdir = str(newdir)
|
||||||
curdir = os.getcwd()
|
curdir = os.getcwd()
|
||||||
LOGGER.debug("Attempting to chdir from %s to %s" % (curdir, newdir))
|
LOGGER.debug("Attempting to chdir from %s to %s" % (curdir, newdir))
|
||||||
os.chdir(newdir)
|
os.chdir(newdir)
|
||||||
|
@ -123,10 +121,13 @@ class Puzzle:
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
self._source_format = "py"
|
||||||
|
|
||||||
self.points = points
|
self.points = points
|
||||||
self.summary = None
|
self.summary = None
|
||||||
self.authors = []
|
self.authors = []
|
||||||
self.answers = []
|
self.answers = []
|
||||||
|
self.xAnchors = {"begin", "end"}
|
||||||
self.scripts = []
|
self.scripts = []
|
||||||
self.pattern = None
|
self.pattern = None
|
||||||
self.hint = None
|
self.hint = None
|
||||||
|
@ -153,8 +154,10 @@ class Puzzle:
|
||||||
line = ""
|
line = ""
|
||||||
if stream.read(3) == "---":
|
if stream.read(3) == "---":
|
||||||
header = "yaml"
|
header = "yaml"
|
||||||
|
self._source_format = "yaml"
|
||||||
else:
|
else:
|
||||||
header = "moth"
|
header = "moth"
|
||||||
|
self._source_format = "moth"
|
||||||
|
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
|
|
||||||
|
@ -210,6 +213,16 @@ class Puzzle:
|
||||||
if not isinstance(val, str):
|
if not isinstance(val, str):
|
||||||
raise ValueError("Answers must be strings, got %s, instead" % (type(val),))
|
raise ValueError("Answers must be strings, got %s, instead" % (type(val),))
|
||||||
self.answers.append(val)
|
self.answers.append(val)
|
||||||
|
elif key == 'x-answer-pattern':
|
||||||
|
a = val.strip("*")
|
||||||
|
assert "*" not in a, "Patterns may only have * at the beginning and end"
|
||||||
|
assert "?" not in a, "Patterns do not currently support ? characters"
|
||||||
|
assert "[" not in a, "Patterns do not currently support character ranges"
|
||||||
|
self.answers.append(a)
|
||||||
|
if val.startswith("*"):
|
||||||
|
self.xAnchors.discard("begin")
|
||||||
|
if val.endswith("*"):
|
||||||
|
self.xAnchors.discard("end")
|
||||||
elif key == "answers":
|
elif key == "answers":
|
||||||
for answer in val:
|
for answer in val:
|
||||||
if not isinstance(answer, str):
|
if not isinstance(answer, str):
|
||||||
|
@ -233,15 +246,31 @@ class Puzzle:
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
self.files[name] = PuzzleFile(stream, name, not hidden)
|
self.files[name] = PuzzleFile(stream, name, not hidden)
|
||||||
elif key == 'files':
|
|
||||||
for file in val:
|
elif key == 'files' and isinstance(val, dict):
|
||||||
path = file["path"]
|
for filename, options in val.items():
|
||||||
stream = open(path, "rb")
|
if not options:
|
||||||
name = file.get("name") or path
|
options = {}
|
||||||
self.files[name] = PuzzleFile(stream, name, not file.get("hidden"))
|
source = options.get("source", filename)
|
||||||
|
hidden = options.get("hidden", False)
|
||||||
|
|
||||||
|
stream = open(source, "rb")
|
||||||
|
self.files[filename] = PuzzleFile(stream, filename, not hidden)
|
||||||
|
|
||||||
|
elif key == 'files' and isinstance(val, list):
|
||||||
|
for filename in val:
|
||||||
|
stream = open(filename, "rb")
|
||||||
|
self.files[filename] = PuzzleFile(stream, filename)
|
||||||
|
|
||||||
elif key == 'script':
|
elif key == 'script':
|
||||||
stream = open(val, 'rb')
|
stream = open(val, 'rb')
|
||||||
self.add_script_stream(stream, val)
|
self.add_script_stream(stream, val)
|
||||||
|
|
||||||
|
elif key == "scripts" and isinstance(val, list):
|
||||||
|
for script in val:
|
||||||
|
stream = open(script, "rb")
|
||||||
|
self.add_script_stream(stream, script)
|
||||||
|
|
||||||
elif key == "objective":
|
elif key == "objective":
|
||||||
self.objective = val
|
self.objective = val
|
||||||
elif key == "success":
|
elif key == "success":
|
||||||
|
@ -307,6 +336,11 @@ class Puzzle:
|
||||||
name = self.random_hash()
|
name = self.random_hash()
|
||||||
self.files[name] = PuzzleFile(stream, name, visible)
|
self.files[name] = PuzzleFile(stream, name, visible)
|
||||||
|
|
||||||
|
def create_stream(self, name=None, visible=True):
|
||||||
|
stream = io.BytesIO()
|
||||||
|
self.add_stream(stream, name, visible)
|
||||||
|
return stream
|
||||||
|
|
||||||
def add_file(self, filename, visible=True):
|
def add_file(self, filename, visible=True):
|
||||||
fd = open(filename, 'rb')
|
fd = open(filename, 'rb')
|
||||||
name = os.path.basename(filename)
|
name = os.path.basename(filename)
|
||||||
|
@ -396,7 +430,12 @@ class Puzzle:
|
||||||
self.body.write('</pre>')
|
self.body.write('</pre>')
|
||||||
|
|
||||||
def get_authors(self):
|
def get_authors(self):
|
||||||
return self.authors or [self.author]
|
if len(self.authors) > 0:
|
||||||
|
return self.authors
|
||||||
|
elif hasattr(self, "author"):
|
||||||
|
return [self.author]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
def get_body(self):
|
def get_body(self):
|
||||||
return self.body.getvalue()
|
return self.body.getvalue()
|
||||||
|
@ -408,7 +447,7 @@ class Puzzle:
|
||||||
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 = sorted([fn for fn,f in self.files.items() if f.visible])
|
||||||
hidden = [fn for fn,f in self.files.items() if not f.visible]
|
hidden = [fn for fn,f in self.files.items() if not f.visible]
|
||||||
return {
|
return {
|
||||||
'authors': self.get_authors(),
|
'authors': self.get_authors(),
|
||||||
|
@ -422,12 +461,13 @@ class Puzzle:
|
||||||
'success': self.success,
|
'success': self.success,
|
||||||
'solution': self.solution,
|
'solution': self.solution,
|
||||||
'ksas': self.ksas,
|
'ksas': self.ksas,
|
||||||
|
'xAnchors': list(self.xAnchors),
|
||||||
}
|
}
|
||||||
|
|
||||||
def hashes(self):
|
def hashes(self):
|
||||||
"Return a list of answer hashes"
|
"Return a list of answer hashes"
|
||||||
|
|
||||||
return [djb2hash(a) for a in self.answers]
|
return [sha256hash(a) for a in self.answers]
|
||||||
|
|
||||||
|
|
||||||
class Category:
|
class Category:
|
||||||
|
@ -460,7 +500,6 @@ class Category:
|
||||||
with pushd(self.path):
|
with pushd(self.path):
|
||||||
self.catmod.make(points, puzzle)
|
self.catmod.make(points, puzzle)
|
||||||
else:
|
else:
|
||||||
with pushd(self.path):
|
|
||||||
puzzle.read_directory(path)
|
puzzle.read_directory(path)
|
||||||
return puzzle
|
return puzzle
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import binascii
|
import binascii
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import moth
|
import moth
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
|
@ -61,6 +63,24 @@ def build_category(categorydir, outdir):
|
||||||
zipfileraw.close()
|
zipfileraw.close()
|
||||||
shutil.move(zipfileraw.name, zipfilename)
|
shutil.move(zipfileraw.name, zipfilename)
|
||||||
|
|
||||||
|
def write_metadata(ziphandle, category):
|
||||||
|
metadata = {"platform": {}, "moth": {}, "category": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("../VERSION", "r") as infile:
|
||||||
|
version = infile.read().strip()
|
||||||
|
metadata["moth"]["version"] = version
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
metadata["category"]["build_time"] = datetime.datetime.now().strftime("%c")
|
||||||
|
metadata["category"]["type"] = "catmod" if category.catmod is not None else "traditional"
|
||||||
|
metadata["platform"]["arch"] = platform.machine()
|
||||||
|
metadata["platform"]["os"] = platform.system()
|
||||||
|
metadata["platform"]["version"] = platform.platform()
|
||||||
|
metadata["platform"]["python_version"] = platform.python_version()
|
||||||
|
|
||||||
|
ziphandle.writestr("meta.json", json.dumps(metadata))
|
||||||
|
|
||||||
# Returns a file-like object containing the contents of the new zip file
|
# Returns a file-like object containing the contents of the new zip file
|
||||||
def package(categoryname, categorydir, seed):
|
def package(categoryname, categorydir, seed):
|
||||||
|
@ -90,6 +110,7 @@ def package(categoryname, categorydir, seed):
|
||||||
zf.writestr("puzzles.txt", "\n".join(str(p) for p in puzzles) + "\n")
|
zf.writestr("puzzles.txt", "\n".join(str(p) for p in puzzles) + "\n")
|
||||||
write_kv_pairs(zf, 'answers.txt', answers)
|
write_kv_pairs(zf, 'answers.txt', answers)
|
||||||
write_kv_pairs(zf, 'summaries.txt', summary)
|
write_kv_pairs(zf, 'summaries.txt', summary)
|
||||||
|
write_metadata(zf, cat)
|
||||||
|
|
||||||
# clean up
|
# clean up
|
||||||
zf.close()
|
zf.close()
|
||||||
|
|
|
@ -28,6 +28,7 @@ If you can't use docker,
|
||||||
try this:
|
try this:
|
||||||
|
|
||||||
apt install python3
|
apt install python3
|
||||||
|
pip3 install scapy pillow PyYAML
|
||||||
git clone https://github.com/dirtbags/moth/
|
git clone https://github.com/dirtbags/moth/
|
||||||
cd moth
|
cd moth
|
||||||
python3 devel/devel-server.py --puzzles example-puzzles
|
python3 devel/devel-server.py --puzzles example-puzzles
|
||||||
|
|
|
@ -3,6 +3,7 @@ Summary: Static puzzle resource files
|
||||||
File: salad.jpg s.jpg
|
File: salad.jpg s.jpg
|
||||||
File: salad2.jpg s2.jpg hidden
|
File: salad2.jpg s2.jpg hidden
|
||||||
Answer: salad
|
Answer: salad
|
||||||
|
X-Answer-Pattern: *pong
|
||||||
|
|
||||||
You can include additional resources in a static puzzle,
|
You can include additional resources in a static puzzle,
|
||||||
by dropping them in the directory and listing them in a `File:` header field.
|
by dropping them in the directory and listing them in a `File:` header field.
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import io
|
||||||
|
import categorylib # Category-level libraries can be imported here
|
||||||
|
|
||||||
|
def make(puzzle):
|
||||||
|
import puzzlelib # puzzle-level libraries can only be imported inside of the make function
|
||||||
|
puzzle.authors = ['donaldson']
|
||||||
|
puzzle.summary = 'more crazy stuff you can do with puzzle generation using Python libraries'
|
||||||
|
|
||||||
|
puzzle.body.write("## Crazy Things You Can Do With Puzzle Generation (part II)\n")
|
||||||
|
puzzle.body.write("\n")
|
||||||
|
puzzle.body.write("The source to this puzzle has some more advanced examples of stuff you can do in Python.\n")
|
||||||
|
puzzle.body.write("\n")
|
||||||
|
puzzle.body.write("1 == %s\n\n" % puzzlelib.getone(),)
|
||||||
|
puzzle.body.write("2 == %s\n\n" % categorylib.gettwo(),)
|
||||||
|
|
||||||
|
puzzle.answers.append('tea')
|
||||||
|
answer = puzzle.make_answer() # Generates a random answer, appending it to puzzle.answers too
|
||||||
|
puzzle.log("Answers: {}".format(puzzle.answers))
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""This is an example of a puzzle-level library.
|
||||||
|
|
||||||
|
This library can be imported by sibling puzzles using `import puzzlelib`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getone():
|
||||||
|
return 1
|
|
@ -1,6 +1,8 @@
|
||||||
Summary: Answer patterns
|
Summary: Answer patterns
|
||||||
Answer: command.com
|
Answer: command.com
|
||||||
Answer: COMMAND.COM
|
Answer: COMMAND.COM
|
||||||
|
X-Answer-Pattern: PINBALL.*
|
||||||
|
X-Answer-Pattern: pinball.*
|
||||||
Author: neale
|
Author: neale
|
||||||
Pattern: [0-9A-Za-z]{1,8}\.[A-Za-z]{1,3}
|
Pattern: [0-9A-Za-z]{1,8}\.[A-Za-z]{1,3}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
// jshint asi:true
|
||||||
|
|
||||||
|
var dragSrcEl_
|
||||||
|
|
||||||
|
function draggableHandleDragStart(e) {
|
||||||
|
e.target.dataset.moveId = e.timeStamp.toString()
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
e.dataTransfer.setData('text/plain', e.target.dataset.moveId)
|
||||||
|
|
||||||
|
// this/e.target is the source node.
|
||||||
|
e.target.classList.add('moving')
|
||||||
|
}
|
||||||
|
|
||||||
|
function draggableHandleDragOver(e) {
|
||||||
|
if (e.target.attributes.draggable) {
|
||||||
|
e.preventDefault() // Allows us to drop.
|
||||||
|
}
|
||||||
|
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function draggableHandleDragEnter(e) {
|
||||||
|
e.target.classList.add('over')
|
||||||
|
}
|
||||||
|
|
||||||
|
function draggableHandleDragLeave(e) {
|
||||||
|
// this/e.target is previous target element.
|
||||||
|
e.target.classList.remove('over')
|
||||||
|
}
|
||||||
|
|
||||||
|
function draggableHandleDrop(e) {
|
||||||
|
// this/e.target is current target element.
|
||||||
|
let tgt = e.target
|
||||||
|
let src = document.querySelector("[data-move-id=\"" + e.dataTransfer.getData("text/plain") + "\"]")
|
||||||
|
|
||||||
|
// Don't do anything if we're dropping on the same column we're dragging.
|
||||||
|
if (src == tgt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let tgtPrev = tgt.previousSibling
|
||||||
|
src.replaceWith(tgt)
|
||||||
|
tgtPrev.after(src)
|
||||||
|
|
||||||
|
tgt.dispatchEvent(new InputEvent("input", {bubbles: true}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function draggableHandleDragEnd(e) {
|
||||||
|
// this/e.target is the source node.
|
||||||
|
for (e of document.querySelectorAll("[draggable].over")) {
|
||||||
|
e.classList.remove("over")
|
||||||
|
}
|
||||||
|
for (e of document.querySelectorAll("[draggable].moving")) {
|
||||||
|
e.classList.remove("moving")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortableInit(event) {
|
||||||
|
for (let e of document.querySelectorAll("[draggable]")) {
|
||||||
|
e.addEventListener('dragstart', draggableHandleDragStart, false)
|
||||||
|
e.addEventListener('dragenter', draggableHandleDragEnter, false)
|
||||||
|
e.addEventListener('dragover', draggableHandleDragOver, false)
|
||||||
|
e.addEventListener('dragleave', draggableHandleDragLeave, false)
|
||||||
|
e.addEventListener('drop', draggableHandleDrop, false)
|
||||||
|
e.addEventListener('dragend', draggableHandleDragEnd, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", sortableInit)
|
||||||
|
} else {
|
||||||
|
sortableInit()
|
||||||
|
}
|
|
@ -78,6 +78,7 @@ function helperActivate(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function helperInit(event) {
|
function helperInit(event) {
|
||||||
for (let e of document.querySelectorAll(".answer")) {
|
for (let e of document.querySelectorAll(".answer")) {
|
||||||
helperActivate(e)
|
helperActivate(e)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
Summary: Using JavaScript Input Helpers
|
Summary: Using JavaScript Input Helpers
|
||||||
Author: neale
|
Author: neale
|
||||||
Script: helpers.js
|
Script: helpers.js
|
||||||
|
Script: draggable.js
|
||||||
Answer: helper
|
Answer: helper
|
||||||
|
|
||||||
MOTH only takes static answers:
|
MOTH only takes static answers:
|
||||||
|
@ -42,6 +43,13 @@ Free input, sorted, concatenated values
|
||||||
<li><button class="expand" title="Add another input">➕</button><l/i>
|
<li><button class="expand" title="Add another input">➕</button><l/i>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
User-draggable values
|
||||||
|
<ul class="answer">
|
||||||
|
<li draggable="true"><input value="First" readonly></li>
|
||||||
|
<li draggable="true"><input value="Third" readonly></li>
|
||||||
|
<li draggable="true"><input value="Second" readonly></li>
|
||||||
|
</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>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""This is an example of a category-level library.
|
||||||
|
|
||||||
|
This library can be imported by child puzzles using `import categorylib`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def gettwo():
|
||||||
|
return 2
|
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg id="svg2" viewBox="54.122 189.992 297.46 291.819" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="layer1" transform="translate(-169.61 -199.65)">
|
||||||
|
<g id="g4449" transform="matrix(-0.88604614,0.46359708,0.46359708,0.88604614,470.47926,-197.23926)">
|
||||||
|
<g id="g5242" transform="matrix(1.5856107,0.2341624,0.16998437,1.0681247,-408.59758,-19.14161)">
|
||||||
|
<path d="m 486.09807,434.34389 c 0,0 33.21943,37.1978 28.44237,74.37852 -9.69431,-1.18729 -28.05784,-15.23506 -49.27832,31.40255 -26.1093,57.38302 4.04174,56.10536 -8.44016,86.24229 -12.48109,30.13515 -22.15846,-14.83784 -21.15287,-20.87068 27.75853,-86.95342 19.16417,-80.74245 50.42898,-171.15268 z" style="fill:#a0b977;stroke:#fff6a4;stroke-width:0.84436792" id="path3012"/>
|
||||||
|
<path id="path5252" style="fill:#a0b977;stroke:#fff6a4;stroke-width:0.84436792" d="m 483.14429,428.90547 c 0,0 -49.53236,20.97257 -65.01984,56.05329 9.315,2.5399 33.23264,-3.21777 27.44694,46.44614 -7.11913,61.10645 -33.44802,48.75472 -38.28907,80.38979 -4.84082,31.6332 27.73702,-5.05478 30.04352,-10.83255 28.20085,-88.31453 25.759,-79.4494 45.81845,-172.05667 z"/>
|
||||||
|
</g>
|
||||||
|
<g id="g2994" transform="translate(137.37681,206.38155)">
|
||||||
|
<g id="g2996">
|
||||||
|
<path id="path2998" style="opacity:0.65;fill:#988378" d="m 331.9,266.68 c 0,0 -3.033,-36.711 32.988,-31.248 0.001,0 -8.798,28.704 -32.988,31.248 z"/>
|
||||||
|
<path id="path3000" style="opacity:0.65;fill:none;stroke:#4d2b2b" d="m 365.19,236.01 c -16.611,5.02 -37.262,34.979 -37.262,34.979"/>
|
||||||
|
</g>
|
||||||
|
<g id="g3002">
|
||||||
|
<path id="path3004" style="opacity:0.65;fill:#988378" d="m 322.29,263.16 c 0,0 30.612,-20.489 3.905,-45.27 0,0 -16.978,24.759 -3.905,45.27 z"/>
|
||||||
|
<path id="path3006" style="opacity:0.65;fill:none;stroke:#4d2b2b" d="m 325.56,218.02 c 6.415,16.123 -4.164,50.937 -4.164,50.937"/>
|
||||||
|
</g>
|
||||||
|
<g id="g3008">
|
||||||
|
<g id="g3014">
|
||||||
|
<g id="g3016">
|
||||||
|
<path id="path3018" style="fill:#a0b977;stroke:#fff8bd" d="m 312.76,265.25 c 0,0 -7.279,-0.166 -8.315,3.177 0,0 -10.622,-1.202 -12.795,0.741 -2.173,1.943 -83.173,23.401 -105.58,33.197 -22.412,9.798 -52.957,23.878 -62.557,56.499 -9.601,32.618 47.177,25.109 47.177,25.109 0,0 15.808,-0.329 43.093,11.279 27.285,11.609 68.089,11.708 84.028,-43.027 0,0 -19.067,71.565 45.054,82.563 64.119,11 88.375,59.362 91.092,15.183 2.717,-44.179 2.635,-40.541 -25.9,-81.855 -28.537,-41.314 -53.385,-72.585 -53.385,-72.585 0,0 -0.527,-10.11 -11.182,-12.893 0,0 -0.28,-5.846 -6.373,-9.83 -0.001,0 -7.393,-10.433 -24.354,-7.558 z"/>
|
||||||
|
<path id="path3020" style="fill:#fff8bd" d="m 360.56,302.09 c -3.478,-4.498 -5.454,-6.984 -5.454,-6.984 0,0 -0.527,-10.111 -11.182,-12.893 0,0 -0.279,-5.846 -6.371,-9.831 0,0 -7.394,-10.434 -24.355,-7.559 0,0 -7.277,-0.165 -8.315,3.178 0,0 -10.621,-1.202 -12.794,0.741 -0.426,0.382 -3.868,1.507 -9.299,3.149 l -6.162,11.986 c 0,0 26.633,-7.093 27.148,2.491 0.516,9.583 5.73,11.9 5.73,11.9 0,0 3.08,11.078 2.149,17.07 0,0 6.907,-12.514 10.485,-12.8 0,0 9.773,1.124 11.463,-7.692 0,0 16.849,10.656 21.34,16.184 0.237,0.292 0.439,0.568 0.604,0.828 3.271,5.205 5.013,-9.768 5.013,-9.768 z"/>
|
||||||
|
<path id="path3022" style="fill:none;stroke:#fff8bd" d="m 313.73,311.85 c 0,0 -13.709,22.16 -17.873,49.646"/>
|
||||||
|
<path id="path3024" style="fill:#fff8bd" d="m 225.91,315.99 c 0,0 6.543,21.195 22.854,12.133 0,0 -8.072,-8.788 -10.24,-9.892 -2.168,-1.102 -6.429,-1.994 -6.429,-1.994 l 3.36,-11.951 c 0,0 -1.844,-4.046 -9.545,11.704 z"/>
|
||||||
|
<path id="path3026" style="fill:#fff8bd" d="m 379.47,367.78 c 0,0 -15.43,8.015 -24.641,-3.771 l 10.435,-3.478 c 0,0 8.961,1.849 8.698,2.699 -0.265,0.851 6.322,-6.878 6.322,-6.878 0,0 3.014,7.895 -0.814,11.428 z"/>
|
||||||
|
<path id="path3028" style="fill:#4d2b2b" d="m 229.7,317.19 c 0,0 8.137,15.76 18.629,10.824 0,0 -8.191,-6.876 -8.109,-7.558 0.082,-0.682 -8,-5.868 -10.52,-3.266 z"/>
|
||||||
|
<path id="path3030" style="fill:#4d2b2b" d="m 374.9,364.06 c 0,0 -15.354,8.875 -21.523,-0.941 0,0 10.607,-1.369 10.906,-1.986 0.299,-0.619 9.903,-0.626 10.617,2.927 z"/>
|
||||||
|
<g id="g3032">
|
||||||
|
<path id="path3034" style="fill:#988378" d="m 341.58,278.41 c 2.846,1.426 1.744,5.08 1.744,5.08 10.654,2.782 11.182,12.894 11.182,12.894 0,0 24.85,31.27 53.385,72.584 28.537,41.315 27.662,40.764 24.943,84.943 0,0 0.961,-3.087 -0.504,-18.654 -1.465,-15.565 -5.752,-23.075 -5.752,-23.075 -12.012,-29.904 -42.311,-57.106 -42.311,-57.106 1.119,3.139 -6.828,16.378 -4.391,11.899 2.438,-4.479 0.834,-10.56 -0.529,-8.413 -1.367,2.148 -3.982,9.456 -7.013,5.725 -3.029,-3.732 -1.735,-2.285 -12.151,-1.33 -10.42,0.955 -6.982,-1.119 -6.982,-1.119 -0.889,2.865 6.521,-0.768 6.521,-0.768 10.789,-3.283 14.434,-0.406 14.434,-0.406 l 2.492,-6.906 c 5.588,-10.133 -29.023,-47.05 -32.24,-54.68 -3.217,-7.629 -7.158,-9.549 -7.158,-9.549 2.932,-6.071 -15.424,-6.533 -15.424,-6.533 -0.482,-2.943 -7.926,-5.949 -13.338,-7.629 -5.412,-1.68 -2.876,3.643 -2.876,3.643 -1.515,-1.865 -8.749,0.079 -16.517,4.299 -7.768,4.221 -29.491,8.996 -40.82,11.763 -11.329,2.766 -14.416,18.339 -14.416,18.339 0,0 5.523,0.191 8.379,5.614 2.854,5.422 6.021,8.15 6.021,8.15 0,0 0.098,4.742 -5.435,-2.732 -5.533,-7.478 -13.437,-7.313 -13.437,-7.313 l 3.704,-11.938 c -4.347,3.886 -7.491,12.333 -7.491,12.333 -1.203,-4.561 3.16,-15.247 3.16,-15.247 0,0 -26.412,7.509 -53.154,14.392 -26.741,6.883 -46.178,32.961 -46.178,32.961 12.879,-23.555 37.309,-38.035 56.473,-46.413 22.411,-9.798 103.41,-31.254 105.58,-33.197 2.174,-1.942 12.795,-0.741 12.795,-0.741 0.52,-1.671 2.598,-2.466 4.547,-2.842 0,0 14.472,4.288 15.098,9.018 0.001,0 6.241,-2.764 17.655,2.954 z"/>
|
||||||
|
<path id="path3036" style="fill:#4d2b2b" d="m 403.91,370.46 c -27.5,-39.816 -50.17,-74.071 -50.17,-74.071 0,0 -0.508,-9.744 -10.775,-12.426 0,0 1.061,-3.521 -1.682,-4.896 -11.001,-5.509 -17.014,-2.846 -17.014,-2.846 -0.604,-4.56 -14.551,-8.693 -14.551,-8.693 -1.878,0.363 -3.883,1.13 -4.381,2.739 0,0 -10.236,-1.157 -12.332,0.715 -2.095,1.872 -79.716,27.89 -101.31,37.332 0,0 108.65,-38.886 110.44,-35.965 0,0 2.538,-0.563 4.079,-0.085 0,0 1.508,-5.949 6.385,-2.073 0,0 2.324,-0.963 9.282,4.91 l 1.392,3.133 c 0,0 5.651,-3.311 11.19,0.771 0,0 8.491,-1.079 6.696,5.792 0,0 6.988,2.507 9.793,9.793 l 0.469,2.847 52.489,73.023 z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="g3038" style="opacity:0.5"/>
|
||||||
|
</g>
|
||||||
|
<g id="g3050" style="opacity:0.2">
|
||||||
|
<g id="g3052">
|
||||||
|
<path id="path3054" style="fill:none;stroke:#fdee72" d="m 193.51,313.22 c 0,0 -5.379,15.082 -24.865,26.137 -19.486,11.057 -23.777,32.758 -23.777,32.758"/>
|
||||||
|
<path id="path3056" style="fill:none;stroke:#fdee72" d="m 229.05,308.89 c 0,0 -17.959,18.51 -37.127,29.663 -19.166,11.153 -27.597,31.571 -27.927,36.007"/>
|
||||||
|
<path id="path3058" style="fill:none;stroke:#fdee72" d="m 219.07,341.05 c 0,0 -24.876,10.43 -32.637,37.687"/>
|
||||||
|
<path id="path3060" style="fill:none;stroke:#fdee72" d="m 237.68,338.45 c 0,0 -28.399,28.531 -29.662,43.846"/>
|
||||||
|
<path id="path3062" style="fill:none;stroke:#fdee72" d="m 301.4,299.58 c 0,0 -8.387,63.012 -22.965,81.872"/>
|
||||||
|
<path id="path3064" style="fill:none;stroke:#fdee72" d="m 284.8,292.34 c 0,0 -22.351,40.531 -18.574,59.851"/>
|
||||||
|
</g>
|
||||||
|
<g id="g3066">
|
||||||
|
<path id="path3068" style="fill:none;stroke:#fdee72" d="m 409.42,384 c 0,0 -4.191,15.455 5.504,35.652 9.695,20.199 0.838,40.471 0.838,40.471"/>
|
||||||
|
<path id="path3070" style="fill:none;stroke:#fdee72" d="m 382.7,360.17 c 0,0 4.191,25.448 13.566,45.545 9.377,20.094 4.648,41.675 2.391,45.505"/>
|
||||||
|
<path id="path3072" style="fill:none;stroke:#fdee72" d="m 372.55,392.27 c 0,0 14.479,22.76 5.299,49.573"/>
|
||||||
|
<path id="path3074" style="fill:none;stroke:#fdee72" d="m 358.75,379.52 c 0,0 7.042,39.637 -0.657,52.934"/>
|
||||||
|
<path id="path3076" style="fill:none;stroke:#fdee72" d="m 328.6,311.25 c 0,0 -29.065,56.534 -27.854,80.341"/>
|
||||||
|
<path id="path3078" style="fill:none;stroke:#fdee72" d="m 346.36,314.77 c 0,0 -4.771,46.039 -18.897,59.751"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
|
@ -1,6 +1,7 @@
|
||||||
// jshint asi:true
|
// jshint asi:true
|
||||||
|
|
||||||
var devel = false
|
var devel = false
|
||||||
|
var teamId
|
||||||
var heartbeatInterval = 40000
|
var heartbeatInterval = 40000
|
||||||
|
|
||||||
function toast(message, timeout=5000) {
|
function toast(message, timeout=5000) {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
<form>
|
<form>
|
||||||
<input type="hidden" name="cat">
|
<input type="hidden" name="cat">
|
||||||
<input type="hidden" name="points">
|
<input type="hidden" name="points">
|
||||||
|
<input type="hidden" name="xAnswer">
|
||||||
Team ID: <input type="text" name="id"> <br>
|
Team ID: <input type="text" name="id"> <br>
|
||||||
Answer: <input type="text" name="answer" id="answer"> <span id="answer_ok"></span><br>
|
Answer: <input type="text" name="answer" id="answer"> <span id="answer_ok"></span><br>
|
||||||
<input type="submit" value="Submit">
|
<input type="submit" value="Submit">
|
||||||
|
|
|
@ -51,12 +51,10 @@ function devel_addin(obj, e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash routine used in v3.4 and earlier
|
||||||
|
|
||||||
// The routine used to hash answers in compiled puzzle packages
|
|
||||||
function djb2hash(buf) {
|
function djb2hash(buf) {
|
||||||
let h = 5381
|
let h = 5381
|
||||||
for (let c of (new TextEncoder).encode(buf)) { // Encode as UTF-8 and read in each byte
|
for (let c of (new TextEncoder()).encode(buf)) { // Encode as UTF-8 and read in each byte
|
||||||
// JavaScript converts everything to a signed 32-bit integer when you do bitwise operations.
|
// JavaScript converts everything to a signed 32-bit integer when you do bitwise operations.
|
||||||
// So we have to do "unsigned right shift" by zero to get it back to unsigned.
|
// So we have to do "unsigned right shift" by zero to get it back to unsigned.
|
||||||
h = (((h * 33) + c) & 0xffffffff) >>> 0
|
h = (((h * 33) + c) & 0xffffffff) >>> 0
|
||||||
|
@ -64,6 +62,47 @@ function djb2hash(buf) {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The routine used to hash answers in compiled puzzle packages
|
||||||
|
async function sha256Hash(message) {
|
||||||
|
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the provided answer possibly correct?
|
||||||
|
async function possiblyCorrect(answer) {
|
||||||
|
for (let correctHash of window.puzzle.hashes) {
|
||||||
|
// CPU time is cheap. Especially if it's not our server's time.
|
||||||
|
// So we'll just try absolutely everything and see what happens.
|
||||||
|
// We're counting on hash collisions being extremely rare with the algorithm we use.
|
||||||
|
// And honestly, this pales in comparison to the amount of CPU being eaten by
|
||||||
|
// something like the github 404 page.
|
||||||
|
|
||||||
|
if (djb2hash(answer) == correctHash) {
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
for (let end = 0; end <= answer.length; end += 1) {
|
||||||
|
if (window.puzzle.xAnchors && window.puzzle.xAnchors.includes("end") && (end != answer.length)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let beg = 0; beg < answer.length; beg += 1) {
|
||||||
|
if (window.puzzle.xAnchors && window.puzzle.xAnchors.includes("begin") && (beg != 0)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let sub = answer.substring(beg, end)
|
||||||
|
let digest = await sha256Hash(sub)
|
||||||
|
|
||||||
|
if (digest == correctHash) {
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Pop up a message
|
// Pop up a message
|
||||||
function toast(message, timeout=5000) {
|
function toast(message, timeout=5000) {
|
||||||
|
@ -80,9 +119,17 @@ function toast(message, timeout=5000) {
|
||||||
// When the user submits an answer
|
// When the user submits an answer
|
||||||
function submit(e) {
|
function submit(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
let data = new FormData(e.target)
|
||||||
|
|
||||||
|
// Kludge for patterned answers
|
||||||
|
let xAnswer = data.get("xAnswer")
|
||||||
|
if (xAnswer) {
|
||||||
|
data.set("answer", xAnswer)
|
||||||
|
}
|
||||||
|
window.data = data
|
||||||
fetch("answer", {
|
fetch("answer", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: new FormData(e.target),
|
body: data,
|
||||||
})
|
})
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
|
@ -180,21 +227,17 @@ function answerCheck(e) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let possiblyCorrect = false
|
possiblyCorrect(answer)
|
||||||
let answerHash = djb2hash(answer)
|
.then (correct => {
|
||||||
for (let correctHash of window.puzzle.hashes) {
|
document.querySelector("[name=xAnswer").value = correct || answer
|
||||||
if (correctHash == answerHash) {
|
if (correct) {
|
||||||
possiblyCorrect = true
|
ok.textContent = "⭕"
|
||||||
|
ok.title = "Possibly correct"
|
||||||
|
} else {
|
||||||
|
ok.textContent = "❌"
|
||||||
|
ok.title = "Definitely not correct"
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
if (possiblyCorrect) {
|
|
||||||
ok.textContent = "❓"
|
|
||||||
ok.title = "Possibly correct"
|
|
||||||
} else {
|
|
||||||
ok.textContent = "⛔"
|
|
||||||
ok.title = "Definitely not correct"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
@ -223,4 +266,3 @@ if (document.readyState === "loading") {
|
||||||
} else {
|
} else {
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue