diff --git a/handlers.go b/handlers.go index e9f430f..132a821 100644 --- a/handlers.go +++ b/handlers.go @@ -22,7 +22,7 @@ func registerHandler(w http.ResponseWriter, req *http.Request) { return } - if ! anchoredSearch(statePath("assigned.txt"), teamid, 0) { + 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 } @@ -97,7 +97,7 @@ func answerHandler(w http.ResponseWriter, req *http.Request) { // Check answer needle := fmt.Sprintf("%s %s", points, answer) - haystack := mothPath("packages", category, "answers.txt") + haystack := cachePath(category, "answers.txt") if ! anchoredSearch(haystack, needle, 0) { showPage(w, "Wrong answer", err.Error()) } @@ -109,3 +109,30 @@ func answerHandler(w http.ResponseWriter, req *http.Request) { showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid)) } +// 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 index 93fca5a..c787318 100644 --- a/maintenance.go +++ b/maintenance.go @@ -15,36 +15,36 @@ func cacheMothball(filepath string, categoryName string) { // maintenance runs func tidy() { // Skip if we've been disabled - if exists(statePath("disabled")) { + if _, err := os.Stat(statePath("disabled")); err == nil { log.Print("disabled file found, suspending maintenance") return } // Skip if we've expired - untilspec, _ := ioutil.ReadFile(statePath("until")) - until, err := time.Parse(time.RFC3339, string(untilspec)) + untilspec, err := ioutil.ReadFile(statePath("until")) if err == nil { - if until.Before(time.Now()) { - log.Print("until file time reached, suspending maintenance") - return + 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 + } } } log.Print("Hello, I'm maintaining!") - // Make sure points directories exist - os.Mkdir(statePath("points.tmp"), 0755) - os.Mkdir(statePath("points.new"), 0755) - // Get current list of categories newCategories := []string{} - files, err := ioutil.ReadDir(mothPath("packages")) + files, err := ioutil.ReadDir(modulesPath()) if err != nil { log.Printf("Error reading packages: %s", err) } for _, f := range files { filename := f.Name() - filepath := mothPath("packages", filename) + filepath := modulesPath(filename) if ! strings.HasSuffix(filename, ".mb") { continue } @@ -61,7 +61,7 @@ func tidy() { } // maintenance is the goroutine that runs a periodic maintenance task -func maintenance() { +func maintenance(maintenanceInterval time.Duration) { for ;; time.Sleep(maintenanceInterval) { tidy() } diff --git a/mothd.go b/mothd.go index 08e0e99..b7d2909 100644 --- a/mothd.go +++ b/mothd.go @@ -2,18 +2,19 @@ package main import ( "bufio" + "flag" "fmt" "log" "net/http" - "io/ioutil" "os" "path" "strings" "time" ) -var basePath = "/home/neale/src/moth" -var maintenanceInterval = 20 * time.Second +var moduleDir string +var stateDir string +var cacheDir string var categories = []string{} // anchoredSearch looks for needle in filename, @@ -38,25 +39,6 @@ func anchoredSearch(filename string, needle string, skip int) bool { return false } - -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 - } - - return nil -} - func showPage(w http.ResponseWriter, title string, body string) { w.WriteHeader(http.StatusOK) @@ -77,30 +59,97 @@ func showPage(w http.ResponseWriter, title string, body string) { fmt.Fprintf(w, "") } -func mothPath(parts ...string) string { +func modulesPath(parts ...string) string { tail := path.Join(parts...) - return path.Join(basePath, tail) + return path.Join(moduleDir, tail) } func statePath(parts ...string) string { tail := path.Join(parts...) - return path.Join(basePath, "state", tail) + return path.Join(stateDir, tail) } -func exists(filename string) bool { - if _, err := os.Stat(filename); os.IsNotExist(err) { - return false; +func cachePath(parts ...string) string { + tail := path.Join(parts...) + return path.Join(cacheDir, tail) +} + +func setup() error { + // Roll over and die if directories aren't even set up + if _, err := os.Stat(modulesPath()); os.IsNotExist(err) { + return err } - return true; + 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 logRequest(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + }) } func main() { - log.Print("Sup") - go maintenance(); + flag.StringVar( + &moduleDir, + "modules", + "/modules", + "Path where your moth modules live", + ) + flag.StringVar( + &stateDir, + "state", + "/state", + "Path where state should be written", + ) + flag.StringVar( + &cacheDir, + "cache", + "/cache", + "Path for ephemeral cache", + ) + maintenanceInterval := flag.Duration( + "maint", + 20 * time.Second, + "Maintenance interval", + ) + listen := flag.String( + "listen", + ":8080", + "[host]:port to bind and listen", + ) + + if err := setup(); err != nil { + log.Fatal(err) + } + go maintenance(*maintenanceInterval) + + http.HandleFunc("/", rootHandler) + http.Handle("/static/", http.FileServer(http.Dir(cacheDir))) + http.HandleFunc("/register", registerHandler) http.HandleFunc("/token", tokenHandler) http.HandleFunc("/answer", answerHandler) - log.Fatal(http.ListenAndServe(":8080", nil)) -} -// docker run --rm -it -p 5880:8080 -v $HOME:$HOME:ro -w $(pwd) golang go run mothd.go + log.Printf("Listening on %s", *listen) + log.Fatal(http.ListenAndServe(*listen, logRequest(http.DefaultServeMux))) +} diff --git a/points.go b/points.go index 9c3e2e2..67817e3 100644 --- a/points.go +++ b/points.go @@ -73,6 +73,25 @@ func pointsLog() []Award { 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 + } + + 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() {