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 + + 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/CHANGELOG.md b/CHANGELOG.md index c99e9f3..10bd406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ 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). +- 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..12867f4 --- /dev/null +++ b/CONTRIBUTING.md @@ -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] 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..c1ca075 100644 --- a/Dockerfile.moth-devel +++ b/Dockerfile.moth-devel @@ -16,6 +16,8 @@ RUN apk --no-cache add \ COPY devel /app/ COPY example-puzzles /puzzles/ COPY theme /theme/ +COPY LICENSE.md /LICENSE +COPY VERSION /VERSION ENTRYPOINT [ "python3", "/app/devel-server.py" ] CMD [ "--bind", "0.0.0.0:8080", "--puzzles", "/puzzles", "--theme", "/theme" ] diff --git a/README.md b/README.md index 1cfbfc8..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", @@ -161,4 +169,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) diff --git a/VERSION b/VERSION index 6cb9d3d..1545d96 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.3 +3.5.0 diff --git a/devel/devel-server.py b/devel/devel-server.py index 0d775dc..1c91c00 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 @@ -43,6 +42,13 @@ class MothRequestHandler(http.server.SimpleHTTPRequestHandler): except TypeError: 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 def translate_path(self, path): @@ -71,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() @@ -112,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") @@ -285,6 +292,8 @@ 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) diff --git a/devel/moth.py b/devel/moth.py index d228b72..0d9c411 100644 --- a/devel/moth.py +++ b/devel/moth.py @@ -22,14 +22,12 @@ 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): + newdir = str(newdir) curdir = os.getcwd() LOGGER.debug("Attempting to chdir from %s to %s" % (curdir, newdir)) os.chdir(newdir) @@ -123,10 +121,13 @@ class Puzzle: super().__init__() + self._source_format = "py" + self.points = points self.summary = None self.authors = [] self.answers = [] + self.xAnchors = {"begin", "end"} self.scripts = [] self.pattern = None self.hint = None @@ -153,8 +154,10 @@ class Puzzle: line = "" if stream.read(3) == "---": header = "yaml" + self._source_format = "yaml" else: header = "moth" + self._source_format = "moth" stream.seek(0) @@ -210,6 +213,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): @@ -233,11 +246,36 @@ 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) + 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": self.objective = val elif key == "success": @@ -290,6 +328,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() @@ -384,7 +427,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() @@ -408,12 +456,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/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() 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/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/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/handlers.go b/src/handlers.go index 00eca05..7b2d6d2 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) { @@ -334,10 +339,22 @@ func (ctx *Instance) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { w: wOrig, 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) log.Printf( "%s %s %s %d\n", - r.RemoteAddr, + clientIP, r.Method, r.URL, *w.statusCode, diff --git a/src/instance.go b/src/instance.go index a415b40..4c5c876 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 @@ -83,6 +84,7 @@ func (ctx *Instance) MaybeInitialize() { os.Remove(ctx.StatePath("until")) os.Remove(ctx.StatePath("disabled")) os.Remove(ctx.StatePath("points.log")) + os.RemoveAll(ctx.StatePath("points.tmp")) os.RemoveAll(ctx.StatePath("points.new")) os.RemoveAll(ctx.StatePath("teams")) @@ -151,14 +153,15 @@ func (ctx *Instance) TooFast(teamId string) bool { return now.Before(next) } -func (ctx *Instance) PointsLog() []*Award { - var ret []*Award - +func (ctx *Instance) PointsLog(teamId string) AwardList { + awardlist := AwardList{} + fn := ctx.StatePath("points.log") + f, err := os.Open(fn) if err != nil { log.Printf("Unable to open %s: %s", fn, err) - return ret + return awardlist } defer f.Close() @@ -170,10 +173,13 @@ func (ctx *Instance) PointsLog() []*Award { log.Printf("Skipping malformed award line %s: %s", line, err) 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. @@ -194,7 +200,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..829c2fc 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "log" "os" + "sort" "strconv" "strings" "time" @@ -28,7 +29,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 +68,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 +94,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 @@ -199,17 +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() { - 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 { 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) @@ -224,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 @@ -234,13 +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) } + removearino = append(removearino, filename) + } - logf.Sync() - if err := os.Remove(filename); err != nil { - log.Printf("Unable to remove %s: %s", filename, err) - } + 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 + } + + for _, filename := range removearino { + if err := os.Remove(filename); err != nil { + log.Printf("Unable to remove %s: %s", filename, err) + } } } @@ -290,7 +321,7 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) { ctx.readTeams() ctx.collectPoints() ctx.generatePuzzleList() - ctx.generatePointsLog() + ctx.generatePointsLog("") } select { case <-ctx.update: 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", 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 @@