From 854ef771b436388644d466f1e09e119f111da5dc Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 12 Oct 2020 17:44:44 -0600 Subject: [PATCH 01/10] transpiler can now create mothballs --- cmd/mothd/httpd.go | 8 ++-- cmd/mothd/mothballs.go | 5 +-- cmd/mothd/server.go | 15 ++++---- cmd/mothd/transpiler.go | 4 +- cmd/transpile/main.go | 41 ++++++++++++-------- cmd/transpile/main_test.go | 70 +++++++++++++++++++++++++++++++--- pkg/transpile/mothball.go | 46 ++++++++++++---------- pkg/transpile/mothball_test.go | 12 ++++-- 8 files changed, 141 insertions(+), 60 deletions(-) diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 66eb6f4..7962fbe 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "log" "net/http" "strconv" @@ -171,11 +172,12 @@ func (h *HTTPServer) MothballerHandler(mh MothRequestHandler, w http.ResponseWri // parts[0] == "mothballer" filename := parts[1] cat := strings.TrimSuffix(filename, ".mb") - mothball, err := mh.Mothball(cat) - if err != nil { + mb := new(bytes.Buffer) + if err := mh.Mothball(cat, mb); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - http.ServeContent(w, req, filename, time.Now(), mothball) + mbReader := bytes.NewReader(mb.Bytes()) + http.ServeContent(w, req, filename, time.Now(), mbReader) } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index deab589..6c3ada4 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -3,7 +3,6 @@ package main import ( "archive/zip" "bufio" - "bytes" "fmt" "io" "log" @@ -174,8 +173,8 @@ func (m *Mothballs) refresh() { } // Mothball just returns an error -func (m *Mothballs) Mothball(cat string) (*bytes.Reader, error) { - return nil, fmt.Errorf("Can't repackage a compiled mothball") +func (m *Mothballs) Mothball(cat string, w io.Writer) error { + return fmt.Errorf("Refusing to repackage a compiled mothball") } // Maintain performs housekeeping for Mothballs. diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index a355cf7..35619bc 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "fmt" "io" "strconv" @@ -42,7 +41,7 @@ type PuzzleProvider interface { Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error) Inventory() []Category CheckAnswer(cat string, points int, answer string) (bool, error) - Mothball(cat string) (*bytes.Reader, error) + Mothball(cat string, w io.Writer) error Maintainer } @@ -233,14 +232,16 @@ func (mh *MothRequestHandler) ExportState() *StateExport { } // Mothball generates a mothball for the given category. -func (mh *MothRequestHandler) Mothball(cat string) (r *bytes.Reader, err error) { +func (mh *MothRequestHandler) Mothball(cat string, w io.Writer) error { + var err error + if !mh.Config.Devel { - return nil, fmt.Errorf("Cannot mothball in production mode") + return fmt.Errorf("Cannot mothball in production mode") } for _, provider := range mh.PuzzleProviders { - if r, err = provider.Mothball(cat); err == nil { - return r, nil + if err = provider.Mothball(cat, w); err == nil { + return nil } } - return nil, err + return err } diff --git a/cmd/mothd/transpiler.go b/cmd/mothd/transpiler.go index ddda86b..7103b20 100644 --- a/cmd/mothd/transpiler.go +++ b/cmd/mothd/transpiler.go @@ -70,9 +70,9 @@ func (p TranspilerProvider) CheckAnswer(cat string, points int, answer string) ( } // Mothball packages up a category into a mothball. -func (p TranspilerProvider) Mothball(cat string) (*bytes.Reader, error) { +func (p TranspilerProvider) Mothball(cat string, w io.Writer) error { c := transpile.NewFsCategory(p.fs, cat) - return transpile.Mothball(c) + return transpile.Mothball(c, w) } // Maintain performs housekeeping. diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index 4c95343..34cec5b 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -16,11 +16,13 @@ import ( // T represents the state of things type T struct { - Stdout io.Writer - Stderr io.Writer - Args []string - BaseFs afero.Fs - fs afero.Fs + Stdout io.Writer + Stderr io.Writer + Args []string + BaseFs afero.Fs + fs afero.Fs + + // Arguments filename string answer string } @@ -51,19 +53,21 @@ func (t *T) ParseArgs() (Command, error) { } flags := flag.NewFlagSet(t.Args[1], flag.ContinueOnError) + flags.SetOutput(t.Stderr) directory := flags.String("dir", "", "Work directory") switch t.Args[1] { case "mothball": cmd = t.DumpMothball + flags.StringVar(&t.filename, "out", "", "Path to create mothball (empty for stdout)") case "inventory": cmd = t.PrintInventory case "open": - flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open") cmd = t.DumpFile + flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open") case "answer": - flags.StringVar(&t.answer, "answer", "", "Answer to check") cmd = t.CheckAnswer + flags.StringVar(&t.answer, "answer", "", "Answer to check") case "help": usage(t.Stderr) return nothing, nil @@ -73,7 +77,6 @@ func (t *T) ParseArgs() (Command, error) { return nothing, fmt.Errorf("Invalid command") } - flags.SetOutput(t.Stderr) if err := flags.Parse(t.Args[2:]); err != nil { return nothing, err } @@ -140,14 +143,24 @@ func (t *T) DumpFile() error { return nil } -// DumpMothball writes a mothball to the writer. +// DumpMothball writes a mothball to the writer, or an output file if specified. func (t *T) DumpMothball() error { + var w io.Writer + c := transpile.NewFsCategory(t.fs, "") - mb, err := transpile.Mothball(c) - if err != nil { - return err + if t.filename == "" { + w = t.Stdout + } else { + log.Println("Writing to", t.filename, t.fs) + outf, err := t.BaseFs.Create(t.filename) + if err != nil { + return err + } + defer outf.Close() + w = outf } - if _, err := io.Copy(t.Stdout, mb); err != nil { + log.Println(t.fs) + if err := transpile.Mothball(c, w); err != nil { return err } return nil @@ -165,8 +178,6 @@ func (t *T) CheckAnswer() error { } func main() { - // XXX: Convert puzzle.py to standalone thingies - t := &T{ Stdout: os.Stdout, Stderr: os.Stderr, diff --git a/cmd/transpile/main_test.go b/cmd/transpile/main_test.go index ad79cef..52e1a1e 100644 --- a/cmd/transpile/main_test.go +++ b/cmd/transpile/main_test.go @@ -1,8 +1,10 @@ package main import ( + "archive/zip" "bytes" "encoding/json" + "io/ioutil" "testing" "github.com/dirtbags/moth/pkg/transpile" @@ -83,16 +85,72 @@ func TestEverything(t *testing.T) { if stdout.String() != "Moo." { t.Error("Wrong file pulled", stdout.String()) } +} + +func TestMothballs(t *testing.T) { + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + tp := T{ + Stdout: stdout, + Stderr: stderr, + BaseFs: newTestFs(), + } stdout.Reset() - if err := tp.Run("mothball", "-dir=unbroken"); err != nil { - t.Log(tp.fs) + if err := tp.Run("mothball", "-dir=unbroken", "-out=unbroken.mb"); err != nil { + t.Error(err) + return + } + + // afero.WriteFile(tp.BaseFs, "unbroken.mb", []byte("moo"), 0644) + fis, err := afero.ReadDir(tp.BaseFs, "/") + if err != nil { t.Error(err) } - if stdout.Len() < 200 { - t.Error("That's way too short to be a mothball") + for _, fi := range fis { + t.Log(fi.Name()) } - if stdout.String()[:2] != "PK" { - t.Error("This mothball isn't a zip file!") + + mb, err := tp.BaseFs.Open("unbroken.mb") + if err != nil { + t.Error(err) + return + } + defer mb.Close() + + info, err := mb.Stat() + if err != nil { + t.Error(err) + return + } + + zmb, err := zip.NewReader(mb, info.Size()) + if err != nil { + t.Error(err) + return + } + for _, zf := range zmb.File { + f, err := zf.Open() + if err != nil { + t.Error(err) + continue + } + defer f.Close() + buf, err := ioutil.ReadAll(f) + if err != nil { + t.Error(err) + continue + } + + switch zf.Name { + case "answers.txt": + if len(buf) == 0 { + t.Error("answers.txt empty") + } + case "puzzles.txt": + if len(buf) == 0 { + t.Error("puzzles.txt empty") + } + } } } diff --git a/pkg/transpile/mothball.go b/pkg/transpile/mothball.go index b393802..6647d5d 100644 --- a/pkg/transpile/mothball.go +++ b/pkg/transpile/mothball.go @@ -10,23 +10,16 @@ import ( ) // Mothball packages a Category up for a production server run. -func Mothball(c Category) (*bytes.Reader, error) { - buf := new(bytes.Buffer) - zf := zip.NewWriter(buf) +func Mothball(c Category, w io.Writer) error { + zf := zip.NewWriter(w) inv, err := c.Inventory() if err != nil { - return nil, err + return err } - puzzlesTxt, err := zf.Create("puzzles.txt") - if err != nil { - return nil, err - } - answersTxt, err := zf.Create("answers.txt") - if err != nil { - return nil, err - } + puzzlesTxt := new(bytes.Buffer) + answersTxt := new(bytes.Buffer) for _, points := range inv { fmt.Fprintln(puzzlesTxt, points) @@ -34,11 +27,11 @@ func Mothball(c Category) (*bytes.Reader, error) { puzzlePath := fmt.Sprintf("%d/puzzle.json", points) pw, err := zf.Create(puzzlePath) if err != nil { - return nil, err + return err } puzzle, err := c.Puzzle(points) if err != nil { - return nil, fmt.Errorf("Puzzle %d: %s", points, err) + return fmt.Errorf("Puzzle %d: %s", points, err) } // Record answers in answers.txt @@ -55,7 +48,7 @@ func Mothball(c Category) (*bytes.Reader, error) { // Write out Puzzle object penc := json.NewEncoder(pw) if err := penc.Encode(puzzle); err != nil { - return nil, fmt.Errorf("Puzzle %d: %s", points, err) + return fmt.Errorf("Puzzle %d: %s", points, err) } // Write out all attachments and scripts @@ -64,20 +57,33 @@ func Mothball(c Category) (*bytes.Reader, error) { attPath := fmt.Sprintf("%d/%s", points, att) aw, err := zf.Create(attPath) if err != nil { - return nil, err + return err } ar, err := c.Open(points, att) if exerr, ok := err.(*exec.ExitError); ok { - return nil, fmt.Errorf("Puzzle %d: %s: %s: %s", points, att, err, string(exerr.Stderr)) + return fmt.Errorf("Puzzle %d: %s: %s: %s", points, att, err, string(exerr.Stderr)) } else if err != nil { - return nil, fmt.Errorf("Puzzle %d: %s: %s", points, att, err) + return fmt.Errorf("Puzzle %d: %s: %s", points, att, err) } if _, err := io.Copy(aw, ar); err != nil { - return nil, fmt.Errorf("Puzzle %d: %s: %s", points, att, err) + return fmt.Errorf("Puzzle %d: %s: %s", points, att, err) } } } + + pf, err := zf.Create("puzzles.txt") + if err != nil { + return err + } + puzzlesTxt.WriteTo(pf) + + af, err := zf.Create("answers.txt") + if err != nil { + return err + } + answersTxt.WriteTo(af) + zf.Close() - return bytes.NewReader(buf.Bytes()), nil + return nil } diff --git a/pkg/transpile/mothball_test.go b/pkg/transpile/mothball_test.go index 753d536..8865b6f 100644 --- a/pkg/transpile/mothball_test.go +++ b/pkg/transpile/mothball_test.go @@ -2,6 +2,7 @@ package transpile import ( "archive/zip" + "bytes" "io/ioutil" "os" "path" @@ -14,7 +15,8 @@ import ( func TestMothballsMemFs(t *testing.T) { static := NewFsCategory(newTestFs(), "cat1") - if _, err := Mothball(static); err != nil { + mb := new(bytes.Buffer) + if err := Mothball(static, mb); err != nil { t.Error(err) } } @@ -25,13 +27,15 @@ func TestMothballsOsFs(t *testing.T) { fs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata") static := NewFsCategory(fs, "static") - mb, err := Mothball(static) + mb := new(bytes.Buffer) + err := Mothball(static, mb) if err != nil { t.Error(err) return } - mbr, err := zip.NewReader(mb, int64(mb.Len())) + mbReader := bytes.NewReader(mb.Bytes()) + mbr, err := zip.NewReader(mbReader, int64(mb.Len())) if err != nil { t.Error(err) } @@ -43,7 +47,7 @@ func TestMothballsOsFs(t *testing.T) { defer f.Close() if buf, err := ioutil.ReadAll(f); err != nil { t.Error(err) - } else if string(buf) != "" { + } else if string(buf) != "1\n2\n3\n" { t.Error("Bad puzzles.txt", string(buf)) } } From b39e287e41cbebdfd08ef8d3b5378dcf84966444 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 13 Oct 2020 16:41:50 -0600 Subject: [PATCH 02/10] Debug prod server, apparently for the first time. --- cmd/mothd/httpd_test.go | 4 +- cmd/mothd/mothballs.go | 2 +- cmd/mothd/mothballs_test.go | 12 +- cmd/mothd/server.go | 5 +- cmd/mothd/server_test.go | 12 ++ cmd/transpile/main.go | 4 +- theme/moth.js | 1 + theme/points.json | 255 ------------------------------------ theme/puzzle.js | 2 +- theme/puzzles.json | 54 -------- 10 files changed, 27 insertions(+), 324 deletions(-) delete mode 100644 theme/points.json delete mode 100644 theme/puzzles.json diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index 6bd66dc..ea8619f 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -50,8 +50,8 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":""},"PointsLog":[],"Puzzles":{"pategory":[1]}}` { - t.Error("Unexpected state") + } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":""},"PointsLog":[],"Puzzles":{}}` { + t.Error("Unexpected state", r.Body.String()) } if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 { diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index 6c3ada4..8d44a0e 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -51,7 +51,7 @@ func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekClose return nil, time.Time{}, fmt.Errorf("No such category: %s", cat) } - f, err := zc.Open(fmt.Sprintf("content/%d/%s", points, filename)) + f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename)) if err != nil { return nil, time.Time{}, err } diff --git a/cmd/mothd/mothballs_test.go b/cmd/mothd/mothballs_test.go index e32ca17..9b75838 100644 --- a/cmd/mothd/mothballs_test.go +++ b/cmd/mothd/mothballs_test.go @@ -13,12 +13,12 @@ var testFiles = []struct { }{ {"puzzles.txt", "1\n3\n2\n"}, {"answers.txt", "1 answer123\n1 answer456\n2 wat\n"}, - {"content/1/puzzle.json", `{"name": "moo"}`}, - {"content/1/moo.txt", `moo`}, - {"content/2/puzzle.json", `{}`}, - {"content/2/moo.txt", `moo`}, - {"content/3/puzzle.json", `{}`}, - {"content/3/moo.txt", `moo`}, + {"1/puzzle.json", `{"name": "moo"}`}, + {"1/moo.txt", `moo`}, + {"2/puzzle.json", `{}`}, + {"2/moo.txt", `moo`}, + {"3/puzzle.json", `{}`}, + {"3/moo.txt", `moo`}, } func (m *Mothballs) createMothball(cat string) { diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 35619bc..556bbf7 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -175,7 +175,8 @@ func (mh *MothRequestHandler) ExportState() *StateExport { export := StateExport{} export.Config = mh.Config - teamName, _ := mh.State.TeamName(mh.teamID) + teamName, err := mh.State.TeamName(mh.teamID) + registered := (err == nil) export.Messages = mh.State.Messages() export.TeamNames = map[string]string{"self": teamName} @@ -204,7 +205,7 @@ func (mh *MothRequestHandler) ExportState() *StateExport { } export.Puzzles = make(map[string][]int) - if _, ok := export.TeamNames["self"]; ok { + if registered { // We used to hand this out to everyone, // but then we got a bad reputation on some secretive blacklist, // and now the Navy can't register for events. diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 2c5f732..586ce7e 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -34,6 +34,18 @@ func TestServer(t *testing.T) { server := NewTestServer() handler := server.NewHandler(participantID, teamID) + + { + es := handler.ExportState() + if es.Config.Devel { + t.Error("Marked as development server", es.Config) + } + if len(es.Puzzles) != 0 { + t.Log("State", es) + t.Error("Unauthenticated state has non-empty puzzles list") + } + } + if err := handler.Register(teamName); err != nil { t.Error(err) } diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index 34cec5b..82f034b 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -81,7 +81,6 @@ func (t *T) ParseArgs() (Command, error) { return nothing, err } if *directory != "" { - log.Println(*directory) t.fs = afero.NewBasePathFs(t.BaseFs, *directory) } else { t.fs = t.BaseFs @@ -151,7 +150,7 @@ func (t *T) DumpMothball() error { if t.filename == "" { w = t.Stdout } else { - log.Println("Writing to", t.filename, t.fs) + log.Println("Writing mothball to", t.filename) outf, err := t.BaseFs.Create(t.filename) if err != nil { return err @@ -159,7 +158,6 @@ func (t *T) DumpMothball() error { defer outf.Close() w = outf } - log.Println(t.fs) if err := transpile.Mothball(c, w); err != nil { return err } diff --git a/theme/moth.js b/theme/moth.js index ccf2080..8631f30 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -97,6 +97,7 @@ function renderPuzzles(obj) { } function renderState(obj) { + window.state = obj devel = obj.Config.Devel if (devel) { let params = new URLSearchParams(window.location.search) diff --git a/theme/points.json b/theme/points.json deleted file mode 100644 index 36e0aef..0000000 --- a/theme/points.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "__comment__": [ - "This file is to help debug themes.", - "MOTHd will ignore it." - ], - "teams": { - "0": "4HED Followers", - "1": "Dirtbags", - "17": "Eyeball", - "2": "Soup Giver!!!!!!!!!", - "24": "Dumb freshmans 3", - "25": "Winner", - "2d": "Cool team name", - "2f": "Dumm freshmans #1", - "4": "K19 the Widow Maker", - "5": "2T2", - "6": "Apples", - "7": "Top Minds", - "8": "DIRTBAGS", - "b": "Antiderivative of Pizza" - }, - "points": [ - [1573007086,"0","codebreaking",1], - [1573007096,"1","codebreaking",1], - [1573007114,"2","codebreaking",1], - [1573007153,"0","codebreaking",2], - [1573007159,"4","codebreaking",1], - [1573007169,"5","codebreaking",1], - [1573007181,"6","sequence",1], - [1573007184,"7","codebreaking",1], - [1573007209,"8","codebreaking",1], - [1573007212,"2","codebreaking",2], - [1573007240,"1","sequence",1], - [1573007244,"b","codebreaking",1], - [1573007246,"1","nocode",1], - [1573007258,"5","nocode",1], - [1573007271,"5","nocode",2], - [1573007284,"1","steg",1], - [1573007295,"7","codebreaking",2], - [1573007298,"2","codebreaking",4], - [1573007305,"5","nocode",3], - [1573007316,"7","codebreaking",4], - [1573007321,"0","codebreaking",4], - [1573007328,"5","nocode",4], - [1573007331,"7","nocode",1], - [1573007336,"17","codebreaking",1], - [1573007340,"7","nocode",2], - [1573007367,"0","nocode",10], - [1573007369,"7","nocode",3], - [1573007371,"b","nocode",1], - [1573007379,"7","nocode",4], - [1573007388,"b","nocode",2], - [1573007391,"6","sequence",2], - [1573007397,"4","codebreaking",2], - [1573007407,"b","nocode",3], - [1573007411,"7","nocode",10], - [1573007413,"5","nocode",10], - [1573007429,"b","nocode",4], - [1573007442,"24","codebreaking",2], - [1573007451,"25","codebreaking",1], - [1573007456,"7","sequence",1], - [1573007460,"b","nocode",10], - [1573007467,"5","sequence",1], - [1573007471,"7","sequence",2], - [1573007478,"5","sequence",2], - [1573007479,"17","codebreaking",2], - [1573007490,"2","codebreaking",5], - [1573007509,"2d","codebreaking",1], - [1573007536,"8","codebreaking",2], - [1573007544,"2f","codebreaking",1], - [1573007546,"b","sequence",1], - [1573007574,"24","codebreaking",4], - [1573007581,"b","sequence",2], - [1573007591,"25","codebreaking",2], - [1573007603,"8","codebreaking",4], - [1573007614,"0","nocode",20], - [1573007639,"4","codebreaking",4], - [1573007678,"6","codebreaking",1], - [1573007692,"8","nocode",1], - [1573007695,"24","codebreaking",5], - [1573007705,"7","codebreaking",5], - [1573007707,"8","nocode",2], - [1573007713,"17","nocode",1], - [1573007727,"17","nocode",2], - [1573007735,"8","nocode",3], - [1573007737,"b","steg",1], - [1573007739,"25","codebreaking",4], - [1573007749,"8","nocode",4], - [1573007757,"17","codebreaking",4], - [1573007768,"8","nocode",10], - [1573007795,"0","sequence",1], - [1573007799,"8","sequence",1], - [1573007816,"0","sequence",2], - [1573007822,"8","sequence",2], - [1573007834,"24","codebreaking",6], - [1573007853,"2d","codebreaking",2], - [1573007905,"1","codebreaking",2], - [1573007941,"4","codebreaking",5], - [1573007956,"1","codebreaking",4], - [1573007974,"6","codebreaking",2], - [1573007998,"17","sequence",1], - [1573008022,"b","codebreaking",4], - [1573008055,"24","sequence",2], - [1573008063,"6","codebreaking",4], - [1573008066,"2d","codebreaking",4], - [1573008074,"24","sequence",1], - [1573008099,"17","nocode",4], - [1573008101,"0","codebreaking",7], - [1573008108,"2d","nocode",1], - [1573008135,"24","nocode",30], - [1573008146,"1","codebreaking",5], - [1573008162,"2d","nocode",2], - [1573008174,"b","codebreaking",2], - [1573008191,"2","codebreaking",6], - [1573008234,"6","codebreaking",5], - [1573008240,"2","nocode",10], - [1573008291,"5","steg",1], - [1573008310,"6","nocode",1], - [1573008323,"2d","nocode",3], - [1573008327,"6","nocode",2], - [1573008330,"25","codebreaking",5], - [1573008334,"2f","codebreaking",2], - [1573008348,"6","nocode",3], - [1573008356,"2d","nocode",4], - [1573008362,"b","codebreaking",5], - [1573008364,"6","nocode",4], - [1573008364,"17","codebreaking",5], - [1573008371,"24","nocode",4], - [1573008385,"24","nocode",3], - [1573008390,"6","nocode",10], - [1573008397,"24","nocode",2], - [1573008400,"25","nocode",1], - [1573008402,"2d","nocode",10], - [1573008408,"24","nocode",1], - [1573008419,"25","nocode",2], - [1573008429,"24","steg",1], - [1573008437,"25","nocode",3], - [1573008451,"25","nocode",4], - [1573008479,"25","nocode",10], - [1573008502,"2d","sequence",1], - [1573008506,"17","codebreaking",6], - [1573008537,"2d","sequence",2], - [1573008649,"17","codebreaking",7], - [1573008668,"2f","codebreaking",4], - [1573008716,"1","codebreaking",6], - [1573008768,"8","steg",1], - [1573008808,"7","nocode",50], - [1573008817,"24","steg",2], - [1573008832,"2f","codebreaking",5], - [1573008890,"17","steg",1], - [1573008902,"b","steg",2], - [1573008932,"7","steg",1], - [1573008944,"24","steg",3], - [1573008978,"2","steg",1], - [1573009006,"24","steg",4], - [1573009032,"6","steg",1], - [1573009038,"b","steg",3], - [1573009052,"2d","codebreaking",5], - [1573009098,"b","steg",4], - [1573009122,"8","steg",2], - [1573009125,"4","nocode",1], - [1573009160,"24","nocode",10], - [1573009161,"4","nocode",2], - [1573009179,"2","steg",2], - [1573009180,"1","steg",2], - [1573009194,"24","nocode",20], - [1573009203,"0","nocode",50], - [1573009212,"2f","codebreaking",6], - [1573009240,"2f","nocode",1], - [1573009250,"4","nocode",4], - [1573009255,"2f","nocode",2], - [1573009258,"2","steg",4], - [1573009282,"4","nocode",10], - [1573009299,"25","sequence",1], - [1573009305,"6","steg",4], - [1573009308,"17","steg",3], - [1573009310,"1","steg",3], - [1573009334,"7","steg",4], - [1573009345,"1","steg",4], - [1573009345,"7","steg",3], - [1573009354,"8","steg",4], - [1573009357,"25","sequence",2], - [1573009402,"6","steg",3], - [1573009402,"b","sequence",8], - [1573009413,"2f","nocode",3], - [1573009437,"17","steg",2], - [1573009455,"2f","nocode",10], - [1573009481,"b","sequence",16], - [1573009502,"b","sequence",19], - [1573009520,"b","sequence",25], - [1573009525,"17","steg",4], - [1573009559,"7","steg",2], - [1573009561,"b","sequence",35], - [1573009571,"0","sequence",35], - [1573009588,"25","steg",1], - [1573009602,"24","sequence",8], - [1573009607,"2","steg",5], - [1573009614,"1","steg",5], - [1573009617,"17","sequence",35], - [1573009620,"7","sequence",50], - [1573009621,"6","steg",5], - [1573009629,"5","steg",3], - [1573009632,"7","sequence",35], - [1573009644,"17","sequence",25], - [1573009670,"6","steg",6], - [1573009698,"8","steg",6], - [1573009700,"17","sequence",19], - [1573009703,"24","steg",6], - [1573009703,"4","sequence",1], - [1573009707,"0","sequence",50], - [1573009710,"25","steg",2], - [1573009729,"2f","sequence",1], - [1573009768,"1","steg",6], - [1573009814,"2","codebreaking",8], - [1573009842,"0","steg",1], - [1573009844,"2f","sequence",2], - [1573009882,"4","steg",1], - [1573009896,"25","steg",3], - [1573009931,"1","sequence",2], - [1573009937,"25","steg",4], - [1573010066,"7","steg",6], - [1573010101,"25","steg",5], - [1573010114,"5","steg",4], - [1573010137,"25","steg",6], - [1573010185,"4","sequence",2], - [1573010229,"17","nocode",80], - [1573010256,"24","sequence",35], - [1573010281,"6","codebreaking",7], - [1573010336,"25","codebreaking",6], - [1573010390,"7","codebreaking",7], - [1573010468,"2f","steg",1], - [1573010712,"0","steg",2], - [1573010739,"0","steg",3], - [1573010754,"0","steg",4], - [1573010778,"0","steg",5], - [1573010784,"7","nocode",90], - [1573010792,"0","steg",6], - [1573011760,"7","sequence",60], - [1573056120,"0","sequence",100], - [1573056324,"0","sequence",200], - [1573056791,"0","sequence",300], - [1573057092,"0","sequence",400], - [1573076767,"25","sequence",400], - [1573076809,"25","sequence",300], - [1573076838,"25","sequence",200], - [1573076936,"25","nocode",20], - [1573077275,"25","nocode",50], - [1573078364,"0","sequence",19], - [1573078432,"0","sequence",25], - [1573078487,"25","sequence",35], - [1573078501,"25","sequence",50], - [1573079359,"0","nocode",90], - [1573079714,"25","nocode",9] - ] -} diff --git a/theme/puzzle.js b/theme/puzzle.js index 9c8e42a..7dd24c5 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -132,7 +132,7 @@ async function loadPuzzle(categoryName, points, puzzleId) { document.getElementById("authors").textContent = window.puzzle.Pre.Authors.join(", ") // If answers are provided, this is the devel server - if (window.puzzle.Answers) { + if (window.puzzle.Answers.length > 0) { devel_addin(document.getElementById("devel")) } diff --git a/theme/puzzles.json b/theme/puzzles.json deleted file mode 100644 index 5a6735d..0000000 --- a/theme/puzzles.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "__comment__": [ - "This file is to help debug themes.", - "MOTHd will ignore it." - ], - "codebreaking": [ - [1,"37117e6b034696b86c6516477cc0bc60bc1e642e"], - [2,"546b586428979771b061608489327da4940086a7"], - [4,"6f2a33c93f56b4f29cc79e6576ba4d1000aa1756"], - [5,"c654fe263909b1940d7aad8c572363a0569c07c6"], - [6,"f30bd32bf940f2bb03506ec334d2d204efc4695b"], - [7,"128b119083b6ae70c380a8eb70ec6a518425e7af"], - [8,"edd4f57aeb565b3b053fa194f5e677cb77ef0285"], - [15,"9781863bca9f596972e2a10460932ec5ec6be3fe"] - ], - "nocode": [ - [1,"37117e6b034696b86c6516477cc0bc60bc1e642e"], - [2,"546b586428979771b061608489327da4940086a7"], - [3,"79c08697a1923da1118fd0c2e922b5d3899cabcc"], - [4,"6f2a33c93f56b4f29cc79e6576ba4d1000aa1756"], - [10,"bf4fae263bf6e4243b143f4ecd64e471f3ec75dd"], - [20,"9f374f6dac9f972fac4693099a7bfa7c535f7503"], - [30,"02de1196d43976b2d050c6c597f068623d2df201"], - [50,"9acb3af947cb4aa10a9c1221c04518f956cdc0d0"], - [80,"78f807ac44f3cbf537861e7cdf1ac53937e4ee47"], - [90,"6d537653aa599178c72528f7e1f2fbb36e6333f9"], - [100,"4f5982a3a7cc9b9af0320130132e8cab39a1fd2c"] - ], - "sequence": [ - [1,"37117e6b034696b86c6516477cc0bc60bc1e642e"], - [2,"546b586428979771b061608489327da4940086a7"], - [8,"edd4f57aeb565b3b053fa194f5e677cb77ef0285"], - [16,"a9ace4b773f045c422260edefaa8563dcd80ac59"], - [19,"f11ca0172451f37ba6f4d66ff9add80013480a49"], - [25,"0458533d28705548829e53d686215cc6fbeec8f5"], - [35,"91aac06bae090ae7d1699b5a78601ef8d29e9271"], - [50,"9acb3af947cb4aa10a9c1221c04518f956cdc0d0"], - [60,"bf84beed9e382268ab40d0113dfeb73c96aa919a"], - [100,"4f5982a3a7cc9b9af0320130132e8cab39a1fd2c"], - [200,"3b9b8993fe639cf0c19a58b39ebbf6077828887a"], - [300,"0f13c4d19bc5d2e10d43e8cd2e40f759e731cece"], - [400,"db7a59f313818fc9598969d2a0a04e21bd26697f"], - [500,"81c5389eb5406aa44053662f6482f246b8a12e0c"] - ], - "steg": [ - [1,"200e8cd902ba7304765c463f6ed1322bc25f3454"], - [2,"707328988c3986d450d8fe419eb49f078fb7998c"], - [3,"d0b336ad59cbcd4415ddf200c6c099db5c3fea1d"], - [4,"f071503b403ffee2b38e186e800bfd5dd28e8f0e"], - [5,"186f425fa5762ef37f874cc602fe0edc4325a5d2"], - [6,"c6527c3c30c4e6a33026192d358d83d259cd17a7"], - [10,"84973f77a1b14e4666f3d8a8bdeead7633c4ed56"] - ] -} From 8874aad7cb9df6b2e906b5491ed920def88f01ef Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 13 Oct 2020 18:33:12 -0600 Subject: [PATCH 03/10] Fix bug that required id to get puzzle content --- cmd/mothd/httpd.go | 4 +++- cmd/mothd/httpd_test.go | 10 ++++++++++ cmd/mothd/server.go | 11 +++++++---- cmd/mothd/server_test.go | 27 +++++++++++++++++++++++++-- cmd/mothd/state.go | 8 ++++++-- theme/moth.js | 4 ++-- theme/puzzle.js | 4 ++-- 7 files changed, 55 insertions(+), 13 deletions(-) diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 7962fbe..922f047 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -110,7 +110,9 @@ func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, // RegisterHandler handles attempts to register a team func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { teamName := req.FormValue("name") - if err := mh.Register(teamName); err != nil { + if err := mh.Register(teamName); err == ErrAlreadyRegistered { + jsend.Sendf(w, jsend.Success, "already registered", "Team ID has already been registered") + } else if err != nil { jsend.Sendf(w, jsend.Fail, "not registered", err.Error()) } else { jsend.Sendf(w, jsend.Success, "registered", "Team ID registered") diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index ea8619f..8c0b8b3 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -66,6 +66,12 @@ func TestHttpd(t *testing.T) { t.Error("Register failed") } +thatt if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"Team ID has already been registered"}}` { + t.Error("Register failed", r.Body.String()) + } + if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":"GoTeam"},"PointsLog":[],"Puzzles":{"pategory":[1]}}` { @@ -108,6 +114,10 @@ func TestHttpd(t *testing.T) { time.Sleep(TestMaintenanceInterval) + if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } + state := StateExport{} if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 556bbf7..9f83c7a 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -107,7 +107,7 @@ type MothRequestHandler struct { // PuzzlesOpen opens a file associated with a puzzle. // BUG(neale): Multiple providers with the same category name are not detected or handled well. func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (r ReadSeekCloser, ts time.Time, err error) { - export := mh.ExportState() + export := mh.exportStateIfRegistered(true) found := false for _, p := range export.Puzzles[cat] { if p == points { @@ -115,7 +115,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) ( } } if !found { - return nil, time.Time{}, fmt.Errorf("Category not found") + return nil, time.Time{}, fmt.Errorf("Puzzle does not exist or is locked") } // Try every provider until someone doesn't return an error @@ -172,11 +172,15 @@ func (mh *MothRequestHandler) Register(teamName string) error { // the anonymized team name for this teamID has the special value "self". // If not, the puzzles list is empty. func (mh *MothRequestHandler) ExportState() *StateExport { + return mh.exportStateIfRegistered(false) +} + +func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExport { export := StateExport{} export.Config = mh.Config teamName, err := mh.State.TeamName(mh.teamID) - registered := (err == nil) + registered := override || (err == nil) export.Messages = mh.State.Messages() export.TeamNames = map[string]string{"self": teamName} @@ -209,7 +213,6 @@ func (mh *MothRequestHandler) ExportState() *StateExport { // We used to hand this out to everyone, // but then we got a bad reputation on some secretive blacklist, // and now the Navy can't register for events. - for _, provider := range mh.PuzzleProviders { for _, category := range provider.Inventory() { // Append sentry (end of puzzles) diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 586ce7e..bc2526f 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -34,6 +34,7 @@ func TestServer(t *testing.T) { server := NewTestServer() handler := server.NewHandler(participantID, teamID) + anonHandler := server.NewHandler("badParticipantId", "badTeamId") { es := handler.ExportState() @@ -49,6 +50,12 @@ func TestServer(t *testing.T) { if err := handler.Register(teamName); err != nil { t.Error(err) } + if err := handler.Register(teamName); err == nil { + t.Error("Registering twice should have raised an error") + } else if err != ErrAlreadyRegistered { + t.Error("Wrong error for duplicate registration:", err) + } + if r, _, err := handler.ThemeOpen("/index.html"); err != nil { t.Error(err) } else if contents, err := ioutil.ReadAll(r); err != nil { @@ -89,12 +96,12 @@ func TestServer(t *testing.T) { r.Close() } - if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzles.json"); err == nil { + if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzle.json"); err == nil { t.Error("Opening locked puzzle shouldn't work") r.Close() } - if r, _, err := handler.PuzzlesOpen("pategory", 20, "puzzles.json"); err == nil { + if r, _, err := handler.PuzzlesOpen("pategory", 20, "puzzle.json"); err == nil { t.Error("Opening non-existent puzzle shouldn't work") r.Close() } @@ -109,6 +116,22 @@ func TestServer(t *testing.T) { if len(es.PointsLog) != 1 { t.Error("I didn't get my points!") } + if len(es.Puzzles["pategory"]) != 2 { + t.Error("The next puzzle didn't unlock!") + } else if es.Puzzles["pategory"][1] != 2 { + t.Error("The 2-point puzzle should have unlocked!") + } + + if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzle.json"); err != nil { + t.Error("Opening unlocked puzzle should work") + } else { + r.Close() + } + if r, _, err := anonHandler.PuzzlesOpen("pategory", 2, "puzzle.json"); err != nil { + t.Error("Opening unlocked puzzle anonymously should work") + } else { + r.Close() + } // BUG(neale): We aren't currently testing the various ways to disable the server } diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 3296749..0270bf9 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "errors" "fmt" "log" "math/rand" @@ -23,6 +24,9 @@ const DistinguishableChars = "34678abcdefhikmnpqrtwxy=" // This is also a valid RFC3339 format. const RFC3339Space = "2006-01-02 15:04:05Z07:00" +// ErrAlreadyRegistered means a team cannot be registered because it was registered previously. +var ErrAlreadyRegistered = errors.New("Team ID has already been registered") + // State defines the current state of a MOTH instance. // We use the filesystem for synchronization between threads. // The only thing State methods need to know is the path to the state directory. @@ -127,7 +131,7 @@ func (s *State) TeamName(teamID string) (string, error) { } // SetTeamName writes out team name. -// This can only be done once. +// This can only be done once per team. func (s *State) SetTeamName(teamID, teamName string) error { idsFile, err := s.Open("teamids.txt") if err != nil { @@ -149,7 +153,7 @@ func (s *State) SetTeamName(teamID, teamName string) error { teamFilename := filepath.Join("teams", teamID) teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_EXCL, 0644) if os.IsExist(err) { - return fmt.Errorf("Team ID is already registered") + return ErrAlreadyRegistered } else if err != nil { return err } diff --git a/theme/moth.js b/theme/moth.js index 8631f30..2724411 100644 --- a/theme/moth.js +++ b/theme/moth.js @@ -59,7 +59,7 @@ function renderPuzzles(obj) { pdiv.appendChild(l) for (let puzzle of puzzles) { let points = puzzle - let id = puzzle + let id = null if (Array.isArray(puzzle)) { points = puzzle[0] @@ -80,7 +80,7 @@ function renderPuzzles(obj) { let url = new URL("puzzle.html", window.location) url.searchParams.set("cat", cat) url.searchParams.set("points", points) - url.searchParams.set("pid", id) + if (id) { url.searchParams.set("pid", id) } a.href = url.toString() } } diff --git a/theme/puzzle.js b/theme/puzzle.js index 7dd24c5..0dd3797 100644 --- a/theme/puzzle.js +++ b/theme/puzzle.js @@ -204,8 +204,8 @@ function init() { let points = params.get("points") let puzzleId = params.get("pid") - if (categoryName && points && puzzleId) { - loadPuzzle(categoryName, points, puzzleId) + if (categoryName && points) { + loadPuzzle(categoryName, points, puzzleId || points) } let teamId = sessionStorage.getItem("id") From cc3f5d0e39b9b50ebea2ca3a9071d87164a3614d Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 13 Oct 2020 18:43:36 -0600 Subject: [PATCH 04/10] changelog entries --- CHANGELOG.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2942bb8..bdfa3a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. 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). -## [v4.0.0] - Unreleased +## [v4.0.0-rc2] - Unreleased +### Fixed +- Multiple bugs preventing production server from working properly + +## [v4.0-rc1] - 2020-10-13 ### Changed - Major rewrite/refactor of `mothd` - Clear separation of roles: State, Puzzles, and Theme @@ -37,14 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security -## [Unreleased] -### Changed - - Endpoints `/points.json`, `/puzzles.json`, and `/messages.html` (optional theme file) combine into `/state` - - No more `__devel__` category for dev server: this is now `.config.devel` in the `/state` endpoint - - Development server no longer serves a static `/` with links: it now redirects you to a randomly-generated seed URL - - Default theme modifications to handle all this - - Default theme now automatically "logs you in" with Team ID if it's getting state from the devel server - ## [v3.5.1] - 2020-03-16 ### Fixed - Support insta-checking for legacy puzzles From 1080783eb936cb22688ecf3122754d0ebfe60fe6 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 13 Oct 2020 19:48:37 -0600 Subject: [PATCH 05/10] Fix bug with registration on actual filesystems --- CHANGELOG.md | 2 ++ cmd/mothd/httpd.go | 6 ++++++ cmd/mothd/httpd_test.go | 2 +- cmd/mothd/state.go | 4 +++- cmd/mothd/state_test.go | 10 ++++++++-- cmd/transpile/main.go | 11 +++++++++-- theme/scoreboard.js | 2 ++ 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdfa3a4..c2a0ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v4.0.0-rc2] - Unreleased ### Fixed - Multiple bugs preventing production server from working properly +- CI builds should be working now +- Team registration now correctly writes names to files ## [v4.0-rc1] - 2020-10-13 ### Changed diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 922f047..87f8acf 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -110,6 +110,12 @@ func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, // RegisterHandler handles attempts to register a team func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { teamName := req.FormValue("name") + teamName = strings.TrimSpace(teamName) + if teamName == "" { + jsend.Sendf(w, jsend.Fail, "empty name", "Team name may not be empty") + return + } + if err := mh.Register(teamName); err == ErrAlreadyRegistered { jsend.Sendf(w, jsend.Success, "already registered", "Team ID has already been registered") } else if err != nil { diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index 8c0b8b3..2ec8e25 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -66,7 +66,7 @@ func TestHttpd(t *testing.T) { t.Error("Register failed") } -thatt if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { + if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { t.Error(r.Result()) } else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"Team ID has already been registered"}}` { t.Error("Register failed", r.Body.String()) diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 0270bf9..ed10197 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -151,14 +151,16 @@ func (s *State) SetTeamName(teamID, teamName string) error { } teamFilename := filepath.Join("teams", teamID) - teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_EXCL, 0644) + teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) if os.IsExist(err) { return ErrAlreadyRegistered } else if err != nil { return err } defer teamFile.Close() + log.Println("Setting team name to:", teamName, teamFilename, teamFile) fmt.Fprintln(teamFile, teamName) + teamFile.Close() return nil } diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 7c6e156..2cbae4b 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -55,12 +55,18 @@ func TestState(t *testing.T) { t.Errorf("Setting bad team ID didn't raise an error") } - if err := s.SetTeamName(teamID, "My Team"); err != nil { - t.Errorf("Setting team name: %v", err) + teamName := "My Team" + if err := s.SetTeamName(teamID, teamName); err != nil { + t.Errorf("Setting team name: %w", err) } if err := s.SetTeamName(teamID, "wat"); err == nil { t.Errorf("Registering team a second time didn't fail") } + if name, err := s.TeamName(teamID); err != nil { + t.Error(err) + } else if name != teamName { + t.Error("Incorrect team name:", name) + } category := "poot" points := 3928 diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index 82f034b..e8f8331 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -147,9 +147,13 @@ func (t *T) DumpMothball() error { var w io.Writer c := transpile.NewFsCategory(t.fs, "") - if t.filename == "" { + + removeOnError := false + switch t.filename { + case "", "-": w = t.Stdout - } else { + default: + removeOnError = true log.Println("Writing mothball to", t.filename) outf, err := t.BaseFs.Create(t.filename) if err != nil { @@ -159,6 +163,9 @@ func (t *T) DumpMothball() error { w = outf } if err := transpile.Mothball(c, w); err != nil { + if removeOnError { + t.BaseFs.Remove(t.filename) + } return err } return nil diff --git a/theme/scoreboard.js b/theme/scoreboard.js index c5b64f1..104efcb 100644 --- a/theme/scoreboard.js +++ b/theme/scoreboard.js @@ -13,6 +13,8 @@ function scoreboardInit() { ] function update(state) { + window.state = state + for (let rotate of document.querySelectorAll(".rotate")) { rotate.appendChild(rotate.firstElementChild) } From 2d3c7072058ca76c6728c8be9a44e4c8e09849d7 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 14 Oct 2020 09:12:06 -0600 Subject: [PATCH 06/10] fix typo --- docs/administration.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/administration.md b/docs/administration.md index aa49c73..7a13145 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -70,7 +70,7 @@ Adjusting scores ------------------ rm /srv/moth/state/enabled # Suspend scoring - nano /srv/moth/state/points.log + nano /srv/moth/state/points.log # Replace nano with your preferred editor touch /srv/moth/state/enabled # Resume scoring We don't warn participants before we do this: @@ -78,10 +78,8 @@ any points scored while scoring is suspended are queued up and processed as soon It's very important to suspend scoring before mucking around with the points log. The maintenance loop assumes it is the only thing writing to this file, -and any edits you make could blow aware points scored. +and any edits you make will remove points scored while you were editing. -No, I don't use nano. -None of us use nano. Changing a team name From 843996f2c4c578236a10bea1773ad39fe6854e68 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 14 Oct 2020 09:46:51 -0600 Subject: [PATCH 07/10] Only anonymize team IDs once per team --- CHANGELOG.md | 1 + cmd/mothd/server.go | 2 +- cmd/mothd/server_test.go | 15 +++++++++++++++ cmd/mothd/state_test.go | 6 +++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a0ef1..939dd2f 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 - Multiple bugs preventing production server from working properly - CI builds should be working now - Team registration now correctly writes names to files +- Anonymized team names now only computed once per team ## [v4.0-rc1] - 2020-10-13 ### Changed diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 9f83c7a..97c90b3 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -196,8 +196,8 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor } else { exportID := strconv.Itoa(logno) name, _ := mh.State.TeamName(awd.TeamID) - awd.TeamID = exportID exportIDs[awd.TeamID] = awd.TeamID + awd.TeamID = exportID export.TeamNames[exportID] = name } export.PointsLog[logno] = awd diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index bc2526f..9144b2e 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -133,5 +133,20 @@ func TestServer(t *testing.T) { r.Close() } + if err := handler.CheckAnswer("pategory", 2, "wat"); err != nil { + t.Error("Right answer marked wrong:", err) + } + + time.Sleep(TestMaintenanceInterval) + es = anonHandler.ExportState() + if len(es.TeamNames) != 2 { + t.Error("Anonymous TeamNames is wrong:", es.TeamNames) + } + + es = handler.ExportState() + if len(es.TeamNames) != 1 { + t.Error("TeamNames is wrong:", es.TeamNames) + } + // BUG(neale): We aren't currently testing the various ways to disable the server } diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 2cbae4b..c05bd3b 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -89,6 +89,10 @@ func TestState(t *testing.T) { t.Error("Duplicate points award didn't fail") } + if err := s.AwardPoints(teamID, category, points+1); err != nil { + t.Error("Awarding more points:", err) + } + pl = s.PointsLog() if len(pl) != 1 { t.Errorf("After awarding points, points log has length %d", len(pl)) @@ -104,7 +108,7 @@ func TestState(t *testing.T) { t.Error(err) } s.refresh() - if len(s.PointsLog()) != 1 { + if len(s.PointsLog()) != 2 { t.Error("Intentional parse error screws up all parsing") } From 60d7192e042cdd3ea6ef6fc76b24edc2b91c20bb Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 14 Oct 2020 09:52:19 -0600 Subject: [PATCH 08/10] Properly anonymize all point log entries --- cmd/mothd/server.go | 2 +- cmd/mothd/server_test.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 97c90b3..ad929b5 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -196,7 +196,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor } else { exportID := strconv.Itoa(logno) name, _ := mh.State.TeamName(awd.TeamID) - exportIDs[awd.TeamID] = awd.TeamID + exportIDs[awd.TeamID] = exportID awd.TeamID = exportID export.TeamNames[exportID] = name } diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 9144b2e..f6695ac 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -142,6 +142,12 @@ func TestServer(t *testing.T) { if len(es.TeamNames) != 2 { t.Error("Anonymous TeamNames is wrong:", es.TeamNames) } + if len(es.PointsLog) != 2 { + t.Error("Points log wrong length") + } + if es.PointsLog[1].TeamID != "0" { + t.Error("Second point log didn't anonymize team ID correctly:", es.PointsLog[1]) + } es = handler.ExportState() if len(es.TeamNames) != 1 { From 5fe464acbcf92ddfdfb542bda1664fdad039a56b Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 14 Oct 2020 10:04:13 -0600 Subject: [PATCH 09/10] Don't map the "self" team in unauthenticated states --- cmd/mothd/server.go | 11 ++++-- cmd/mothd/server_test.go | 85 ++++++++++++++++++++++------------------ 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index ad929b5..f801857 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -183,13 +183,18 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor registered := override || (err == nil) export.Messages = mh.State.Messages() - export.TeamNames = map[string]string{"self": teamName} +inin export.TeamNames = make(map[string]string) // Anonymize team IDs in points log, and write out team names pointsLog := mh.State.PointsLog() - exportIDs := map[string]string{mh.teamID: "self"} - maxSolved := map[string]int{} + exportIDs := make(map[string]string) + maxSolved := make(map[string]int) export.PointsLog = make(award.List, len(pointsLog)) + + if registered { + export.TeamNames["self"] = teamName + exportIDs[mh.teamID] = "self" + } for logno, awd := range pointsLog { if id, ok := exportIDs[awd.TeamID]; ok { awd.TeamID = id diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index f6695ac..4b0df96 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -64,24 +64,26 @@ func TestServer(t *testing.T) { t.Error("index.html wrong contents", contents) } - es := handler.ExportState() - if es.Config.Devel { - t.Error("Marked as development server", es.Config) - } - if len(es.Puzzles) != 1 { - t.Error("Puzzle categories wrong length") - } - if es.Messages != "messages.html" { - t.Error("Messages has wrong contents") - } - if len(es.PointsLog) != 0 { - t.Error("Points log not empty") - } - if len(es.TeamNames) != 1 { - t.Error("Wrong number of team names") - } - if es.TeamNames["self"] != teamName { - t.Error("TeamNames['self'] wrong") + { + es := handler.ExportState() + if es.Config.Devel { + t.Error("Marked as development server", es.Config) + } + if len(es.Puzzles) != 1 { + t.Error("Puzzle categories wrong length") + } + if es.Messages != "messages.html" { + t.Error("Messages has wrong contents") + } + if len(es.PointsLog) != 0 { + t.Error("Points log not empty") + } + if len(es.TeamNames) != 1 { + t.Error("Wrong number of team names") + } + if es.TeamNames["self"] != teamName { + t.Error("TeamNames['self'] wrong") + } } if r, _, err := handler.PuzzlesOpen("pategory", 1, "moo.txt"); err != nil { @@ -112,14 +114,16 @@ func TestServer(t *testing.T) { time.Sleep(TestMaintenanceInterval) - es = handler.ExportState() - if len(es.PointsLog) != 1 { - t.Error("I didn't get my points!") - } - if len(es.Puzzles["pategory"]) != 2 { - t.Error("The next puzzle didn't unlock!") - } else if es.Puzzles["pategory"][1] != 2 { - t.Error("The 2-point puzzle should have unlocked!") + { + es := handler.ExportState() + if len(es.PointsLog) != 1 { + t.Error("I didn't get my points!") + } + if len(es.Puzzles["pategory"]) != 2 { + t.Error("The next puzzle didn't unlock!") + } else if es.Puzzles["pategory"][1] != 2 { + t.Error("The 2-point puzzle should have unlocked!") + } } if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzle.json"); err != nil { @@ -138,20 +142,25 @@ func TestServer(t *testing.T) { } time.Sleep(TestMaintenanceInterval) - es = anonHandler.ExportState() - if len(es.TeamNames) != 2 { - t.Error("Anonymous TeamNames is wrong:", es.TeamNames) - } - if len(es.PointsLog) != 2 { - t.Error("Points log wrong length") - } - if es.PointsLog[1].TeamID != "0" { - t.Error("Second point log didn't anonymize team ID correctly:", es.PointsLog[1]) + + { + es := anonHandler.ExportState() + if len(es.TeamNames) != 1 { + t.Error("Anonymous TeamNames is wrong:", es.TeamNames) + } + if len(es.PointsLog) != 2 { + t.Error("Points log wrong length") + } + if es.PointsLog[1].TeamID != "0" { + t.Error("Second point log didn't anonymize team ID correctly:", es.PointsLog[1]) + } } - es = handler.ExportState() - if len(es.TeamNames) != 1 { - t.Error("TeamNames is wrong:", es.TeamNames) + { + es := handler.ExportState() + if len(es.TeamNames) != 1 { + t.Error("TeamNames is wrong:", es.TeamNames) + } } // BUG(neale): We aren't currently testing the various ways to disable the server From ed5a4c90050c2c39cb90717da07f46cebf712abd Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Wed, 14 Oct 2020 10:05:53 -0600 Subject: [PATCH 10/10] all tests passing again + editor fail --- cmd/mothd/httpd_test.go | 2 +- cmd/mothd/server.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index 2ec8e25..f8bbb5b 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -50,7 +50,7 @@ func TestHttpd(t *testing.T) { if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { t.Error(r.Result()) - } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":""},"PointsLog":[],"Puzzles":{}}` { + } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{},"PointsLog":[],"Puzzles":{}}` { t.Error("Unexpected state", r.Body.String()) } diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index f801857..0b35618 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -183,7 +183,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor registered := override || (err == nil) export.Messages = mh.State.Messages() -inin export.TeamNames = make(map[string]string) + export.TeamNames = make(map[string]string) // Anonymize team IDs in points log, and write out team names pointsLog := mh.State.PointsLog()