From 7b353131e46dd9697e2b1fdf2b797e8c9ede22ca Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 14 Aug 2020 20:28:12 -0600 Subject: [PATCH] Remove v3 code --- cmd/mothdv3/handlers.go | 342 ------------------------------------- cmd/mothdv3/instance.go | 250 --------------------------- cmd/mothdv3/maintenance.go | 333 ------------------------------------ cmd/mothdv3/mothd.go | 92 ---------- 4 files changed, 1017 deletions(-) delete mode 100644 cmd/mothdv3/handlers.go delete mode 100644 cmd/mothdv3/instance.go delete mode 100644 cmd/mothdv3/maintenance.go delete mode 100644 cmd/mothdv3/mothd.go diff --git a/cmd/mothdv3/handlers.go b/cmd/mothdv3/handlers.go deleted file mode 100644 index 76bb4dd..0000000 --- a/cmd/mothdv3/handlers.go +++ /dev/null @@ -1,342 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "path" - "path/filepath" - "regexp" - "strconv" - "strings" -) - - - -// hasLine returns true if line appears in r. -// The entire line must match. -func hasLine(r io.Reader, line string) bool { - scanner := bufio.NewScanner(r) - for scanner.Scan() { - if scanner.Text() == line { - return true - } - } - return false -} - -func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) { - teamName := req.FormValue("name") - teamId := req.FormValue("id") - - if !ctx.ValidTeamId(teamId) { - respond( - w, req, JSendFail, - "Invalid Team ID", - "I don't have a record of that team ID. Maybe you used capital letters accidentally?", - ) - return - } - - f, err := os.OpenFile(ctx.StatePath("teams", teamId), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) - if err != nil { - if os.IsExist(err) { - respond( - w, req, JSendFail, - "Already registered", - "This team ID has already been registered.", - ) - } else { - log.Print(err) - respond( - w, req, JSendFail, - "Registration failed", - "Unable to register. Perhaps a teammate has already registered?", - ) - } - return - } - defer f.Close() - - fmt.Fprintln(f, teamName) - respond( - w, req, JSendSuccess, - "Team registered", - "Your team has been named and you may begin using your team ID!", - ) -} - -func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) { - teamId := req.FormValue("id") - category := req.FormValue("cat") - pointstr := req.FormValue("points") - answer := req.FormValue("answer") - - if !ctx.ValidTeamId(teamId) { - respond( - w, req, JSendFail, - "Invalid team ID", - "That team ID is not valid for this event.", - ) - return - } - if ctx.TooFast(teamId) { - respond( - w, req, JSendFail, - "Submitting too quickly", - "Your team can only submit one answer every %v", ctx.AttemptInterval, - ) - return - } - - if err != nil { - respond( - points, err := strconv.Atoi(pointstr) - w, req, JSendFail, - "Cannot parse point value", - "This doesn't look like an integer: %s", pointstr, - ) - return - } - - haystack, err := ctx.OpenCategoryFile(category, "answers.txt") - if err != nil { - respond( - w, req, JSendFail, - "Cannot list answers", - "Unable to read the list of answers for this category.", - ) - return - } - defer haystack.Close() - - // Look for the answer - needle := fmt.Sprintf("%d %s", points, answer) - if !hasLine(haystack, needle) { - respond( - w, req, JSendFail, - "Wrong answer", - "That is not the correct answer for %s %d.", category, points, - ) - return - } - - if err := ctx.AwardPoints(teamId, category, points); err != nil { - respond( - w, req, JSendError, - "Cannot award points", - "The answer is correct, but there was an error awarding points: %v", err.Error(), - ) - return - } - respond( - w, req, JSendSuccess, - "Points awarded", - fmt.Sprintf("%d points for %s!", points, teamId), - ) -} - -func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) { - teamId := req.FormValue("id") - if _, err := ctx.TeamName(teamId); err != nil { - http.Error(w, "Must provide team ID", http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write(ctx.jPuzzleList) -} - -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(pointsLog) -} - -func (ctx *Instance) contentHandler(w http.ResponseWriter, req *http.Request) { - // Prevent directory traversal - if strings.Contains(req.URL.Path, "/.") { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - // Be clever: use only the last three parts of the path. This may prove to be a bad idea. - parts := strings.Split(req.URL.Path, "/") - if len(parts) < 3 { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - fileName := parts[len(parts)-1] - puzzleId := parts[len(parts)-2] - categoryName := parts[len(parts)-3] - - mb, ok := ctx.categories[categoryName] - if !ok { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - mbFilename := fmt.Sprintf("content/%s/%s", puzzleId, fileName) - mf, err := mb.Open(mbFilename) - if err != nil { - log.Print(err) - http.Error(w, "Not Found", http.StatusNotFound) - return - } - defer mf.Close() - - http.ServeContent(w, req, fileName, mf.ModTime(), mf) -} - -func (ctx *Instance) staticHandler(w http.ResponseWriter, req *http.Request) { - path := req.URL.Path - if strings.Contains(path, "..") { - http.Error(w, "Invalid URL path", http.StatusBadRequest) - return - } - if path == "/" { - path = "/index.html" - } - - f, err := os.Open(ctx.ThemePath(path)) - if err != nil { - http.NotFound(w, req) - return - } - defer f.Close() - - d, err := f.Stat() - if err != nil { - http.NotFound(w, req) - return - } - - http.ServeContent(w, req, path, d.ModTime(), f) -} - -func (ctx *Instance) manifestHandler(w http.ResponseWriter, req *http.Request) { - if !ctx.Runtime.export_manifest { - http.Error(w, "Endpoint disabled", http.StatusForbidden) - return - } - - teamId := req.FormValue("id") - if _, err := ctx.TeamName(teamId); err != nil { - http.Error(w, "Must provide a valid team ID", http.StatusUnauthorized) - return - } - - if req.Method == http.MethodHead { - w.WriteHeader(http.StatusOK) - return - } - - manifest := make([]string, 0) - manifest = append(manifest, "puzzles.json") - manifest = append(manifest, "points.json") - - // Pack up the theme files - theme_root_re := regexp.MustCompile(fmt.Sprintf("^%s/", ctx.ThemeDir)) - filepath.Walk(ctx.ThemeDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if !info.IsDir() { // Only package up files - localized_path := theme_root_re.ReplaceAllLiteralString(path, "") - manifest = append(manifest, localized_path) - } - return nil - }) - - // Package up files for currently-unlocked puzzles in categories - for category_name, category := range ctx.categories { - if _, ok := ctx.MaxPointsUnlocked[category_name]; ok { // Check that the category is actually unlocked. This should never fail, probably - for _, file := range category.zf.File { - parts := strings.Split(file.Name, "/") - - if parts[0] == "content" { // Only pick up content files, not thing like map.txt - for _, puzzlemap := range category.puzzlemap { // Figure out which puzzles are currently unlocked - if puzzlemap.Path == parts[1] && puzzlemap.Points <= ctx.MaxPointsUnlocked[category_name] { - - manifest = append(manifest, path.Join("content", category_name, path.Join(parts[1:]...))) - break - } - } - } - } - } - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - manifest_json, _ := json.Marshal(manifest) - w.Write(manifest_json) -} - -type FurtiveResponseWriter struct { - w http.ResponseWriter - statusCode *int -} - -func (w FurtiveResponseWriter) WriteHeader(statusCode int) { - *w.statusCode = statusCode - w.w.WriteHeader(statusCode) -} - -func (w FurtiveResponseWriter) Write(buf []byte) (n int, err error) { - n, err = w.w.Write(buf) - return -} - -func (w FurtiveResponseWriter) Header() http.Header { - return w.w.Header() -} - -// This gives Instances the signature of http.Handler -func (ctx *Instance) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { - w := FurtiveResponseWriter{ - 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", - clientIP, - r.Method, - r.URL, - *w.statusCode, - ) -} - -func (ctx *Instance) BindHandlers() { - ctx.mux.HandleFunc(ctx.Base+"/", ctx.staticHandler) - ctx.mux.HandleFunc(ctx.Base+"/register", ctx.registerHandler) - ctx.mux.HandleFunc(ctx.Base+"/answer", ctx.answerHandler) - ctx.mux.HandleFunc(ctx.Base+"/content/", ctx.contentHandler) - ctx.mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler) - ctx.mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler) - ctx.mux.HandleFunc(ctx.Base+"/current_manifest.json", ctx.manifestHandler) -} diff --git a/cmd/mothdv3/instance.go b/cmd/mothdv3/instance.go deleted file mode 100644 index 2330105..0000000 --- a/cmd/mothdv3/instance.go +++ /dev/null @@ -1,250 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "io" - "io/ioutil" - "log" - "math/rand" - "net/http" - "os" - "path" - "strings" - "sync" - "time" -) - -type RuntimeConfig struct { - export_manifest bool -} - -type Instance struct { - Base string - MothballDir string - PuzzlesDir string - StateDir string - ThemeDir string - AttemptInterval time.Duration - UseXForwarded bool - - Runtime RuntimeConfig - - categories map[string]*Mothball - MaxPointsUnlocked map[string]int - update chan bool - jPuzzleList []byte - jPointsLog []byte - nextAttempt map[string]time.Time - nextAttemptMutex *sync.RWMutex - mux *http.ServeMux -} - -func (ctx *Instance) Initialize() error { - // Roll over and die if directories aren't even set up - if _, err := os.Stat(ctx.MothballDir); err != nil { - return err - } - if _, err := os.Stat(ctx.StateDir); err != nil { - return err - } - - ctx.Base = strings.TrimRight(ctx.Base, "/") - ctx.categories = map[string]*Zipfs{} - ctx.update = make(chan bool, 10) - ctx.nextAttempt = map[string]time.Time{} - ctx.nextAttemptMutex = new(sync.RWMutex) - ctx.mux = http.NewServeMux() - - ctx.BindHandlers() - ctx.MaybeInitialize() - - return nil -} - -// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift -const distinguishableChars = "234678abcdefhijkmnpqrtwxyz=" - -func mktoken() string { - a := make([]byte, 8) - for i := range a { - char := rand.Intn(len(distinguishableChars)) - a[i] = distinguishableChars[char] - } - return string(a) -} - -func (ctx *Instance) MaybeInitialize() { - // Only do this if it hasn't already been done - if _, err := os.Stat(ctx.StatePath("initialized")); err == nil { - return - } - log.Print("initialized file missing, re-initializing") - - // Remove any extant control and state files - 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")) - - // Make sure various subdirectories exist - os.Mkdir(ctx.StatePath("points.tmp"), 0755) - os.Mkdir(ctx.StatePath("points.new"), 0755) - os.Mkdir(ctx.StatePath("teams"), 0755) - - // Preseed available team ids if file doesn't exist - if f, err := os.OpenFile(ctx.StatePath("teamids.txt"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { - defer f.Close() - for i := 0; i <= 100; i += 1 { - fmt.Fprintln(f, mktoken()) - } - } - - // Create initialized file that signals whether we're set up - f, err := os.OpenFile(ctx.StatePath("initialized"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) - if err != nil { - log.Print(err) - } - defer f.Close() - fmt.Fprintln(f, "Remove this file to reinitialize the contest") -} - -func pathCleanse(parts []string) string { - clean := make([]string, len(parts)) - for i := range parts { - part := parts[i] - part = strings.TrimLeft(part, ".") - if p := strings.LastIndex(part, "/"); p >= 0 { - part = part[p+1:] - } - clean[i] = part - } - return path.Join(clean...) -} - -func (ctx Instance) MothballPath(parts ...string) string { - tail := pathCleanse(parts) - return path.Join(ctx.MothballDir, tail) -} - -func (ctx *Instance) StatePath(parts ...string) string { - tail := pathCleanse(parts) - return path.Join(ctx.StateDir, tail) -} - -func (ctx *Instance) ThemePath(parts ...string) string { - tail := pathCleanse(parts) - return path.Join(ctx.ThemeDir, tail) -} - -func (ctx *Instance) TooFast(teamId string) bool { - now := time.Now() - - ctx.nextAttemptMutex.RLock() - next, _ := ctx.nextAttempt[teamId] - ctx.nextAttemptMutex.RUnlock() - - ctx.nextAttemptMutex.Lock() - ctx.nextAttempt[teamId] = now.Add(ctx.AttemptInterval) - ctx.nextAttemptMutex.Unlock() - - return now.Before(next) -} - -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 awardlist - } - 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 - } - if len(teamId) > 0 && cur.TeamId != teamId { - continue - } - awardlist = append(awardlist, cur) - } - - return awardlist -} - -// AwardPoints gives points to teamId in category. -// It first checks to make sure these are not duplicate points. -// This is not a perfect check, you can trigger a race condition here. -// It's just a courtesy to the user. -// The maintenance task makes sure we never have duplicate points in the log. -func (ctx *Instance) AwardPoints(teamId, category string, points int) error { - a := Award{ - When: time.Now(), - TeamId: teamId, - Category: category, - Points: points, - } - - _, err := ctx.TeamName(teamId) - if err != nil { - return fmt.Errorf("No registered team with this hash") - } - - for _, e := range ctx.PointsLog("") { - if a.Same(e) { - return fmt.Errorf("Points already awarded to this team in this category") - } - } - - fn := fmt.Sprintf("%s-%s-%d", teamId, category, points) - tmpfn := ctx.StatePath("points.tmp", fn) - newfn := ctx.StatePath("points.new", fn) - - if err := ioutil.WriteFile(tmpfn, []byte(a.String()), 0644); err != nil { - return err - } - - if err := os.Rename(tmpfn, newfn); err != nil { - return err - } - - ctx.update <- true - log.Printf("Award %s %s %d", teamId, category, points) - return nil -} - -func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.ReadCloser, error) { - mb, ok := ctx.categories[category] - if !ok { - return nil, fmt.Errorf("No such category: %s", category) - } - - filename := path.Join(parts...) - f, err := mb.Open(filename) - return f, err -} - -func (ctx *Instance) ValidTeamId(teamId string) bool { - ctx.nextAttemptMutex.RLock() - _, ok := ctx.nextAttempt[teamId] - ctx.nextAttemptMutex.RUnlock() - - return ok -} - -func (ctx *Instance) TeamName(teamId string) (string, error) { - teamNameBytes, err := ioutil.ReadFile(ctx.StatePath("teams", teamId)) - teamName := strings.TrimSpace(string(teamNameBytes)) - return teamName, err -} diff --git a/cmd/mothdv3/maintenance.go b/cmd/mothdv3/maintenance.go deleted file mode 100644 index 8f52504..0000000 --- a/cmd/mothdv3/maintenance.go +++ /dev/null @@ -1,333 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "os" - "sort" - "strconv" - "strings" - "time" -) - -func (pm *PuzzleMap) MarshalJSON() ([]byte, error) { - if pm == nil { - return []byte("null"), nil - } - - jPath, err := json.Marshal(pm.Path) - if err != nil { - return nil, err - } - - ret := fmt.Sprintf("[%d,%s]", pm.Points, string(jPath)) - return []byte(ret), nil -} - -func (ctx *Instance) generatePuzzleList() { - maxByCategory := map[string]int{} - for _, a := range ctx.PointsLog("") { - if a.Points > maxByCategory[a.Category] { - maxByCategory[a.Category] = a.Points - } - } - - ret := map[string][]PuzzleMap{} - for catName, mb := range ctx.categories { - filtered_puzzlemap := make([]PuzzleMap, 0, 30) - completed := true - - for _, pm := range mb.puzzlemap { - filtered_puzzlemap = append(filtered_puzzlemap, pm) - - if pm.Points > maxByCategory[catName] { - completed = false - maxByCategory[catName] = pm.Points - break - } - } - - if completed { - filtered_puzzlemap = append(filtered_puzzlemap, PuzzleMap{0, ""}) - } - - ret[catName] = filtered_puzzlemap - } - - // Cache the unlocked points for use in other functions - ctx.MaxPointsUnlocked = maxByCategory - - jpl, err := json.Marshal(ret) - if err != nil { - log.Printf("Marshalling puzzles.js: %v", err) - return - } - ctx.jPuzzleList = jpl -} - -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(teamId) - - teamNumbersById := map[string]int{} - for nr, a := range ret.Points { - teamNumber, ok := teamNumbersById[a.TeamId] - if !ok { - teamName, err := ctx.TeamName(a.TeamId) - if err != nil { - teamName = "Rodney" // https://en.wikipedia.org/wiki/Rogue_(video_game)#Gameplay - } - teamNumber = nr - teamNumbersById[a.TeamId] = teamNumber - ret.Teams[strconv.FormatInt(int64(teamNumber), 16)] = teamName - } - a.TeamId = strconv.FormatInt(int64(teamNumber), 16) - } - - jpl, err := json.Marshal(ret) - if err != nil { - log.Printf("Marshalling points.js: %v", err) - return nil - } - - if len(teamId) == 0 { - ctx.jPointsLog = jpl - } - return jpl -} - -// maintenance runs -func (ctx *Instance) tidy() { - // Do they want to reset everything? - ctx.MaybeInitialize() - - // Check set config - ctx.UpdateConfig() - - // Refresh all current categories - for categoryName, mb := range ctx.categories { - if err := mb.Refresh(); err != nil { - // Backing file vanished: remove this category - log.Printf("Removing category: %s: %s", categoryName, err) - mb.Close() - delete(ctx.categories, categoryName) - } - } - - // Any new categories? - files, err := ioutil.ReadDir(ctx.MothballPath()) - if err != nil { - log.Printf("Error listing mothballs: %s", err) - } - for _, f := range files { - filename := f.Name() - filepath := ctx.MothballPath(filename) - if !strings.HasSuffix(filename, ".mb") { - continue - } - categoryName := strings.TrimSuffix(filename, ".mb") - - if _, ok := ctx.categories[categoryName]; !ok { - mb, err := OpenZipfs(filepath) - if err != nil { - log.Printf("Error opening %s: %s", filepath, err) - continue - } - log.Printf("New category: %s", filename) - ctx.categories[categoryName] = mb - } - } -} - -// readTeams reads in the list of team IDs, -// so we can quickly validate them. -func (ctx *Instance) readTeams() { - filepath := ctx.StatePath("teamids.txt") - teamids, err := os.Open(filepath) - if err != nil { - log.Printf("Error openining %s: %s", filepath, err) - return - } - defer teamids.Close() - - // List out team IDs - newList := map[string]bool{} - scanner := bufio.NewScanner(teamids) - for scanner.Scan() { - teamId := scanner.Text() - if (teamId == "..") || strings.ContainsAny(teamId, "/") { - log.Printf("Dangerous team ID dropped: %s", teamId) - continue - } - newList[scanner.Text()] = true - } - - // For any new team IDs, set their next attempt time to right now - now := time.Now() - added := 0 - for k, _ := range newList { - ctx.nextAttemptMutex.RLock() - _, ok := ctx.nextAttempt[k] - ctx.nextAttemptMutex.RUnlock() - - if !ok { - ctx.nextAttemptMutex.Lock() - ctx.nextAttempt[k] = now - ctx.nextAttemptMutex.Unlock() - - added += 1 - } - } - - // For any removed team IDs, remove them - removed := 0 - ctx.nextAttemptMutex.Lock() // XXX: This could be less of a cludgel - for k, _ := range ctx.nextAttempt { - if _, ok := newList[k]; !ok { - delete(ctx.nextAttempt, k) - } - } - ctx.nextAttemptMutex.Unlock() - - if (added > 0) || (removed > 0) { - log.Printf("Team IDs updated: %d added, %d removed", added, removed) - } -} - -// 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() { - 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 - } - - 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) - 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 - } - - duplicate := false - for _, e := range points { - if award.Same(e) { - duplicate = true - break - } - } - - if duplicate { - log.Printf("Skipping duplicate points: %s", award.String()) - } else { - points = append(points, award) - } - removearino = append(removearino, filename) - } - - 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) - } - } -} - -func (ctx *Instance) isEnabled() bool { - // Skip if we've been disabled - if _, err := os.Stat(ctx.StatePath("disabled")); err == nil { - log.Print("Suspended: disabled file found") - return false - } - - untilspec, err := ioutil.ReadFile(ctx.StatePath("until")) - if err == nil { - untilspecs := strings.TrimSpace(string(untilspec)) - until, err := time.Parse(time.RFC3339, untilspecs) - if err != nil { - log.Printf("Suspended: Unparseable until date: %s", untilspec) - return false - } - if until.Before(time.Now()) { - log.Print("Suspended: until time reached, suspending maintenance") - return false - } - } - - return true -} - -func (ctx *Instance) UpdateConfig() { - // Handle export manifest - if _, err := os.Stat(ctx.StatePath("export_manifest")); err == nil { - if !ctx.Runtime.export_manifest { - log.Print("Enabling manifest export") - ctx.Runtime.export_manifest = true - } - } else if ctx.Runtime.export_manifest { - log.Print("Disabling manifest export") - ctx.Runtime.export_manifest = false - } - -} - -// maintenance is the goroutine that runs a periodic maintenance task -func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) { - for { - if ctx.isEnabled() { - ctx.tidy() - ctx.readTeams() - ctx.collectPoints() - ctx.generatePuzzleList() - ctx.generatePointsLog("") - } - select { - case <-ctx.update: - // log.Print("Forced update") - case <-time.After(maintenanceInterval): - // log.Print("Housekeeping...") - } - } -} diff --git a/cmd/mothdv3/mothd.go b/cmd/mothdv3/mothd.go deleted file mode 100644 index 0db9632..0000000 --- a/cmd/mothdv3/mothd.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "flag" - "log" - "math/rand" - "mime" - "net/http" - "time" -) - -func setup() error { - rand.Seed(time.Now().UnixNano()) - return nil -} - -func main() { - ctx := &Instance{} - - flag.StringVar( - &ctx.Base, - "base", - "/", - "Base URL of this instance", - ) - flag.StringVar( - &ctx.MothballDir, - "mothballs", - "/mothballs", - "Path to read mothballs", - ) - flag.StringVar( - &ctx.PuzzlesDir, - "puzzles", - "", - "Path to read puzzle source trees", - ) - flag.StringVar( - &ctx.StateDir, - "state", - "/state", - "Path to write state", - ) - flag.StringVar( - &ctx.ThemeDir, - "theme", - "/theme", - "Path to static theme resources (HTML, images, css, ...)", - ) - flag.DurationVar( - &ctx.AttemptInterval, - "attempt", - 500*time.Millisecond, - "Per-team time required between answer attempts", - ) - maintenanceInterval := flag.Duration( - "maint", - 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", - "[host]:port to bind and listen", - ) - flag.Parse() - - if err := setup(); err != nil { - log.Fatal(err) - } - - err := ctx.Initialize() - if err != nil { - log.Fatal(err) - } - - // Add some MIME extensions - // Doing this avoids decompressing a mothball entry twice per request - mime.AddExtensionType(".json", "application/json") - mime.AddExtensionType(".zip", "application/zip") - - go ctx.Maintenance(*maintenanceInterval) - - log.Printf("Listening on %s", *listen) - log.Fatal(http.ListenAndServe(*listen, ctx)) -}