From 7726444a98f79e848ecebcec0246ef9a15d2b65f Mon Sep 17 00:00:00 2001 From: Jack Miner <5547581+3ch01c@users.noreply.github.com> Date: Tue, 19 Nov 2019 19:16:21 -0700 Subject: [PATCH 01/42] Added id parameter to points.json to return only a team's score --- src/handlers.go | 7 ++++++- src/instance.go | 7 +++++-- src/maintenance.go | 18 +++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/handlers.go b/src/handlers.go index 00eca05..a0ceded 100644 --- a/src/handlers.go +++ b/src/handlers.go @@ -183,9 +183,14 @@ func (ctx *Instance) puzzlesHandler(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.WriteHeader(http.StatusOK) - w.Write(ctx.jPointsLog) + w.Write(pointsLog) } func (ctx *Instance) contentHandler(w http.ResponseWriter, req *http.Request) { diff --git a/src/instance.go b/src/instance.go index a415b40..0ec5d8e 100644 --- a/src/instance.go +++ b/src/instance.go @@ -151,7 +151,7 @@ func (ctx *Instance) TooFast(teamId string) bool { return now.Before(next) } -func (ctx *Instance) PointsLog() []*Award { +func (ctx *Instance) PointsLog(teamId string) []*Award { var ret []*Award fn := ctx.StatePath("points.log") @@ -170,6 +170,9 @@ func (ctx *Instance) PointsLog() []*Award { log.Printf("Skipping malformed award line %s: %s", line, err) continue } + if len(teamId) > 0 && cur.TeamId != teamId { + continue + } ret = append(ret, cur) } @@ -194,7 +197,7 @@ func (ctx *Instance) AwardPoints(teamId, category string, points int) error { return fmt.Errorf("No registered team with this hash") } - for _, e := range ctx.PointsLog() { + for _, e := range ctx.PointsLog("") { if a.Same(e) { return fmt.Errorf("Points already awarded to this team in this category") } diff --git a/src/maintenance.go b/src/maintenance.go index 87f35dc..0174902 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -28,7 +28,7 @@ func (pm *PuzzleMap) MarshalJSON() ([]byte, error) { func (ctx *Instance) generatePuzzleList() { maxByCategory := map[string]int{} - for _, a := range ctx.PointsLog() { + for _, a := range ctx.PointsLog("") { if a.Points > maxByCategory[a.Category] { maxByCategory[a.Category] = a.Points } @@ -67,13 +67,13 @@ func (ctx *Instance) generatePuzzleList() { ctx.jPuzzleList = jpl } -func (ctx *Instance) generatePointsLog() { +func (ctx *Instance) generatePointsLog(teamId string) []byte { var ret struct { Teams map[string]string `json:"teams"` Points []*Award `json:"points"` } ret.Teams = map[string]string{} - ret.Points = ctx.PointsLog() + ret.Points = ctx.PointsLog(teamId) teamNumbersById := map[string]int{} for nr, a := range ret.Points { @@ -93,9 +93,13 @@ func (ctx *Instance) generatePointsLog() { jpl, err := json.Marshal(ret) if err != nil { log.Printf("Marshalling points.js: %v", err) - return + return nil } - ctx.jPointsLog = jpl + + if len(teamId) == 0 { + ctx.jPointsLog = jpl + } + return jpl } // maintenance runs @@ -224,7 +228,7 @@ func (ctx *Instance) collectPoints() { } duplicate := false - for _, e := range ctx.PointsLog() { + for _, e := range ctx.PointsLog("") { if award.Same(e) { duplicate = true break @@ -290,7 +294,7 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) { ctx.readTeams() ctx.collectPoints() ctx.generatePuzzleList() - ctx.generatePointsLog() + ctx.generatePointsLog("") } select { case <-ctx.update: From 08082f0844d83bc934cd1d813f30f9703adcbf66 Mon Sep 17 00:00:00 2001 From: Jack Miner <5547581+3ch01c@users.noreply.github.com> Date: Wed, 20 Nov 2019 14:52:58 -0700 Subject: [PATCH 02/42] Added changes to CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b37b48..8eb2c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- 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). ## [3.4.2] - 2019-11-18 ### Fixed From 7e465eb94f1849d07e66bc4eb683efc62752453f Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 17 Dec 2019 17:48:22 +0000 Subject: [PATCH 03/42] Adding contribution guidelines --- CHANGELOG.md | 1 + CONTRIBUTING.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 +++ 3 files changed, 64 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb2c9d..d4c2611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 ## [3.4.2] - 2019-11-18 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..18ae606 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# 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 [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests +Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). 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 +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 impracticle to run MOTH anywhere but inside of Docker + +## Any contributions you make will be under the TODO Software License +TODO: +In short, 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](); it's that easy! + +## Write bug reports with detail, background, and sample code +[This is an example](http://stackoverflow.com/q/12488905/180626) of a bug report I wrote, and I think it's not a bad model. Here's [another example from Craig Hockenberry](http://www.openradar.me/11905408), an app developer whom I greatly respect. + +**Great Bug Reports** tend to have: + +- A quick summary and/or background +- Steps to reproduce + - Be specific! + - Give sample code if you can. [My stackoverflow question](http://stackoverflow.com/q/12488905/180626) includes sample code that *anyone* with a base R setup can run to reproduce what I was seeing +- 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) + +People *love* thorough bug reports. I'm not even kidding. + +## Use a Consistent Coding Style +I'm again borrowing these from [Facebook's Guidelines](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) + +* 2 spaces for indentation rather than tabs +* You can try running `npm run lint` for style unification + +### We use Javascript ASI + +## License +By contributing, you agree that your contributions will be licensed under its TODO License. + +## References +This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) diff --git a/README.md b/README.md index 99549f0..1268f45 100644 --- a/README.md +++ b/README.md @@ -161,4 +161,7 @@ If you remove a mothball, the category will vanish, but points scored in that category won't! +Contributing to MOTH +================== +Please read [CONTRIBUTING.md](CONTRIBUTING.md) From 1a1e69fac940bb03e730c1ef74ba4fcf03bca37e Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 17 Dec 2019 17:52:48 +0000 Subject: [PATCH 04/42] Updating git branching model --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18ae606..a9a2b51 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,8 +9,8 @@ We love your input! We want to make contributing to this project as easy and tra ## We Develop with Github We use github to host code, to track issues and feature requests, as well as accept pull requests. -## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests -Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your 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. From 2ca07d517bee8be0cd60fb6d965f4437b0cf9529 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 17 Dec 2019 19:47:57 +0000 Subject: [PATCH 05/42] Cleaning up CONTRIBUTING.md, clarifying license --- CONTRIBUTING.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9a2b51..a86cc91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,44 +17,34 @@ Pull requests are the best way to propose changes to the codebase (we use [Gitfl 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 +6. Update [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 impracticle to run MOTH anywhere but inside of Docker -## Any contributions you make will be under the TODO Software License -TODO: +## Any contributions you make will be under the MIT Software License In short, 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](); it's that easy! ## Write bug reports with detail, background, and sample code -[This is an example](http://stackoverflow.com/q/12488905/180626) of a bug report I wrote, and I think it's not a bad model. Here's [another example from Craig Hockenberry](http://www.openradar.me/11905408), an app developer whom I greatly respect. **Great Bug Reports** tend to have: - A quick summary and/or background - Steps to reproduce - Be specific! - - Give sample code if you can. [My stackoverflow question](http://stackoverflow.com/q/12488905/180626) includes sample code that *anyone* with a base R setup can run to reproduce what I was seeing + - 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) -People *love* thorough bug reports. I'm not even kidding. - ## Use a Consistent Coding Style -I'm again borrowing these from [Facebook's Guidelines](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) -* 2 spaces for indentation rather than tabs -* You can try running `npm run lint` for style unification - -### We use Javascript ASI - -## License -By contributing, you agree that your contributions will be licensed under its TODO License. +### Javascript +* We use Javascript ASI ## References -This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) +This document was adapted from the open-source contribution guidelines from [https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62] From 5f804feda2ffbd0460ed91b62aeb6a8eb28408f9 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 17 Dec 2019 19:51:23 +0000 Subject: [PATCH 06/42] Updating issues link --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a86cc91..12e6b82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ MOTH is most often deployed using Docker, but we strive to ensure that it can ea In short, 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](); it's that easy! +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 From 9447d4e3de759dae7692b4d9e2f39433c3752a12 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 21 Jan 2020 08:26:22 -0700 Subject: [PATCH 07/42] Add a MIME-type for .mjs files --- devel/devel-server.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index 0d775dc..31e6f12 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -1,6 +1,5 @@ #!/usr/bin/python3 -import asyncio import cgitb import html import cgi @@ -42,7 +41,15 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): super().__init__(request, client_address, server, directory=server.args["theme_dir"]) except TypeError: super().__init__(request, client_address, server) + # Why can't they just use mimetypes?! + # 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 def translate_path(self, path): @@ -285,10 +292,13 @@ if __name__ == '__main__': logging.basicConfig(level=log_level) + mimetypes.add_type("application/javascript", ".mjs") + server = MothServer((addr, port), MothRequestHandler) server.args["base_url"] = args.base server.args["puzzles_dir"] = pathlib.Path(args.puzzles) server.args["theme_dir"] = args.theme + logging.info("Listening on %s:%d", addr, port) server.serve_forever() From d3389d5bf127a56efeb48efa665802ab2bb170d3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 21 Jan 2020 08:29:08 -0700 Subject: [PATCH 08/42] Remove superfluous comment --- devel/devel-server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index 31e6f12..fd53d12 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -41,7 +41,6 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): super().__init__(request, client_address, server, directory=server.args["theme_dir"]) except TypeError: super().__init__(request, client_address, server) - # Why can't they just use mimetypes?! # Why isn't this the default?! def guess_type(self, path): From f643f7b754e0dbcd176dcfbb6ead9dbedad8e203 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 21 Jan 2020 08:29:25 -0700 Subject: [PATCH 09/42] Remove superfluous newline --- devel/devel-server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index fd53d12..7d7b505 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -297,7 +297,6 @@ if __name__ == '__main__': server.args["base_url"] = args.base server.args["puzzles_dir"] = pathlib.Path(args.puzzles) server.args["theme_dir"] = args.theme - logging.info("Listening on %s:%d", addr, port) server.serve_forever() From 84c9a65aed576ad6e93f6e0081e55ed2431ee4b9 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Wed, 29 Jan 2020 16:36:11 +0000 Subject: [PATCH 10/42] Fixing CHANGELOG link --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12e6b82..c12eb60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Gitfl 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] +6. Update [CHANGELOG.md](CHANGELOG.md) 7. Issue that pull request! ## We Deploy to a Variety of Architectures From d6818dc4090dbe52b7946f9d8f615797654c8a62 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Wed, 29 Jan 2020 20:30:55 +0000 Subject: [PATCH 11/42] Fixing issue with detecting which authors/author field to use --- CHANGELOG.md | 2 ++ devel/moth.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf17fc..1ad1b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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). +### Fixed +- Handle cases where non-legacy puzzles don't have an `author` attribute ## [3.4.3] - 2019-11-20 ### Fixed diff --git a/devel/moth.py b/devel/moth.py index d228b72..886ea14 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -384,7 +384,12 @@ class Puzzle: self.body.write('') 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): return self.body.getvalue() From 3bd34ec5b5943430d0c08701dfe4498d08a7ef31 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Wed, 29 Jan 2020 22:21:58 +0000 Subject: [PATCH 12/42] Fixed handling of multiple files and scripts in YAML-formatted puzzles --- CHANGELOG.md | 3 +++ devel/moth.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf17fc..974ffbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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). +### Fixed +- Handle YAML-formatted file and script lists as expected +- YAML-formatted example puzzle actually works as expected ## [3.4.3] - 2019-11-20 ### Fixed diff --git a/devel/moth.py b/devel/moth.py index d228b72..dc61727 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -233,11 +233,39 @@ class Puzzle: except IndexError: pass self.files[name] = PuzzleFile(stream, name, not hidden) + + elif key == 'files' and isinstance(val, dict): + for filename, options in val.items(): + if "source" in options: + source = options["source"] + else: + source = filename + + if "hidden" in options and options["hidden"]: + hidden = True + else: + 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': stream = open(val, 'rb') # Make sure this shows up in the header block of the HTML output. self.files[val] = PuzzleFile(stream, val, visible=False) self.scripts.append(val) + + elif key == "scripts" and isinstance(val, list): + for script in val: + stream = open(script, "rb") + self.files[script] = PuzzleFile(stream, script, visible=False) + self.scripts.append(script) + elif key == "objective": self.objective = val elif key == "success": From 89d5c7094c2428b12c420638826d3a4940e675c8 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Wed, 29 Jan 2020 23:55:10 +0000 Subject: [PATCH 13/42] Add basic metadata to compiled mothballs and devel server --- CHANGELOG.md | 1 + Dockerfile.moth-devel | 1 + devel/devel-server.py | 1 + devel/moth.py | 4 ++++ devel/mothballer.py | 21 +++++++++++++++++++++ 5 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf17fc..8ca8f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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). +- Include basic metadata in mothballs ## [3.4.3] - 2019-11-20 ### Fixed diff --git a/Dockerfile.moth-devel b/Dockerfile.moth-devel index a667a8e..a3d9e15 100644 --- a/Dockerfile.moth-devel +++ b/Dockerfile.moth-devel @@ -16,6 +16,7 @@ RUN apk --no-cache add \ COPY devel /app/ COPY example-puzzles /puzzles/ COPY theme /theme/ +COPY VERSION /VERSION ENTRYPOINT [ "python3", "/app/devel-server.py" ] CMD [ "--bind", "0.0.0.0:8080", "--puzzles", "/puzzles", "--theme", "/theme" ] diff --git a/devel/devel-server.py b/devel/devel-server.py index 7d7b505..e1e6cae 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -118,6 +118,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): obj["hint"] = puzzle.hint obj["summary"] = puzzle.summary obj["logs"] = puzzle.logs + obj["format"] = puzzle._source_format self.send_response(200) self.send_header("Content-Type", "application/json") diff --git a/devel/moth.py b/devel/moth.py index d228b72..f82e024 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -123,6 +123,8 @@ class Puzzle: super().__init__() + self._source_format = "py" + self.points = points self.summary = None self.authors = [] @@ -153,8 +155,10 @@ class Puzzle: line = "" if stream.read(3) == "---": header = "yaml" + self._source_format = "yaml" else: header = "moth" + self._source_format = "moth" stream.seek(0) diff --git a/devel/mothballer.py b/devel/mothballer.py index 19bd46d..e6afbd9 100755 --- a/devel/mothballer.py +++ b/devel/mothballer.py @@ -2,12 +2,14 @@ import argparse import binascii +import datetime import hashlib import io import json import logging import moth import os +import platform import shutil import tempfile import zipfile @@ -61,6 +63,24 @@ def build_category(categorydir, outdir): zipfileraw.close() 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 def package(categoryname, categorydir, seed): @@ -94,6 +114,7 @@ def package(categoryname, categorydir, seed): write_kv_pairs(zf, 'map.txt', mapping) write_kv_pairs(zf, 'answers.txt', answers) write_kv_pairs(zf, 'summaries.txt', summary) + write_metadata(zf, cat) # clean up zf.close() From 4532e7c6bb9285c2d6691dfdfd95e980925852db Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Thu, 30 Jan 2020 00:03:07 +0000 Subject: [PATCH 14/42] Adding license to Docker images --- Dockerfile.moth | 2 ++ Dockerfile.moth-devel | 1 + 2 files changed, 3 insertions(+) diff --git a/Dockerfile.moth b/Dockerfile.moth index b1fd733..86768d9 100644 --- a/Dockerfile.moth +++ b/Dockerfile.moth @@ -7,4 +7,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o /mo FROM scratch COPY --from=builder /mothd /mothd COPY theme /theme +COPY LICENSE.md /LICENSE + ENTRYPOINT [ "/mothd" ] diff --git a/Dockerfile.moth-devel b/Dockerfile.moth-devel index a667a8e..9bdbd23 100644 --- a/Dockerfile.moth-devel +++ b/Dockerfile.moth-devel @@ -16,6 +16,7 @@ RUN apk --no-cache add \ COPY devel /app/ COPY example-puzzles /puzzles/ COPY theme /theme/ +COPY LICENSE.md /LICENSE ENTRYPOINT [ "python3", "/app/devel-server.py" ] CMD [ "--bind", "0.0.0.0:8080", "--puzzles", "/puzzles", "--theme", "/theme" ] From abb69e788b217fd61d6ea91e2f0479b67afa8170 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 21 Feb 2020 18:28:09 +0000 Subject: [PATCH 15/42] Adding some fixes for @nealey --- CONTRIBUTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c12eb60..a6b819b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Gitfl 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 impracticle to run MOTH anywhere but inside of Docker ## Any contributions you make will be under the MIT Software License -In short, 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. +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! @@ -43,6 +43,9 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue] ## Use a Consistent Coding Style +### Go +* Run it through `gofmt` + ### Javascript * We use Javascript ASI From 5a2a676493ceb74f3cc77edb01aec8fc34252c21 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 21 Feb 2020 21:25:13 +0000 Subject: [PATCH 16/42] @nealey 's spelling is impractical --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6b819b..12867f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Gitfl 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 impracticle to run MOTH anywhere but inside of Docker +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. From 8d8db58f73512c91cab4788dcc1257198ef7c0d2 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 22 Feb 2020 17:25:46 -0600 Subject: [PATCH 17/42] Now you can add scripts from puzzle.py --- devel/moth.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/devel/moth.py b/devel/moth.py index 95c2c5a..0c8af68 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -260,15 +260,12 @@ class Puzzle: elif key == 'script': stream = open(val, 'rb') - # Make sure this shows up in the header block of the HTML output. - self.files[val] = PuzzleFile(stream, val, visible=False) - self.scripts.append(val) + self.add_script_stream(stream, val) elif key == "scripts" and isinstance(val, list): for script in val: stream = open(script, "rb") - self.files[script] = PuzzleFile(stream, script, visible=False) - self.scripts.append(script) + self.add_script_stream(stream, val) elif key == "objective": self.objective = val @@ -322,6 +319,11 @@ class Puzzle: self.add_stream(stream, name, visible) return stream + def add_script_stream(self, stream, name): + # Make sure this shows up in the header block of the HTML output. + self.files[name] = PuzzleFile(stream, name, visible=False) + self.scripts.append(name) + def add_stream(self, stream, name=None, visible=True): if name is None: name = self.random_hash() From adf34e673b9532cafb9deba968b5dbc41c31b838 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Feb 2020 18:55:50 +0000 Subject: [PATCH 18/42] Updating CHANGELOG for 1d307c71a8758d63f7fac23d50457af4e1d03093 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b76b209..485c344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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). - Include basic metadata in mothballs +- add_script_stream convenience function allows easy script addition to puzzle ### Fixed - Handle cases where non-legacy puzzles don't have an `author` attribute - Handle YAML-formatted file and script lists as expected From 9be4387316addedf50e385b6549405ad79423121 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 25 Feb 2020 17:22:39 -0700 Subject: [PATCH 19/42] Add a few things for new CyFi theme --- devel/devel-server.py | 108 +++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index e1e6cae..f08b1c9 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -67,7 +67,27 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): cat = moth.Category(catpath, self.seed) puzzle = cat.puzzle(points) return puzzle + + + def send_json(self, obj): + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(obj).encode("utf-8")) + + def handle_register(self): + # Everybody eats when they come to my house + ret = { + "status": "success", + "data": { + "short": "You win", + "description": "Welcome to the development server, you wily hacker you" + } + } + self.send_json(ret) + endpoints.append(('/{seed}/register', handle_register)) + def handle_answer(self): for f in ("cat", "points", "answer"): @@ -83,17 +103,12 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): if self.req.get("answer") in puzzle.answers: ret["data"]["description"] = "Answer is correct" - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(ret).encode("utf-8")) + self.send_json(ret) endpoints.append(('/{seed}/answer', handle_answer)) - - def handle_puzzlelist(self): - puzzles = { - "__devel__": [[0, ""]], - } + + def puzzlelist(self): + puzzles = {} for p in self.server.args["puzzles_dir"].glob("*"): if not p.is_dir() or p.match(".*"): continue @@ -103,13 +118,28 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): puzzles[catName].append([0, ""]) if len(puzzles) <= 1: logging.warning("No directories found matching {}/*".format(self.server.args["puzzles_dir"])) - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(puzzles).encode("utf-8")) + + return puzzles + + + # XXX: Remove this (redundant) when we've upgraded the bundled theme (probably v3.6 and beyond) + def handle_puzzlelist(self): + self.send_json(self.puzzlelist()) endpoints.append(('/{seed}/puzzles.json', handle_puzzlelist)) + def handle_state(self): + resp = { + "config": { + "devel": True, + }, + "puzzles": self.puzzlelist(), + "messages": "

[MOTH Development Server] Participant broadcast messages would go here.

", + } + self.send_json(resp) + endpoints.append(('/{seed}/state', handle_state)) + + def handle_puzzle(self): puzzle = self.get_puzzle() @@ -120,10 +150,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): obj["logs"] = puzzle.logs obj["format"] = puzzle._source_format - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(obj).encode("utf-8")) + self.send_json(obj) endpoints.append(('/{seed}/content/{cat}/{points}/puzzle.json', handle_puzzle)) @@ -151,7 +178,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): try: catdir = self.server.args["puzzles_dir"].joinpath(category) - mb = mothballer.package(category, catdir, self.seed) + mb = mothballer.package(category, str(catdir), self.seed) except Exception as ex: logging.exception(ex) self.send_response(200) @@ -169,48 +196,11 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): def handle_index(self): seed = random.getrandbits(32) - body = """ - - - Dev Server - - - -

Dev Server

- -

- Pick a seed: -

-
    -
  • {seed}: a special seed I made just for you!
  • -
  • random: will use a different seed every time you load a page (could be useful for debugging)
  • -
  • You can also hack your own seed into the URL, if you want to.
  • -
- -

- Puzzles can be generated from Python code: these puzzles can use a random number generator if they want. - The seed is used to create these random numbers. -

- -

- We like to make a new seed for every contest, - and re-use that seed whenever we regenerate a category during an event - (say to fix a bug). - By using the same seed, - we make sure that all the dynamically-generated puzzles have the same values - in any new packages we build. -

- - -""".format(seed=seed) - - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_response(307) + self.send_header("Location", "%s/" % seed) + self.send_header("Content-Type", "text/html") self.end_headers() - self.wfile.write(body.encode('utf-8')) + self.wfile.write("Your browser was supposed to redirect you to here." % seed) endpoints.append((r"/", handle_index)) From c9b71ac6286cb9144dbba6f1402bebcbca45bcdf Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Feb 2020 21:52:25 +0000 Subject: [PATCH 20/42] Adding workflows that build Docker images and display a badge on the README page --- .github/workflows/docker_build_devel.yml | 12 ++++++++++++ .github/workflows/docker_build_mothd.yml | 12 ++++++++++++ README.md | 8 ++++++++ 3 files changed, 32 insertions(+) create mode 100644 .github/workflows/docker_build_devel.yml create mode 100644 .github/workflows/docker_build_mothd.yml diff --git a/.github/workflows/docker_build_devel.yml b/.github/workflows/docker_build_devel.yml new file mode 100644 index 0000000..e726c91 --- /dev/null +++ b/.github/workflows/docker_build_devel.yml @@ -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 . diff --git a/.github/workflows/docker_build_mothd.yml b/.github/workflows/docker_build_mothd.yml new file mode 100644 index 0000000..1aff1ea --- /dev/null +++ b/.github/workflows/docker_build_mothd.yml @@ -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 . diff --git a/README.md b/README.md index bf443be..7fc26e9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ 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, which in the past has been called "Tracer FIRE", From c55b1c62c3d994db201fa0b1b7bf6c271f36be45 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Feb 2020 21:57:51 +0000 Subject: [PATCH 21/42] Updating changelog with info about autobuild --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c54b27..c6c04b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 ### Fixed - Handle cases where non-legacy puzzles don't have an `author` attribute - Handle YAML-formatted file and script lists as expected From f3beab4e89bf7f0d47c8dfa4d1a0cc70ea8e1408 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Feb 2020 22:49:48 +0000 Subject: [PATCH 22/42] Revert "Add a few things for new CyFi theme" This reverts commit 9be4387316addedf50e385b6549405ad79423121. --- devel/devel-server.py | 108 +++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 49 deletions(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index f08b1c9..e1e6cae 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -67,27 +67,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): cat = moth.Category(catpath, self.seed) puzzle = cat.puzzle(points) return puzzle - - - def send_json(self, obj): - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(obj).encode("utf-8")) - - def handle_register(self): - # Everybody eats when they come to my house - ret = { - "status": "success", - "data": { - "short": "You win", - "description": "Welcome to the development server, you wily hacker you" - } - } - self.send_json(ret) - endpoints.append(('/{seed}/register', handle_register)) - def handle_answer(self): for f in ("cat", "points", "answer"): @@ -103,12 +83,17 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): if self.req.get("answer") in puzzle.answers: ret["data"]["description"] = "Answer is correct" - self.send_json(ret) + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(ret).encode("utf-8")) endpoints.append(('/{seed}/answer', handle_answer)) - - def puzzlelist(self): - puzzles = {} + + def handle_puzzlelist(self): + puzzles = { + "__devel__": [[0, ""]], + } for p in self.server.args["puzzles_dir"].glob("*"): if not p.is_dir() or p.match(".*"): continue @@ -118,28 +103,13 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): puzzles[catName].append([0, ""]) if len(puzzles) <= 1: logging.warning("No directories found matching {}/*".format(self.server.args["puzzles_dir"])) - - return puzzles - - - # XXX: Remove this (redundant) when we've upgraded the bundled theme (probably v3.6 and beyond) - def handle_puzzlelist(self): - self.send_json(self.puzzlelist()) + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(puzzles).encode("utf-8")) endpoints.append(('/{seed}/puzzles.json', handle_puzzlelist)) - def handle_state(self): - resp = { - "config": { - "devel": True, - }, - "puzzles": self.puzzlelist(), - "messages": "

[MOTH Development Server] Participant broadcast messages would go here.

", - } - self.send_json(resp) - endpoints.append(('/{seed}/state', handle_state)) - - def handle_puzzle(self): puzzle = self.get_puzzle() @@ -150,7 +120,10 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): obj["logs"] = puzzle.logs obj["format"] = puzzle._source_format - self.send_json(obj) + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(obj).encode("utf-8")) endpoints.append(('/{seed}/content/{cat}/{points}/puzzle.json', handle_puzzle)) @@ -178,7 +151,7 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): try: catdir = self.server.args["puzzles_dir"].joinpath(category) - mb = mothballer.package(category, str(catdir), self.seed) + mb = mothballer.package(category, catdir, self.seed) except Exception as ex: logging.exception(ex) self.send_response(200) @@ -196,11 +169,48 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): def handle_index(self): seed = random.getrandbits(32) - self.send_response(307) - self.send_header("Location", "%s/" % seed) - self.send_header("Content-Type", "text/html") + body = """ + + + Dev Server + + + +

Dev Server

+ +

+ Pick a seed: +

+
    +
  • {seed}: a special seed I made just for you!
  • +
  • random: will use a different seed every time you load a page (could be useful for debugging)
  • +
  • You can also hack your own seed into the URL, if you want to.
  • +
+ +

+ Puzzles can be generated from Python code: these puzzles can use a random number generator if they want. + The seed is used to create these random numbers. +

+ +

+ We like to make a new seed for every contest, + and re-use that seed whenever we regenerate a category during an event + (say to fix a bug). + By using the same seed, + we make sure that all the dynamically-generated puzzles have the same values + in any new packages we build. +

+ + +""".format(seed=seed) + + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() - self.wfile.write("Your browser was supposed to redirect you to here." % seed) + self.wfile.write(body.encode('utf-8')) endpoints.append((r"/", handle_index)) From 152da3667cb58db82420de02e7188d2b778cd6e1 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 28 Feb 2020 23:14:05 +0000 Subject: [PATCH 23/42] Adding some issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 36 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 22 ++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..29f1990 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve MOTH +labels: bug + +--- + +### Description + + + +### Steps to Reproduce + +1. +2. +3. + +**Expected behavior:** + + + +**Actual behavior:** + + + +**Reproduces how often:** + + + +### Versions + + + +### Additional Information + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2df678b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for MOTH +labels: enhancement + +--- + +## Summary + + + +## Motivation + + + +## Describe alternatives you've considered + + + +## Additional context + + From d0ccdd2a72f5929e2e92a8861cc187474bb1a732 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Mon, 2 Mar 2020 19:23:51 +0000 Subject: [PATCH 24/42] Force points.log to be sorted chronologically --- CHANGELOG.md | 1 + src/instance.go | 10 +++++++++ src/maintenance.go | 54 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c04b2..5eb4eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 diff --git a/src/instance.go b/src/instance.go index 0ec5d8e..49a1f41 100644 --- a/src/instance.go +++ b/src/instance.go @@ -36,6 +36,7 @@ type Instance struct { nextAttempt map[string]time.Time nextAttemptMutex *sync.RWMutex mux *http.ServeMux + PointsMux *sync.RWMutex } func (ctx *Instance) Initialize() error { @@ -53,6 +54,7 @@ func (ctx *Instance) Initialize() error { ctx.nextAttempt = map[string]time.Time{} ctx.nextAttemptMutex = new(sync.RWMutex) ctx.mux = http.NewServeMux() + ctx.PointsMux = new(sync.RWMutex) ctx.BindHandlers() ctx.MaybeInitialize() @@ -82,7 +84,11 @@ func (ctx *Instance) MaybeInitialize() { // Remove any extant control and state files os.Remove(ctx.StatePath("until")) os.Remove(ctx.StatePath("disabled")) + + ctx.PointsMux.Lock() os.Remove(ctx.StatePath("points.log")) + ctx.PointsMux.Unlock() + os.RemoveAll(ctx.StatePath("points.tmp")) os.RemoveAll(ctx.StatePath("points.new")) os.RemoveAll(ctx.StatePath("teams")) @@ -155,6 +161,10 @@ func (ctx *Instance) PointsLog(teamId string) []*Award { var ret []*Award fn := ctx.StatePath("points.log") + + ctx.PointsMux.RLock() + defer ctx.PointsMux.RUnlock() + f, err := os.Open(fn) if err != nil { log.Printf("Unable to open %s: %s", fn, err) diff --git a/src/maintenance.go b/src/maintenance.go index 0174902..f845aad 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -4,9 +4,11 @@ import ( "bufio" "encoding/json" "fmt" + "io" "io/ioutil" "log" "os" + "sort" "strconv" "strings" "time" @@ -95,7 +97,7 @@ func (ctx *Instance) generatePointsLog(teamId string) []byte { log.Printf("Marshalling points.js: %v", err) return nil } - + if len(teamId) == 0 { ctx.jPointsLog = jpl } @@ -203,6 +205,9 @@ func (ctx *Instance) readTeams() { // collectPoints gathers up files in points.new/ and appends their contents to points.log, // removing each points.new/ file as it goes. func (ctx *Instance) collectPoints() { + ctx.PointsMux.Lock() + defer ctx.PointsMux.Unlock() + logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Printf("Can't append to points log: %s", err) @@ -248,6 +253,52 @@ func (ctx *Instance) collectPoints() { } } +// Ensure that points.log is sorted chronologically +func (ctx *Instance) sortPoints() { + var points []*Award + + ctx.PointsMux.Lock() + defer ctx.PointsMux.Unlock() + + logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + log.Printf("Can't sort points.log: %s", err) + return + } + defer logf.Close() + + scanner := bufio.NewScanner(logf) + for scanner.Scan() { + line := scanner.Text() + cur, err := ParseAward(line) + if err != nil { + log.Printf("Encountered malformed award line, not sorting %s: %s", line, err) + return + } + + points = append(points, cur) + } + + // Only sort and write to file if we need to + if ! sort.SliceIsSorted(points, func( i, j int) bool { return points[i].When.Before(points[j].When) }) { + + sort.SliceStable(points, func(i, j int) bool { return points[i].When.Before(points[j].When) }) + + fmt.Println("By time: \n") + + for i := range points { + fmt.Println(points[i]) + } + + logf.Seek(0, io.SeekStart) + + for i := range points { + point := points[i] + fmt.Fprintf(logf, "%s\n", point.String()) + } + } +} + func (ctx *Instance) isEnabled() bool { // Skip if we've been disabled if _, err := os.Stat(ctx.StatePath("disabled")); err == nil { @@ -293,6 +344,7 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) { ctx.tidy() ctx.readTeams() ctx.collectPoints() + ctx.sortPoints() ctx.generatePuzzleList() ctx.generatePointsLog("") } From 2d5e8d6f9f1893a16e28479c7260dd66ee47dbcf Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Mon, 2 Mar 2020 19:32:25 +0000 Subject: [PATCH 25/42] Remove some unneeded debugging code --- src/maintenance.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/maintenance.go b/src/maintenance.go index f845aad..65bb70e 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -284,12 +284,6 @@ func (ctx *Instance) sortPoints() { sort.SliceStable(points, func(i, j int) bool { return points[i].When.Before(points[j].When) }) - fmt.Println("By time: \n") - - for i := range points { - fmt.Println(points[i]) - } - logf.Seek(0, io.SeekStart) for i := range points { From cf72f8f2538e1d712e5b6e12cd2cadbcaf96c8e2 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Thu, 5 Mar 2020 02:19:46 +0000 Subject: [PATCH 26/42] Extract and use X-Forwarded-For headers in mothd logging --- CHANGELOG.md | 1 + src/handlers.go | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c04b2..79da341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 ### Fixed - Handle cases where non-legacy puzzles don't have an `author` attribute - Handle YAML-formatted file and script lists as expected diff --git a/src/handlers.go b/src/handlers.go index a0ceded..9c4256e 100644 --- a/src/handlers.go +++ b/src/handlers.go @@ -339,10 +339,18 @@ func (ctx *Instance) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { w: wOrig, statusCode: new(int), } + + clientIP := r.Header.Get("X-Forwarded-For") + clientIP = strings.Split(clientIP, ", ")[0] + + if clientIP == "" { + clientIP = r.RemoteAddr + } + ctx.mux.ServeHTTP(w, r) log.Printf( "%s %s %s %d\n", - r.RemoteAddr, + clientIP, r.Method, r.URL, *w.statusCode, From d9277ad423b1e006db5e3ccfbb00536ddacd3ea9 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Thu, 5 Mar 2020 03:22:34 +0000 Subject: [PATCH 27/42] Make X-Forwarded-For handling an optional flag --- src/handlers.go | 12 ++++++++---- src/instance.go | 1 + src/mothd.go | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/handlers.go b/src/handlers.go index 9c4256e..7b2d6d2 100644 --- a/src/handlers.go +++ b/src/handlers.go @@ -340,11 +340,15 @@ func (ctx *Instance) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { statusCode: new(int), } - clientIP := r.Header.Get("X-Forwarded-For") - clientIP = strings.Split(clientIP, ", ")[0] + clientIP := r.RemoteAddr - if clientIP == "" { - 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) diff --git a/src/instance.go b/src/instance.go index 0ec5d8e..f446439 100644 --- a/src/instance.go +++ b/src/instance.go @@ -25,6 +25,7 @@ type Instance struct { StateDir string ThemeDir string AttemptInterval time.Duration + UseXForwarded bool Runtime RuntimeConfig diff --git a/src/mothd.go b/src/mothd.go index abf02cb..a61666e 100644 --- a/src/mothd.go +++ b/src/mothd.go @@ -52,6 +52,12 @@ func main() { 20*time.Second, "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", ":8080", From cfa8111dd846cba43f0e56ad11cc96aec7d27e93 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Fri, 6 Mar 2020 00:10:22 +0000 Subject: [PATCH 28/42] Releasing v3.5.1-rc1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 6cb9d3d..d711e9a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.3 +3.5.0-rc1 From 04136ef264fec469e90712313921936d2ba95e03 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 Mar 2020 13:31:38 -0600 Subject: [PATCH 29/42] Support patterned answers --- devel/moth.py | 21 +++++--- example-puzzles/example/4/puzzle.moth | 2 + theme/puzzle.html | 1 + theme/puzzle.js | 78 +++++++++++++++++++-------- 4 files changed, 74 insertions(+), 28 deletions(-) diff --git a/devel/moth.py b/devel/moth.py index 0c8af68..58d17f7 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -22,11 +22,8 @@ messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' LOGGER = logging.getLogger(__name__) -def djb2hash(str): - h = 5381 - for c in str.encode("utf-8"): - h = ((h * 33) + c) & 0xffffffff - return h +def sha256hash(str): + return hashlib.sha256(str.encode("utf-8")).hexdigest() @contextlib.contextmanager def pushd(newdir): @@ -129,6 +126,7 @@ class Puzzle: self.summary = None self.authors = [] self.answers = [] + self.xAnchors = {"begin", "end"} self.scripts = [] self.pattern = None self.hint = None @@ -214,6 +212,16 @@ class Puzzle: if not isinstance(val, str): raise ValueError("Answers must be strings, got %s, instead" % (type(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": for answer in val: if not isinstance(answer, str): @@ -447,12 +455,13 @@ class Puzzle: 'success': self.success, 'solution': self.solution, 'ksas': self.ksas, + 'xAnchors': list(self.xAnchors), } def hashes(self): "Return a list of answer hashes" - return [djb2hash(a) for a in self.answers] + return [sha256hash(a) for a in self.answers] class Category: diff --git a/example-puzzles/example/4/puzzle.moth b/example-puzzles/example/4/puzzle.moth index b021ecc..c06a653 100644 --- a/example-puzzles/example/4/puzzle.moth +++ b/example-puzzles/example/4/puzzle.moth @@ -1,6 +1,8 @@ Summary: Answer patterns Answer: command.com Answer: COMMAND.COM +X-Answer-Pattern: PINBALL.* +X-Answer-Pattern: pinball.* Author: neale Pattern: [0-9A-Za-z]{1,8}\.[A-Za-z]{1,3} diff --git a/theme/puzzle.html b/theme/puzzle.html index 88de8dc..a7be166 100644 --- a/theme/puzzle.html +++ b/theme/puzzle.html @@ -22,6 +22,7 @@
+ Team ID:
Answer:
diff --git a/theme/puzzle.js b/theme/puzzle.js index f0bbba3..23a14df 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -52,16 +52,46 @@ function devel_addin(obj, e) { } - // The routine used to hash answers in compiled puzzle packages -function djb2hash(buf) { - let h = 5381 - 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. - // So we have to do "unsigned right shift" by zero to get it back to unsigned. - h = (((h * 33) + c) & 0xffffffff) >>> 0 +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. + for (let len = 0; len <= answer.length; len += 1) { + if (window.puzzle.xAnchors.includes("end") && (len != answer.length)) { + console.log(` Skipping unanchored end (len=${len})`) + continue + } + console.log(" What about length", len) + for (let pos = 0; pos < answer.length - len + 1; pos += 1) { + if (window.puzzle.xAnchors.includes("begin") && (pos > 0)) { + console.log(` Skipping unanchored begin (pos=${pos})`) + continue + } + let sub = answer.substring(pos, pos+len) + let digest = await sha256Hash(sub) + + console.log(" Could it be", sub, digest) + if (digest == correctHash) { + console.log(" YESSS") + return sub + } + } + } } - return h + return false } @@ -80,6 +110,14 @@ function toast(message, timeout=5000) { // When the user submits an answer function submit(e) { 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", { method: "POST", body: new FormData(e.target), @@ -180,21 +218,17 @@ function answerCheck(e) { return } - let possiblyCorrect = false - let answerHash = djb2hash(answer) - for (let correctHash of window.puzzle.hashes) { - if (correctHash == answerHash) { - possiblyCorrect = true + possiblyCorrect(answer) + .then (correct => { + if (correct) { + document.querySelector("[name=xAnswer").value = correct + 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() { From a963daa19aebbac8a074b0a954fef7086fa983cd Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 Mar 2020 14:28:50 -0600 Subject: [PATCH 30/42] submit right form, remove debugging --- devel/devel-server.py | 4 ++-- theme/puzzle.js | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/devel/devel-server.py b/devel/devel-server.py index e1e6cae..1c91c00 100755 --- a/devel/devel-server.py +++ b/devel/devel-server.py @@ -77,12 +77,12 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): "status": "success", "data": { "short": "", - "description": "Provided answer was not in list of answers" + "description": "%r was not in list of answers" % self.req.get("answer") }, } if self.req.get("answer") in puzzle.answers: - ret["data"]["description"] = "Answer is correct" + ret["data"]["description"] = "Answer %r is correct" % self.req.get("answer") self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() diff --git a/theme/puzzle.js b/theme/puzzle.js index 23a14df..6eb5896 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -71,21 +71,16 @@ async function possiblyCorrect(answer) { // something like the github 404 page. for (let len = 0; len <= answer.length; len += 1) { if (window.puzzle.xAnchors.includes("end") && (len != answer.length)) { - console.log(` Skipping unanchored end (len=${len})`) continue } - console.log(" What about length", len) for (let pos = 0; pos < answer.length - len + 1; pos += 1) { if (window.puzzle.xAnchors.includes("begin") && (pos > 0)) { - console.log(` Skipping unanchored begin (pos=${pos})`) continue } let sub = answer.substring(pos, pos+len) let digest = await sha256Hash(sub) - console.log(" Could it be", sub, digest) if (digest == correctHash) { - console.log(" YESSS") return sub } } @@ -120,7 +115,7 @@ function submit(e) { window.data = data fetch("answer", { method: "POST", - body: new FormData(e.target), + body: data, }) .then(resp => { if (resp.ok) { From dac6fd40e76c5240042fa0ec01f560eb217ed6bc Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 Mar 2020 14:58:43 -0600 Subject: [PATCH 31/42] Change changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc955c..10bd406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### 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). @@ -13,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 From fe0805e9c1cb1e081ffb9513ba0c37bf709b04ea Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 Mar 2020 15:03:32 -0600 Subject: [PATCH 32/42] Also set xAnswer for wrong answers --- theme/puzzle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/puzzle.js b/theme/puzzle.js index 6eb5896..7ffbd61 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -215,8 +215,8 @@ function answerCheck(e) { possiblyCorrect(answer) .then (correct => { + document.querySelector("[name=xAnswer").value = correct || answer if (correct) { - document.querySelector("[name=xAnswer").value = correct ok.textContent = "⭕" ok.title = "Possibly correct" } else { From baafdd27a2f1e506860e7797bbdd332bfdac5160 Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Tue, 10 Mar 2020 21:29:30 +0000 Subject: [PATCH 33/42] Fixing a dynamic grading bug Fixing a script stream bug --- devel/moth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devel/moth.py b/devel/moth.py index 0c8af68..420b272 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -30,6 +30,7 @@ def djb2hash(str): @contextlib.contextmanager def pushd(newdir): + newdir = str(newdir) curdir = os.getcwd() LOGGER.debug("Attempting to chdir from %s to %s" % (curdir, newdir)) os.chdir(newdir) @@ -265,7 +266,7 @@ class Puzzle: elif key == "scripts" and isinstance(val, list): for script in val: stream = open(script, "rb") - self.add_script_stream(stream, val) + self.add_script_stream(stream, script) elif key == "objective": self.objective = val From 47edd56937ec5202b5193e1bec63fbdd1d191810 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 Mar 2020 16:40:37 -0600 Subject: [PATCH 34/42] Attempt to make client work with v3.4 and earlier mothballs --- theme/puzzle.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/theme/puzzle.js b/theme/puzzle.js index 7ffbd61..868bc80 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -51,6 +51,16 @@ function devel_addin(obj, e) { } } +// Hash routine used in v3.4 and earlier +function djb2hash(buf) { + let h = 5381 + 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. + // So we have to do "unsigned right shift" by zero to get it back to unsigned. + h = (((h * 33) + c) & 0xffffffff) >>> 0 + } + return h +} // The routine used to hash answers in compiled puzzle packages async function sha256Hash(message) { @@ -69,6 +79,10 @@ async function possiblyCorrect(answer) { // 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 len = 0; len <= answer.length; len += 1) { if (window.puzzle.xAnchors.includes("end") && (len != answer.length)) { continue From b896969d80a350ed3a019508d5d3d613fd24a528 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 10 Mar 2020 20:50:03 -0600 Subject: [PATCH 35/42] fix for #107 that doesn't require a mutex I didn't look into why the mutex was causing some sort of deadlock condition on retrieving points. This fixes it, as long as the underlying filesystem supports atomic file renames. So don't run this with NFS. --- src/award.go | 18 +++++++++++ src/award_test.go | 21 ++++++++++++ src/instance.go | 20 ++++-------- src/maintenance.go | 81 ++++++++++++++++++---------------------------- 4 files changed, 76 insertions(+), 64 deletions(-) diff --git a/src/award.go b/src/award.go index 4a8ba75..e5dc17b 100644 --- a/src/award.go +++ b/src/award.go @@ -14,6 +14,24 @@ type Award struct { 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) { ret := Award{} diff --git a/src/award_test.go b/src/award_test.go index 2875557..9ac9097 100644 --- a/src/award_test.go +++ b/src/award_test.go @@ -2,6 +2,7 @@ package main import ( "testing" + "sort" ) func TestAward(t *testing.T) { @@ -32,3 +33,23 @@ func TestAward(t *testing.T) { 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") + } +} diff --git a/src/instance.go b/src/instance.go index bdb9fdd..4c5c876 100644 --- a/src/instance.go +++ b/src/instance.go @@ -37,7 +37,6 @@ type Instance struct { nextAttempt map[string]time.Time nextAttemptMutex *sync.RWMutex mux *http.ServeMux - PointsMux *sync.RWMutex } func (ctx *Instance) Initialize() error { @@ -55,7 +54,6 @@ func (ctx *Instance) Initialize() error { ctx.nextAttempt = map[string]time.Time{} ctx.nextAttemptMutex = new(sync.RWMutex) ctx.mux = http.NewServeMux() - ctx.PointsMux = new(sync.RWMutex) ctx.BindHandlers() ctx.MaybeInitialize() @@ -85,10 +83,7 @@ func (ctx *Instance) MaybeInitialize() { // Remove any extant control and state files os.Remove(ctx.StatePath("until")) os.Remove(ctx.StatePath("disabled")) - - ctx.PointsMux.Lock() os.Remove(ctx.StatePath("points.log")) - ctx.PointsMux.Unlock() os.RemoveAll(ctx.StatePath("points.tmp")) os.RemoveAll(ctx.StatePath("points.new")) @@ -158,18 +153,15 @@ func (ctx *Instance) TooFast(teamId string) bool { return now.Before(next) } -func (ctx *Instance) PointsLog(teamId string) []*Award { - var ret []*Award - +func (ctx *Instance) PointsLog(teamId string) AwardList { + awardlist := AwardList{} + fn := ctx.StatePath("points.log") - ctx.PointsMux.RLock() - defer ctx.PointsMux.RUnlock() - f, err := os.Open(fn) if err != nil { log.Printf("Unable to open %s: %s", fn, err) - return ret + return awardlist } defer f.Close() @@ -184,10 +176,10 @@ func (ctx *Instance) PointsLog(teamId string) []*Award { if len(teamId) > 0 && cur.TeamId != teamId { continue } - ret = append(ret, cur) + awardlist = append(awardlist, cur) } - return ret + return awardlist } // AwardPoints gives points to teamId in category. diff --git a/src/maintenance.go b/src/maintenance.go index 65bb70e..829c2fc 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -4,7 +4,6 @@ import ( "bufio" "encoding/json" "fmt" - "io" "io/ioutil" "log" "os" @@ -205,20 +204,26 @@ func (ctx *Instance) readTeams() { // collectPoints gathers up files in points.new/ and appends their contents to points.log, // removing each points.new/ file as it goes. func (ctx *Instance) collectPoints() { - ctx.PointsMux.Lock() - defer ctx.PointsMux.Unlock() + points := ctx.PointsLog("") - logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + 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 { log.Printf("Can't append to points log: %s", err) return } - defer logf.Close() files, err := ioutil.ReadDir(ctx.StatePath("points.new")) if err != nil { log.Printf("Error reading packages: %s", err) } + removearino := make([]string, 0, len(files)) for _, f := range files { filename := ctx.StatePath("points.new", f.Name()) s, err := ioutil.ReadFile(filename) @@ -233,7 +238,7 @@ func (ctx *Instance) collectPoints() { } duplicate := false - for _, e := range ctx.PointsLog("") { + for _, e := range points { if award.Same(e) { duplicate = true break @@ -243,53 +248,30 @@ func (ctx *Instance) collectPoints() { if duplicate { log.Printf("Skipping duplicate points: %s", award.String()) } else { - fmt.Fprintf(logf, "%s\n", award.String()) + points = append(points, award) } - - logf.Sync() - if err := os.Remove(filename); err != nil { - log.Printf("Unable to remove %s: %s", filename, err) - } - } -} - -// Ensure that points.log is sorted chronologically -func (ctx *Instance) sortPoints() { - var points []*Award - - ctx.PointsMux.Lock() - defer ctx.PointsMux.Unlock() - - logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_CREATE|os.O_RDWR, 0644) - if err != nil { - log.Printf("Can't sort points.log: %s", err) - return - } - defer logf.Close() - - scanner := bufio.NewScanner(logf) - for scanner.Scan() { - line := scanner.Text() - cur, err := ParseAward(line) - if err != nil { - log.Printf("Encountered malformed award line, not sorting %s: %s", line, err) - return - } - - points = append(points, cur) + removearino = append(removearino, filename) } - // Only sort and write to file if we need to - if ! sort.SliceIsSorted(points, func( i, j int) bool { return points[i].When.Before(points[j].When) }) { + sort.Stable(points) + for _, point := range points { + 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 + } - sort.SliceStable(points, func(i, j int) bool { return points[i].When.Before(points[j].When) }) - - logf.Seek(0, io.SeekStart) - - for i := range points { - point := points[i] - fmt.Fprintf(logf, "%s\n", point.String()) - } + for _, filename := range removearino { + if err := os.Remove(filename); err != nil { + log.Printf("Unable to remove %s: %s", filename, err) + } } } @@ -338,7 +320,6 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) { ctx.tidy() ctx.readTeams() ctx.collectPoints() - ctx.sortPoints() ctx.generatePuzzleList() ctx.generatePointsLog("") } From 989bc2978e58aee743b3fc7305a0c068351a9e56 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 11 Mar 2020 16:19:37 -0600 Subject: [PATCH 36/42] Fix for *pattern --- example-puzzles/example/2/puzzle.moth | 1 + theme/puzzle.js | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/example-puzzles/example/2/puzzle.moth b/example-puzzles/example/2/puzzle.moth index 2576b31..50d7918 100644 --- a/example-puzzles/example/2/puzzle.moth +++ b/example-puzzles/example/2/puzzle.moth @@ -3,6 +3,7 @@ Summary: Static puzzle resource files File: salad.jpg s.jpg File: salad2.jpg s2.jpg hidden Answer: salad +X-Answer-Pattern: *pong You can include additional resources in a static puzzle, by dropping them in the directory and listing them in a `File:` header field. diff --git a/theme/puzzle.js b/theme/puzzle.js index 868bc80..2c084c6 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -83,15 +83,15 @@ async function possiblyCorrect(answer) { if (djb2hash(answer) == correctHash) { return answer } - for (let len = 0; len <= answer.length; len += 1) { - if (window.puzzle.xAnchors.includes("end") && (len != answer.length)) { + for (let end = 0; end <= answer.length; end += 1) { + if (window.puzzle.xAnchors.includes("end") && (end != answer.length)) { continue } - for (let pos = 0; pos < answer.length - len + 1; pos += 1) { - if (window.puzzle.xAnchors.includes("begin") && (pos > 0)) { + for (let beg = 0; beg < answer.length; beg += 1) { + if (window.puzzle.xAnchors.includes("begin") && (beg != 0)) { continue } - let sub = answer.substring(pos, pos+len) + let sub = answer.substring(beg, end) let digest = await sha256Hash(sub) if (digest == correctHash) { From 1b8053d9b5c93b11f74f5be3883bdd91c47a549f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 11 Mar 2020 17:35:46 -0600 Subject: [PATCH 37/42] Help with this puzzle link --- theme/basic.css | 9 +++++++++ theme/puzzle.html | 1 + theme/puzzle.js | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/theme/basic.css b/theme/basic.css index e7bb81e..721eec5 100644 --- a/theme/basic.css +++ b/theme/basic.css @@ -140,3 +140,12 @@ li[draggable] { #cacheButton.disabled { display: none; } + +#help { + text-decoration: none; + color: #fff; + background-color: #118; + padding: 0.25em; + border: 1px solid black; + font-size: 120%; +} \ No newline at end of file diff --git a/theme/puzzle.html b/theme/puzzle.html index a7be166..6ef271a 100644 --- a/theme/puzzle.html +++ b/theme/puzzle.html @@ -25,6 +25,7 @@ Team ID:
Answer:
+
diff --git a/theme/puzzle.js b/theme/puzzle.js index 2c084c6..920e271 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -151,6 +151,12 @@ function submit(e) { function loadPuzzle(categoryName, points, puzzleId) { let puzzle = document.getElementById("puzzle") let base = "content/" + categoryName + "/" + puzzleId + "/" + + let helpElement = document.querySelector("#help") + if (helpElement) { + helpElement.classList.remove("hidden") + helpElement.href = "https://mattermost.cyfi.training/cyber-fire/channels/puzzle-" + categoryName + } fetch(base + "puzzle.json") .then(resp => { From 59f98617ed70f5980e3f5796245c74e98f1a6073 Mon Sep 17 00:00:00 2001 From: Donaldson Date: Thu, 12 Mar 2020 10:35:38 -0500 Subject: [PATCH 38/42] Releasing v3.5.0-rc2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d711e9a..eb0863c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0-rc1 +3.5.0-rc2 From 22abc1f84896ea2ec0b5997c2d8092c96716452f Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Thu, 12 Mar 2020 16:26:52 +0000 Subject: [PATCH 39/42] Revert "Help with this puzzle link" This reverts commit 1b8053d9b5c93b11f74f5be3883bdd91c47a549f. --- theme/basic.css | 9 --------- theme/puzzle.html | 1 - theme/puzzle.js | 6 ------ 3 files changed, 16 deletions(-) diff --git a/theme/basic.css b/theme/basic.css index 721eec5..e7bb81e 100644 --- a/theme/basic.css +++ b/theme/basic.css @@ -140,12 +140,3 @@ li[draggable] { #cacheButton.disabled { display: none; } - -#help { - text-decoration: none; - color: #fff; - background-color: #118; - padding: 0.25em; - border: 1px solid black; - font-size: 120%; -} \ No newline at end of file diff --git a/theme/puzzle.html b/theme/puzzle.html index 6ef271a..a7be166 100644 --- a/theme/puzzle.html +++ b/theme/puzzle.html @@ -25,7 +25,6 @@ Team ID:
Answer:
-
diff --git a/theme/puzzle.js b/theme/puzzle.js index 920e271..2c084c6 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -151,12 +151,6 @@ function submit(e) { function loadPuzzle(categoryName, points, puzzleId) { let puzzle = document.getElementById("puzzle") let base = "content/" + categoryName + "/" + puzzleId + "/" - - let helpElement = document.querySelector("#help") - if (helpElement) { - helpElement.classList.remove("hidden") - helpElement.href = "https://mattermost.cyfi.training/cyber-fire/channels/puzzle-" + categoryName - } fetch(base + "puzzle.json") .then(resp => { From 79397a88614b635f68fd4a7520b9dbcaf2c631bf Mon Sep 17 00:00:00 2001 From: John Donaldson Date: Thu, 12 Mar 2020 16:27:34 +0000 Subject: [PATCH 40/42] Releasing v3.5.0-rc3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index eb0863c..1c146fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0-rc2 +3.5.0-rc3 From db08e522f7976435d23556abbc24736769301438 Mon Sep 17 00:00:00 2001 From: Donaldson Date: Fri, 13 Mar 2020 16:04:34 -0500 Subject: [PATCH 41/42] Rev v3.5.0 for release --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1c146fb..1545d96 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0-rc3 +3.5.0 From 25c3bdc72cab6ccd8d64acac4fd38472da0ba174 Mon Sep 17 00:00:00 2001 From: Donaldson Date: Fri, 13 Mar 2020 16:14:43 -0500 Subject: [PATCH 42/42] Updating changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10bd406..c73e46e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [v3.5.0] - 2020-03-13 ### Changed - We are now using SHA256 instead of djb2hash ### Added