diff --git a/res/puzzles.html b/res/puzzle-list.html
similarity index 91%
rename from res/puzzles.html
rename to res/puzzle-list.html
index 4d1942e..32f79dc 100644
--- a/res/puzzles.html
+++ b/res/puzzle-list.html
@@ -38,12 +38,12 @@ function render(obj) {
l.appendChild(i);
if (points === 0) {
- i.textContent = "‡";
+ i.textContent = "✿";
} else {
var a = document.createElement('a');
i.appendChild(a);
a.textContent = points;
- a.href = cat + "/" + id + "/index.html";
+ a.href = "puzzle.html?cat=" + cat + "&points=" + points + "&pid=" + id;
}
}
@@ -73,7 +73,7 @@ document.addEventListener("DOMContentLoaded", init);
diff --git a/res/scoreboard.html b/res/scoreboard.html
index e16abf6..a50d9f1 100644
--- a/res/scoreboard.html
+++ b/res/scoreboard.html
@@ -140,7 +140,7 @@ document.addEventListener("DOMContentLoaded", init);
diff --git a/src/handlers.go b/src/handlers.go
index cdd6254..4750c70 100644
--- a/src/handlers.go
+++ b/src/handlers.go
@@ -318,6 +318,42 @@ func (ctx Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
w.Write(jret)
}
+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) {
ServeStatic(w, req, ctx.ResourcesDir)
}
@@ -327,6 +363,7 @@ func (ctx Instance) BindHandlers(mux *http.ServeMux) {
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+"/content/", ctx.contentHandler)
mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
}
diff --git a/src/instance.go b/src/instance.go
index bad4112..267a85e 100644
--- a/src/instance.go
+++ b/src/instance.go
@@ -18,6 +18,7 @@ type Instance struct {
StateDir string
ResourcesDir string
Categories map[string]*Mothball
+ update chan bool
}
func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) {
@@ -27,6 +28,7 @@ func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, e
StateDir: stateDir,
ResourcesDir: resourcesDir,
Categories: map[string]*Mothball{},
+ update: make(chan bool, 10),
}
// Roll over and die if directories aren't even set up
@@ -145,6 +147,7 @@ func (ctx *Instance) AwardPoints(teamid, category string, points int) error {
return err
}
+ ctx.update <- true
log.Printf("Award %s %s %d", teamid, category, points)
return nil
}
diff --git a/src/maintenance.go b/src/maintenance.go
index 501e140..fa7b0cd 100644
--- a/src/maintenance.go
+++ b/src/maintenance.go
@@ -121,7 +121,13 @@ func (ctx *Instance) CollectPoints() {
// maintenance is the goroutine that runs a periodic maintenance task
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
- for ; ; time.Sleep(maintenanceInterval) {
+ for {
ctx.Tidy()
+ select {
+ case <-ctx.update:
+ log.Print("Forced update")
+ case <-time.After(maintenanceInterval):
+ log.Print("Housekeeping...")
+ }
}
}
diff --git a/src/mothball.go b/src/mothball.go
index 4f18257..4e2a962 100644
--- a/src/mothball.go
+++ b/src/mothball.go
@@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"os"
+ "strings"
"time"
)
@@ -15,6 +16,101 @@ type Mothball struct {
mtime time.Time
}
+type MothballFile struct {
+ f io.ReadCloser
+ pos int64
+ zf *zip.File
+ io.Reader
+ io.Seeker
+ io.Closer
+}
+
+func NewMothballFile(zf *zip.File) (*MothballFile, error) {
+ mf := &MothballFile{
+ zf: zf,
+ pos: 0,
+ f: nil,
+ }
+ if err := mf.reopen(); err != nil {
+ return nil, err
+ }
+ return mf, nil
+}
+
+func (mf *MothballFile) reopen() error {
+ if mf.f != nil {
+ if err := mf.f.Close(); err != nil {
+ return err
+ }
+ }
+ f, err := mf.zf.Open()
+ if err != nil {
+ return err
+ }
+ mf.f = f
+ mf.pos = 0
+ return nil
+}
+
+func (mf *MothballFile) ModTime() time.Time {
+ return mf.zf.Modified
+}
+
+func (mf *MothballFile) Read(p []byte) (int, error) {
+ n, err := mf.f.Read(p)
+ mf.pos += int64(n)
+ return n, err
+}
+
+func (mf *MothballFile) Seek(offset int64, whence int) (int64, error) {
+ var pos int64
+ switch whence {
+ case io.SeekStart:
+ pos = offset
+ case io.SeekCurrent:
+ pos = mf.pos + int64(offset)
+ case io.SeekEnd:
+ pos = int64(mf.zf.UncompressedSize64) - int64(offset)
+ }
+
+ if pos < 0 {
+ return mf.pos, fmt.Errorf("Tried to seek %d before start of file", pos)
+ }
+ if pos >= int64(mf.zf.UncompressedSize64) {
+ // We don't need to decompress anything, we're at the end of the file
+ mf.f.Close()
+ mf.f = ioutil.NopCloser(strings.NewReader(""))
+ mf.pos = int64(mf.zf.UncompressedSize64)
+ return mf.pos, nil
+ }
+ if pos < mf.pos {
+ if err := mf.reopen(); err != nil {
+ return mf.pos, err
+ }
+ }
+
+ buf := make([]byte, 32 * 1024)
+ for pos > mf.pos {
+ l := pos - mf.pos
+ if l > int64(cap(buf)) {
+ l = int64(cap(buf)) - 1
+ }
+ p := buf[0:int(l)]
+ n, err := mf.Read(p)
+ if err != nil {
+ return mf.pos, err
+ } else if n <= 0 {
+ return mf.pos, fmt.Errorf("Short read (%d bytes)", n)
+ }
+ }
+
+ return mf.pos, nil
+}
+
+func (mf *MothballFile) Close() error {
+ return mf.f.Close()
+}
+
func OpenMothball(filename string) (*Mothball, error) {
var m Mothball
@@ -57,14 +153,30 @@ func (m *Mothball) Refresh() error {
return nil
}
-func (m *Mothball) Open(filename string) (io.ReadCloser, error) {
+func (m *Mothball) get(filename string) (*zip.File, error) {
for _, f := range m.zf.File {
if filename == f.Name {
- ret, err := f.Open()
- return ret, err
+ return f, nil
}
}
- return nil, fmt.Errorf("File not found: %s in %s", filename, m.filename)
+ return nil, fmt.Errorf("File not found: %s %s", m.filename, filename)
+}
+
+func (m *Mothball) Header(filename string) (*zip.FileHeader, error) {
+ f, err := m.get(filename)
+ if err != nil {
+ return nil, err
+ }
+ return &f.FileHeader, nil
+}
+
+func (m *Mothball) Open(filename string) (*MothballFile, error) {
+ f, err := m.get(filename)
+ if err != nil {
+ return nil, err
+ }
+ mf, err := NewMothballFile(f)
+ return mf, err
}
func (m *Mothball) ReadFile(filename string) ([]byte, error) {
diff --git a/src/mothd.go b/src/mothd.go
index 9a7bcc4..c61af99 100644
--- a/src/mothd.go
+++ b/src/mothd.go
@@ -3,6 +3,7 @@ package main
import (
"flag"
"log"
+ "mime"
"net/http"
"time"
)
@@ -61,6 +62,11 @@ func main() {
}
ctx.BindHandlers(http.DefaultServeMux)
+ // 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)
diff --git a/src/static.go b/src/static.go
index 64d4031..c13a0c9 100644
--- a/src/static.go
+++ b/src/static.go
@@ -67,7 +67,7 @@ func ShowHtml(w http.ResponseWriter, status Status, title string, body string) {
fmt.Fprintf(w, "", body)
fmt.Fprintf(w, "")
@@ -397,7 +397,7 @@ func ServeStatic(w http.ResponseWriter, req *http.Request, resourcesDir string)
staticIndex(w)
case "/scoreboard.html":
staticScoreboard(w)
- case "/puzzles.html":
+ case "/puzzle-list.html":
staticPuzzles(w)
default:
http.NotFound(w, req)