A passable start at a go-based mothd

This commit is contained in:
Neale Pickett 2018-09-15 00:24:48 +00:00
parent e45e460fe0
commit 5bb050166e
14 changed files with 470 additions and 409 deletions

View File

@ -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
View File

@ -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
View File

@ -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)
}
}
}

46
src/award.go Normal file
View File

@ -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
}

34
src/award_test.go Normal file
View File

@ -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")
}
}

View File

@ -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)
}

114
src/instance.go Normal file
View File

@ -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
}

1
src/instance_test.go Normal file
View File

@ -0,0 +1 @@
package main

105
src/maintenance.go Normal file
View File

@ -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()
}
}

View File

@ -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 -.

View File

@ -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
}

View File

@ -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

View File

@ -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

86
src/mothd.go Normal file
View File

@ -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)))
}