mirror of https://github.com/dirtbags/moth.git
A passable start at a go-based mothd
This commit is contained in:
parent
789fd00e47
commit
61c1129e0f
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
167
mothd.go
167
mothd.go
|
@ -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, "<!DOCTYPE html>")
|
|
||||||
fmt.Fprintf(w, "<html><head>")
|
|
||||||
fmt.Fprintf(w, "<title>%s</title>", title)
|
|
||||||
fmt.Fprintf(w, "<link rel=\"stylesheet\" href=\"static/style.css\">")
|
|
||||||
fmt.Fprintf(w, "<meta name=\"viewport\" content=\"width=device-width\"></head>")
|
|
||||||
fmt.Fprintf(w, "<link rel=\"icon\" href=\"res/luna-moth.svg\" type=\"image/svg+xml\">")
|
|
||||||
fmt.Fprintf(w, "<link rel=\"icon\" href=\"res/luna-moth.png\" type=\"image/png\">")
|
|
||||||
fmt.Fprintf(w, "<body><h1>%s</h1>", title)
|
|
||||||
fmt.Fprintf(w, "<section>%s</section>", body)
|
|
||||||
fmt.Fprintf(w, "<nav>")
|
|
||||||
fmt.Fprintf(w, "<ul>")
|
|
||||||
fmt.Fprintf(w, "<li><a href=\"static/puzzles.html\">Puzzles</a></li>")
|
|
||||||
fmt.Fprintf(w, "<li><a href=\"static/scoreboard.html\">Scoreboard</a></li>")
|
|
||||||
fmt.Fprintf(w, "</ul>")
|
|
||||||
fmt.Fprintf(w, "</nav>")
|
|
||||||
fmt.Fprintf(w, "</body></html>")
|
|
||||||
}
|
|
||||||
|
|
||||||
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)))
|
|
||||||
}
|
|
129
points.go
129
points.go
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Award struct {
|
||||||
|
When time.Time
|
||||||
|
TeamId string
|
||||||
|
Category string
|
||||||
|
Points int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Award) String() string {
|
||||||
|
return fmt.Sprintf("%d %s %s %d", a.When.Unix(), a.TeamId, a.Category, a.Points)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAward(t *testing.T) {
|
||||||
|
entry := "1536958399 1a2b3c4d counting 1"
|
||||||
|
a, err := ParseAward(entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.TeamId != "1a2b3c4d" {
|
||||||
|
t.Error("TeamID parsed wrong")
|
||||||
|
}
|
||||||
|
if a.Category != "counting" {
|
||||||
|
t.Error("Category parsed wrong")
|
||||||
|
}
|
||||||
|
if a.Points != 1 {
|
||||||
|
t.Error("Points parsed wrong")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.String() != entry {
|
||||||
|
t.Error("String conversion wonky")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ParseAward("bad bad bad 1"); err == nil {
|
||||||
|
t.Error("Not throwing error on bad timestamp")
|
||||||
|
}
|
||||||
|
if _, err := ParseAward("1 bad bad bad"); err == nil {
|
||||||
|
t.Error("Not throwing error on bad points")
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,9 +7,38 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"bufio"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerHandler(w http.ResponseWriter, req *http.Request) {
|
// anchoredSearch looks for needle in r,
|
||||||
|
// skipping the first skip space-delimited words
|
||||||
|
func anchoredSearch(r io.Reader, needle string, skip int) bool {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
parts := strings.SplitN(line, " ", skip+1)
|
||||||
|
if (len(parts) > skip) && (parts[skip] == needle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func anchoredSearchFile(filename string, needle string, skip int) bool {
|
||||||
|
r, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
return anchoredSearch(r, needle, skip)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (ctx Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
teamname := req.FormValue("n")
|
teamname := req.FormValue("n")
|
||||||
teamid := req.FormValue("h")
|
teamid := req.FormValue("h")
|
||||||
|
|
||||||
|
@ -22,12 +51,12 @@ func registerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ! anchoredSearch(statePath("teamids.txt"), teamid, 0) {
|
if ! anchoredSearchFile(ctx.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?")
|
showPage(w, "Invalid Team ID", "I don't have a record of that team ID. Maybe you used capital letters accidentally?")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(statePath("state", teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
f, err := os.OpenFile(ctx.StatePath(teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
showPage(
|
showPage(
|
||||||
w,
|
w,
|
||||||
|
@ -41,12 +70,12 @@ func registerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
showPage(w, "Success", "Okay, your team has been named and you may begin using your team ID!")
|
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) {
|
func (ctx Instance) tokenHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
teamid := req.FormValue("t")
|
teamid := req.FormValue("t")
|
||||||
token := req.FormValue("k")
|
token := req.FormValue("k")
|
||||||
|
|
||||||
// Check answer
|
// Check answer
|
||||||
if ! anchoredSearch(token, statePath("tokens.txt"), 0) {
|
if ! anchoredSearchFile(ctx.StatePath("tokens.txt"), token, 0) {
|
||||||
showPage(w, "Unrecognized token", "I don't recognize that token. Did you type in the whole thing?")
|
showPage(w, "Unrecognized token", "I don't recognize that token. Did you type in the whole thing?")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -72,14 +101,14 @@ func tokenHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := awardPoints(teamid, category, points); err != nil {
|
if err := ctx.AwardPoints(teamid, category, points); err != nil {
|
||||||
showPage(w, "Error awarding points", err.Error())
|
showPage(w, "Error awarding points", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
|
showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
|
||||||
}
|
}
|
||||||
|
|
||||||
func answerHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
teamid := req.FormValue("t")
|
teamid := req.FormValue("t")
|
||||||
category := req.FormValue("c")
|
category := req.FormValue("c")
|
||||||
pointstr := req.FormValue("p")
|
pointstr := req.FormValue("p")
|
||||||
|
@ -90,41 +119,51 @@ func answerHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
points = 0
|
points = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defang category name; prevent directory traversal
|
catmb, ok := ctx.Categories[category]
|
||||||
if matched, _ := regexp.MatchString("^[A-Za-z0-9_-]", category); matched {
|
if ! ok {
|
||||||
category = ""
|
showPage(w, "Category does not exist", "The specified category does not exist. Sorry!")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check answer
|
// Get the answers
|
||||||
needle := fmt.Sprintf("%s %s", points, answer)
|
haystack, err := catmb.Open("answers.txt")
|
||||||
haystack := cachePath(category, "answers.txt")
|
if err != nil {
|
||||||
|
showPage(w, "Answers do not exist",
|
||||||
|
"Please tell the contest people that the mothball for this category has no answers.txt in it!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer haystack.Close()
|
||||||
|
|
||||||
|
// Look for the answer
|
||||||
|
needle := fmt.Sprintf("%d %s", points, answer)
|
||||||
if ! anchoredSearch(haystack, needle, 0) {
|
if ! anchoredSearch(haystack, needle, 0) {
|
||||||
showPage(w, "Wrong answer", err.Error())
|
showPage(w, "Wrong answer", err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := awardPoints(teamid, category, points); err != nil {
|
if err := ctx.AwardPoints(teamid, category, points); err != nil {
|
||||||
showPage(w, "Error awarding points", err.Error())
|
showPage(w, "Error awarding points", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
|
showPage(w, "Points awarded", fmt.Sprintf("%d points for %s!", points, teamid))
|
||||||
}
|
}
|
||||||
|
|
||||||
func puzzlesHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
puzzles := map[string][]interface{}{}
|
puzzles := map[string][]interface{}{}
|
||||||
// v := map[string][]interface{}{"Moo": {1, "0177f85ae895a33e2e7c5030c3dc484e8173e55c"}}
|
// v := map[string][]interface{}{"Moo": {1, "0177f85ae895a33e2e7c5030c3dc484e8173e55c"}}
|
||||||
// j, _ := json.Marshal(v)
|
// j, _ := json.Marshal(v)
|
||||||
|
|
||||||
for _, category := range categories {
|
for _, category := range ctx.Categories {
|
||||||
|
log.Print(puzzles, category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pointsHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// staticHandler serves up static files.
|
// staticHandler serves up static files.
|
||||||
func rootHandler(w http.ResponseWriter, req *http.Request) {
|
func (ctx Instance) rootHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.URL.Path == "/" {
|
if req.URL.Path == "/" {
|
||||||
showPage(
|
showPage(
|
||||||
w,
|
w,
|
||||||
|
@ -150,3 +189,12 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx Instance) BindHandlers(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc(ctx.Base + "/", ctx.rootHandler)
|
||||||
|
mux.HandleFunc(ctx.Base + "/register", ctx.registerHandler)
|
||||||
|
mux.HandleFunc(ctx.Base + "/token", ctx.tokenHandler)
|
||||||
|
mux.HandleFunc(ctx.Base + "/answer", ctx.answerHandler)
|
||||||
|
mux.HandleFunc(ctx.Base + "/puzzles.json", ctx.puzzlesHandler)
|
||||||
|
mux.HandleFunc(ctx.Base + "/points.json", ctx.pointsHandler)
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"log"
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Instance struct {
|
||||||
|
Base string
|
||||||
|
MothballDir string
|
||||||
|
StateDir string
|
||||||
|
Categories map[string]*Mothball
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInstance(base, mothballDir, stateDir string) (*Instance, error) {
|
||||||
|
ctx := &Instance{
|
||||||
|
Base: strings.TrimRight(base, "/"),
|
||||||
|
MothballDir: mothballDir,
|
||||||
|
StateDir: stateDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll over and die if directories aren't even set up
|
||||||
|
if _, err := os.Stat(mothballDir); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(stateDir); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Initialize()
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) Initialize () {
|
||||||
|
// Make sure points directories exist
|
||||||
|
os.Mkdir(ctx.StatePath("points.tmp"), 0755)
|
||||||
|
os.Mkdir(ctx.StatePath("points.new"), 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 <= 9999; i += 1 {
|
||||||
|
fmt.Fprintf(f, "%04d\n", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, err := os.OpenFile(ctx.StatePath("initialized"), os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0644); err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
fmt.Println("Remove this file to reinitialize the contest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx Instance) MothballPath(parts ...string) string {
|
||||||
|
tail := path.Join(parts...)
|
||||||
|
return path.Join(ctx.MothballDir, tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Instance) StatePath(parts ...string) string {
|
||||||
|
tail := path.Join(parts...)
|
||||||
|
return path.Join(ctx.StateDir, tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (ctx *Instance) PointsLog() []Award {
|
||||||
|
var ret []Award
|
||||||
|
|
||||||
|
fn := ctx.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 (ctx *Instance) AwardPoints(teamid string, category string, points int) error {
|
||||||
|
fn := fmt.Sprintf("%s-%s-%d", teamid, category, points)
|
||||||
|
tmpfn := ctx.StatePath("points.tmp", fn)
|
||||||
|
newfn := ctx.StatePath("points.new", fn)
|
||||||
|
|
||||||
|
contents := fmt.Sprintf("%d %s %s %d\n", time.Now().Unix(), teamid, category, 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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package main
|
|
@ -0,0 +1,105 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maintenance runs
|
||||||
|
func (ctx *Instance) Tidy() {
|
||||||
|
// Skip if we've been disabled
|
||||||
|
if _, err := os.Stat(ctx.StatePath("disabled")); err == nil {
|
||||||
|
log.Print("disabled file found, suspending maintenance")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if we've expired
|
||||||
|
untilspec, err := ioutil.ReadFile(ctx.StatePath("until"))
|
||||||
|
if err == nil {
|
||||||
|
until, err := time.Parse(time.RFC3339, string(untilspec))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unparseable date in until file: %v", until)
|
||||||
|
} else {
|
||||||
|
if until.Before(time.Now()) {
|
||||||
|
log.Print("until file time reached, suspending maintenance")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := OpenMothball(filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error opening %s: %s", filepath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ctx.Categories[categoryName] = mb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any old categories?
|
||||||
|
log.Print("XXX: Check for and reap old categories")
|
||||||
|
|
||||||
|
ctx.CollectPoints()
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectPoints gathers up files in points.new/ and appends their contents to points.log,
|
||||||
|
// removing each points.new/ file as it goes.
|
||||||
|
func (ctx *Instance) CollectPoints() {
|
||||||
|
logf, err := os.OpenFile(ctx.StatePath("points.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Can't append to points log: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer logf.Close()
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(ctx.StatePath("points.new"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading packages: %s", err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
fmt.Fprintf(logf, "%s\n", award.String())
|
||||||
|
log.Print("XXX: check for duplicates", award.String())
|
||||||
|
logf.Sync()
|
||||||
|
if err := os.Remove(filename); err != nil {
|
||||||
|
log.Printf("Unable to remove %s: %s", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// maintenance is the goroutine that runs a periodic maintenance task
|
||||||
|
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
|
||||||
|
for ;; time.Sleep(maintenanceInterval) {
|
||||||
|
ctx.Tidy()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
#! /bin/sh
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
/moth/bin/once
|
|
||||||
sleep 20
|
|
||||||
done &
|
|
||||||
|
|
||||||
cd /moth/www
|
|
||||||
s6-tcpserver -u $(id -u www) -g $(id -g www) 0.0.0.0 80 /usr/bin/eris -c -d -.
|
|
|
@ -1,9 +1,10 @@
|
||||||
package mothball
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -14,7 +15,7 @@ type Mothball struct {
|
||||||
mtime time.Time
|
mtime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(filename string) (*Mothball, error) {
|
func OpenMothball(filename string) (*Mothball, error) {
|
||||||
var m Mothball
|
var m Mothball
|
||||||
|
|
||||||
m.filename = filename
|
m.filename = filename
|
||||||
|
@ -38,7 +39,7 @@ func (m *Mothball) Refresh() (error) {
|
||||||
}
|
}
|
||||||
mtime := info.ModTime()
|
mtime := info.ModTime()
|
||||||
|
|
||||||
if mtime == m.mtime {
|
if ! mtime.After(m.mtime) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,3 +66,14 @@ func (m *Mothball) Open(filename string) (io.ReadCloser, error) {
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("File not found: %s in %s", filename, m.filename)
|
return nil, fmt.Errorf("File not found: %s in %s", filename, m.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Mothball) ReadFile(filename string) ([]byte, error) {
|
||||||
|
f, err := m.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
bytes, err := ioutil.ReadAll(f)
|
||||||
|
return bytes, err
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package mothball
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
@ -35,7 +35,7 @@ func TestMothball(t *testing.T) {
|
||||||
tf.Close()
|
tf.Close()
|
||||||
|
|
||||||
// Now read it in
|
// Now read it in
|
||||||
mb, err := Open(tf.Name())
|
mb, err := OpenMothball(tf.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
14
src/mothd
14
src/mothd
|
@ -1,14 +0,0 @@
|
||||||
#! /bin/sh
|
|
||||||
|
|
||||||
cd ${1:-$(dirname $0)}
|
|
||||||
KOTH_BASE=$(pwd)
|
|
||||||
|
|
||||||
echo "Running koth instances in $KOTH_BASE"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
for i in $KOTH_BASE/*/assigned.txt; do
|
|
||||||
dir=${i%/*}
|
|
||||||
$dir/bin/once
|
|
||||||
done
|
|
||||||
sleep 5
|
|
||||||
done
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func showPage(w http.ResponseWriter, title string, body string) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "<!DOCTYPE html>")
|
||||||
|
fmt.Fprintf(w, "<html><head>")
|
||||||
|
fmt.Fprintf(w, "<title>%s</title>", title)
|
||||||
|
fmt.Fprintf(w, "<link rel=\"stylesheet\" href=\"static/style.css\">")
|
||||||
|
fmt.Fprintf(w, "<meta name=\"viewport\" content=\"width=device-width\"></head>")
|
||||||
|
fmt.Fprintf(w, "<link rel=\"icon\" href=\"res/luna-moth.svg\" type=\"image/svg+xml\">")
|
||||||
|
fmt.Fprintf(w, "<link rel=\"icon\" href=\"res/luna-moth.png\" type=\"image/png\">")
|
||||||
|
fmt.Fprintf(w, "<body><h1>%s</h1>", title)
|
||||||
|
fmt.Fprintf(w, "<section>%s</section>", body)
|
||||||
|
fmt.Fprintf(w, "<nav>")
|
||||||
|
fmt.Fprintf(w, "<ul>")
|
||||||
|
fmt.Fprintf(w, "<li><a href=\"static/puzzles.html\">Puzzles</a></li>")
|
||||||
|
fmt.Fprintf(w, "<li><a href=\"static/scoreboard.html\">Scoreboard</a></li>")
|
||||||
|
fmt.Fprintf(w, "</ul>")
|
||||||
|
fmt.Fprintf(w, "</nav>")
|
||||||
|
fmt.Fprintf(w, "</body></html>")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
base := flag.String(
|
||||||
|
"base",
|
||||||
|
"/",
|
||||||
|
"Base URL of this instance",
|
||||||
|
)
|
||||||
|
mothballDir := flag.String(
|
||||||
|
"mothballs",
|
||||||
|
"/moth/mothballs",
|
||||||
|
"Path to read mothballs",
|
||||||
|
)
|
||||||
|
stateDir := flag.String(
|
||||||
|
"state",
|
||||||
|
"/moth/state",
|
||||||
|
"Path to write state",
|
||||||
|
)
|
||||||
|
maintenanceInterval := flag.Duration(
|
||||||
|
"maint",
|
||||||
|
20 * time.Second,
|
||||||
|
"Maintenance interval",
|
||||||
|
)
|
||||||
|
listen := flag.String(
|
||||||
|
"listen",
|
||||||
|
":80",
|
||||||
|
"[host]:port to bind and listen",
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := setup(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, err := NewInstance(*base, *mothballDir, *stateDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx.BindHandlers(http.DefaultServeMux)
|
||||||
|
|
||||||
|
go ctx.Maintenance(*maintenanceInterval)
|
||||||
|
|
||||||
|
log.Printf("Listening on %s", *listen)
|
||||||
|
log.Fatal(http.ListenAndServe(*listen, logRequest(http.DefaultServeMux)))
|
||||||
|
}
|
Loading…
Reference in New Issue