diff --git a/docs/api.md b/docs/api.md index 25a6536..dbb34b1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -48,7 +48,7 @@ RegisterTeam(teamId, teamName) Register a team name with a team hash. -Parameters: +### Parameters * teamId: Team's unique identifier (usually a hex value) * teamName: Team's human-readable name @@ -56,13 +56,13 @@ Parameters: On success, no data is returned. On failure, message contains an English explanation of why. -Example: +### Example https://server/RegisterTeam?teamId=8b1292ca { status: "success", - data: nil + data: null } @@ -71,12 +71,12 @@ GetPuzzleList() Return all currently-open puzzles. -Return data: +### Return data * puzzles: dictionary mapping from category to a list of point values. -Example: +### Example https://server/GetPuzzleList @@ -96,12 +96,12 @@ GetPuzzle(category, points) Return a puzzle. -Parameters: +### Parameters * category: name of category to fetch from * points: point value of the puzzle to fetch -Return data: +### Return data * authors: List of puzzle authors * hashes: list of djbhash values of acceptable answers @@ -109,7 +109,7 @@ Return data: * body: HTML body of the puzzle -Example: +### Example https://server/GetPuzzle?category=sequence&points=1 @@ -130,7 +130,7 @@ GetPointsLog() Return the entire points log, and team names. -Return data: +### Return data * teams: mapping from team number (int) to team name * log: list of (timestamp, team number, category, points) @@ -138,7 +138,7 @@ Return data: Note: team number may change between calls. -Example: +### Example https://server/GetEventsLog @@ -163,20 +163,27 @@ SubmitAnswer(teamId, category, points, answer) Submit an answer to a puzzle. -Parameters: +### Parameters * teamId: Team ID (optional: if ommitted, answer is verified but no points are awarded) * category: category name of puzzle * points: point value of puzzle * answer: attempted answer -Example: + +### Return Data + +* epilogue: HTML to display as an "epilogue" to the puzzle + +### Example https://server/SubmitAnswer?teamId=8b1292ca&category=sequence&points=1&answer=6 { status: "success", - data: null + data: { + epilogue: "That's right: in base 10, 5 + 1 = 6." + } } SubmitToken(teamId, token) @@ -184,18 +191,18 @@ SubmitToken(teamId, token) Submit a token for points -Parameters: +### Parameters * teamId: Team ID * token: Token being submitted -Return data: +### Return data * category: category for which this token awarded points * points: number of points awarded -Example: +### Example https://server/SubmitToken?teamId=8b1292ca&token=wat:30:xylep-radar-nanox diff --git a/handlers.go b/handlers.go deleted file mode 100644 index a1edddd..0000000 --- a/handlers.go +++ /dev/null @@ -1,152 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "os" - "regexp" - "strings" - "strconv" -) - -func registerHandler(w http.ResponseWriter, req *http.Request) { - teamname := req.FormValue("n") - teamid := req.FormValue("h") - - if matched, _ := regexp.MatchString("[^0-9a-z]", teamid); matched { - teamid = "" - } - - if (teamid == "") || (teamname == "") { - showPage(w, "Invalid Entry", "Oops! Are you sure you got that right?") - return - } - - if ! anchoredSearch(statePath("teamids.txt"), teamid, 0) { - showPage(w, "Invalid Team ID", "I don't have a record of that team ID. Maybe you used capital letters accidentally?") - return - } - - f, err := os.OpenFile(statePath("state", teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) - if err != nil { - showPage( - w, - "Registration failed", - "Unable to register. Perhaps a teammate has already registered?", - ) - return - } - defer f.Close() - fmt.Fprintln(f, teamname) - showPage(w, "Success", "Okay, your team has been named and you may begin using your team ID!") -} - -func tokenHandler(w http.ResponseWriter, req *http.Request) { - teamid := req.FormValue("t") - token := req.FormValue("k") - - // Check answer - if ! anchoredSearch(token, statePath("tokens.txt"), 0) { - showPage(w, "Unrecognized token", "I don't recognize that token. Did you type in the whole thing?") - return - } - - parts := strings.Split(token, ":") - category := "" - pointstr := "" - if len(parts) >= 2 { - category = parts[0] - pointstr = parts[1] - } - points, err := strconv.Atoi(pointstr) - if err != nil { - points = 0 - } - // Defang category name; prevent directory traversal - if matched, _ := regexp.MatchString("^[A-Za-z0-9_-]", category); matched { - category = "" - } - - if (category == "") || (points == 0) { - showPage(w, "Unrecognized token", "Something doesn't look right about that token") - return - } - - if err := awardPoints(teamid, category, points); err != nil { - showPage(w, "Error awarding points", err.Error()) - return - } - showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid)) -} - -func answerHandler(w http.ResponseWriter, req *http.Request) { - teamid := req.FormValue("t") - category := req.FormValue("c") - pointstr := req.FormValue("p") - answer := req.FormValue("a") - - points, err := strconv.Atoi(pointstr) - if err != nil { - points = 0 - } - - // Defang category name; prevent directory traversal - if matched, _ := regexp.MatchString("^[A-Za-z0-9_-]", category); matched { - category = "" - } - - // Check answer - needle := fmt.Sprintf("%s %s", points, answer) - haystack := cachePath(category, "answers.txt") - if ! anchoredSearch(haystack, needle, 0) { - showPage(w, "Wrong answer", err.Error()) - } - - if err := awardPoints(teamid, category, points); err != nil { - showPage(w, "Error awarding points", err.Error()) - return - } - showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid)) -} - -func puzzlesHandler(w http.ResponseWriter, req *http.Request) { - puzzles := map[string][]interface{}{} - // v := map[string][]interface{}{"Moo": {1, "0177f85ae895a33e2e7c5030c3dc484e8173e55c"}} - // j, _ := json.Marshal(v) - - for _, category := range categories { - - } -} - -func pointsHandler(w http.ResponseWriter, req *http.Request) { - -} - -// staticHandler serves up static files. -func rootHandler(w http.ResponseWriter, req *http.Request) { - if req.URL.Path == "/" { - showPage( - w, - "Welcome", - ` -

Register your team

- -
- Team ID:
- Team name: - -
- -
- If someone on your team has already registered, - proceed to the - puzzles overview. -
- `, - ) - return - } - - http.NotFound(w, req) -} diff --git a/maintenance.go b/maintenance.go deleted file mode 100644 index f352c33..0000000 --- a/maintenance.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "log" - "io/ioutil" - "time" - "os" - "strings" -) - -func cacheMothball(filepath string, categoryName string) { - log.Printf("I'm exploding a mothball %s %s", filepath, categoryName) -} - -// maintenance runs -func tidy() { - // Skip if we've been disabled - if _, err := os.Stat(statePath("disabled")); err == nil { - log.Print("disabled file found, suspending maintenance") - return - } - - // Skip if we've expired - untilspec, err := ioutil.ReadFile(statePath("until")) - if err == nil { - until, err := time.Parse(time.RFC3339, string(untilspec)) - if err != nil { - log.Print("Unparseable date in until file: %s", until) - } else { - if until.Before(time.Now()) { - log.Print("until file time reached, suspending maintenance") - return - } - } - } - - // Get current list of categories - newCategories := []string{} - files, err := ioutil.ReadDir(modulesPath()) - if err != nil { - log.Printf("Error reading packages: %s", err) - } - for _, f := range files { - filename := f.Name() - filepath := modulesPath(filename) - if ! strings.HasSuffix(filename, ".mb") { - continue - } - - categoryName := strings.TrimSuffix(filename, ".mb") - newCategories = append(newCategories, categoryName) - - // Uncompress into cache directory - cacheMothball(filepath, categoryName) - } - categories = newCategories - - collectPoints() -} - -// maintenance is the goroutine that runs a periodic maintenance task -func maintenance(maintenanceInterval time.Duration) { - for ;; time.Sleep(maintenanceInterval) { - tidy() - } -} diff --git a/mothd.go b/mothd.go deleted file mode 100644 index 18f42a6..0000000 --- a/mothd.go +++ /dev/null @@ -1,167 +0,0 @@ -package main - -import ( - "bufio" - "github.com/namsral/flag" - "fmt" - "log" - "net/http" - "os" - "path" - "strings" - "time" -) - -var moduleDir string -var stateDir string -var cacheDir string -var categories = []string{} - -// anchoredSearch looks for needle in filename, -// skipping the first skip space-delimited words -func anchoredSearch(filename string, needle string, skip int) bool { - f, err := os.Open(filename) - if err != nil { - log.Print("Can't open %s: %s", filename, err) - return false - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := scanner.Text() - parts := strings.SplitN(" ", line, skip+1) - if parts[skip+1] == needle { - return true - } - } - - return false -} - -func showPage(w http.ResponseWriter, title string, body string) { - w.WriteHeader(http.StatusOK) - - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "%s", title) - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "

%s

", title) - fmt.Fprintf(w, "
%s
", body) - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") -} - -func modulesPath(parts ...string) string { - tail := path.Join(parts...) - return path.Join(moduleDir, tail) -} - -func statePath(parts ...string) string { - tail := path.Join(parts...) - return path.Join(stateDir, tail) -} - -func cachePath(parts ...string) string { - tail := path.Join(parts...) - return path.Join(cacheDir, tail) -} - -func logRequest(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("HTTP %s %s %s\n", r.RemoteAddr, r.Method, r.URL) - handler.ServeHTTP(w, r) - }) -} - -func setup() error { - // Roll over and die if directories aren't even set up - if _, err := os.Stat(modulesPath()); os.IsNotExist(err) { - return err - } - if _, err := os.Stat(statePath()); os.IsNotExist(err) { - return err - } - if _, err := os.Stat(cachePath()); os.IsNotExist(err) { - return err - } - - // Make sure points directories exist - os.Mkdir(statePath("points.tmp"), 0755) - os.Mkdir(statePath("points.new"), 0755) - - // Preseed available team ids if file doesn't exist - if f, err := os.OpenFile(statePath("teamids.txt"), os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0644); err == nil { - defer f.Close() - for i := 0; i <= 9999; i += 1 { - fmt.Fprintf(f, "%04d\n", i) - } - } - - return nil -} - -func main() { - var maintenanceInterval time.Duration - var listen string - - fs := flag.NewFlagSetWithEnvPrefix(os.Args[0], "MOTH", flag.ExitOnError) - fs.StringVar( - &moduleDir, - "modules", - "/moth/modules", - "Path where your moth modules live", - ) - fs.StringVar( - &stateDir, - "state", - "/moth/state", - "Path where state should be written", - ) - fs.StringVar( - &cacheDir, - "cache", - "/moth/cache", - "Path for ephemeral cache", - ) - fs.DurationVar( - &maintenanceInterval, - "maint", - 20 * time.Second, - "Maintenance interval", - ) - fs.StringVar( - &listen, - "listen", - ":8080", - "[host]:port to bind and listen", - ) - fs.Parse(os.Args[1:]) - - if err := setup(); err != nil { - log.Fatal(err) - } - go maintenance(maintenanceInterval) - - fileserver := http.FileServer(http.Dir(cacheDir)) - http.HandleFunc("/", rootHandler) - http.Handle("/static/", http.StripPrefix("/static", fileserver)) - - http.HandleFunc("/register", registerHandler) - http.HandleFunc("/token", tokenHandler) - http.HandleFunc("/answer", answerHandler) - - http.HandleFunc("/puzzles.json", puzzlesHandler) - http.HandleFunc("/points.json", pointsHandler) - - log.Printf("Listening on %s", listen) - log.Fatal(http.ListenAndServe(listen, logRequest(http.DefaultServeMux))) -} diff --git a/points.go b/points.go deleted file mode 100644 index f6f16c0..0000000 --- a/points.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "io/ioutil" - "log" - "os" - "strconv" - "strings" - "time" -) - -type Award struct { - when time.Time - teamid string - category string - points int -} - -func ParseAward(s string) (*Award, error) { - ret := Award{} - - parts := strings.SplitN(s, " ", 5) - if len(parts) < 4 { - return nil, fmt.Errorf("Malformed award string") - } - - whenEpoch, err := strconv.ParseInt(parts[0], 10, 64) - if (err != nil) { - return nil, fmt.Errorf("Malformed timestamp: %s", parts[0]) - } - ret.when = time.Unix(whenEpoch, 0) - - ret.teamid = parts[1] - ret.category = parts[2] - - points, err := strconv.Atoi(parts[3]) - if (err != nil) { - return nil, fmt.Errorf("Malformed points: %s", parts[3]) - } - ret.points = points - - return &ret, nil -} - -func (a *Award) String() string { - return fmt.Sprintf("%d %s %s %d", a.when.Unix(), a.teamid, a.category, a.points) -} - -func pointsLog() []Award { - var ret []Award - - fn := statePath("points.log") - f, err := os.Open(fn) - if err != nil { - log.Printf("Unable to open %s: %s", fn, err) - return ret - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := scanner.Text() - cur, err := ParseAward(line) - if err != nil { - log.Printf("Skipping malformed award line %s: %s", line, err) - continue - } - ret = append(ret, *cur) - } - - return ret -} - -// awardPoints gives points points to team teamid in category category -func awardPoints(teamid string, category string, points int) error { - fn := fmt.Sprintf("%s-%s-%d", teamid, category, points) - tmpfn := statePath("points.tmp", fn) - newfn := statePath("points.new", fn) - - contents := fmt.Sprintf("%d %s %s %d\n", time.Now().Unix(), teamid, points) - - if err := ioutil.WriteFile(tmpfn, []byte(contents), 0644); err != nil { - return err - } - - if err := os.Rename(tmpfn, newfn); err != nil { - return err - } - - log.Printf("Award %s %s %d", teamid, category, points) - return nil -} - -// collectPoints gathers up files in points.new/ and appends their contents to points.log, -// removing each points.new/ file as it goes. -func collectPoints() { - logf, err := os.OpenFile(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) - return - } - defer logf.Close() - - files, err := ioutil.ReadDir(statePath("points.new")) - if err != nil { - log.Printf("Error reading packages: %s", err) - } - for _, f := range files { - filename := statePath("points.new", f.Name()) - s, err := ioutil.ReadFile(filename) - if err != nil { - log.Printf("Can't read points file %s: %s", filename, err) - continue - } - award, err := ParseAward(string(s)) - if err != nil { - log.Printf("Can't parse award file %s: %s", filename, err) - continue - } - fmt.Fprintf(logf, "%s\n", award.String()) - log.Print(award.String()) - logf.Sync() - if err := os.Remove(filename); err != nil { - log.Printf("Unable to remove %s: %s", filename, err) - } - } -} \ No newline at end of file