I think I'm done.

This commit is contained in:
Neale Pickett 2018-09-19 23:56:26 +00:00
parent 5075bb601d
commit 21902ce8e4
8 changed files with 175 additions and 11 deletions

View File

@ -38,12 +38,12 @@ function render(obj) {
l.appendChild(i); l.appendChild(i);
if (points === 0) { if (points === 0) {
i.textContent = ""; i.textContent = "";
} else { } else {
var a = document.createElement('a'); var a = document.createElement('a');
i.appendChild(a); i.appendChild(a);
a.textContent = points; 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);
</section> </section>
<nav> <nav>
<ul> <ul>
<li><a href="puzzles.html">Puzzles</a></li> <li><a href="puzzle-list.html">Puzzles</a></li>
<li><a href="scoreboard.html">Scoreboard</a></li> <li><a href="scoreboard.html">Scoreboard</a></li>
</ul> </ul>
</nav> </nav>

View File

@ -140,7 +140,7 @@ document.addEventListener("DOMContentLoaded", init);
</section> </section>
<nav> <nav>
<ul> <ul>
<li><a href="puzzles.html">Puzzles</a></li> <li><a href="puzzle-list.html">Puzzles</a></li>
<li><a href="scoreboard.html">Scoreboard</a></li> <li><a href="scoreboard.html">Scoreboard</a></li>
</ul> </ul>
</nav> </nav>

View File

@ -318,6 +318,42 @@ func (ctx Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
w.Write(jret) 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) { func (ctx Instance) staticHandler(w http.ResponseWriter, req *http.Request) {
ServeStatic(w, req, ctx.ResourcesDir) 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+"/register", ctx.registerHandler)
mux.HandleFunc(ctx.Base+"/token", ctx.tokenHandler) mux.HandleFunc(ctx.Base+"/token", ctx.tokenHandler)
mux.HandleFunc(ctx.Base+"/answer", ctx.answerHandler) 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+"/puzzles.json", ctx.puzzlesHandler)
mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler) mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
} }

View File

@ -18,6 +18,7 @@ type Instance struct {
StateDir string StateDir string
ResourcesDir string ResourcesDir string
Categories map[string]*Mothball Categories map[string]*Mothball
update chan bool
} }
func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) { func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) {
@ -27,6 +28,7 @@ func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, e
StateDir: stateDir, StateDir: stateDir,
ResourcesDir: resourcesDir, ResourcesDir: resourcesDir,
Categories: map[string]*Mothball{}, Categories: map[string]*Mothball{},
update: make(chan bool, 10),
} }
// Roll over and die if directories aren't even set up // 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 return err
} }
ctx.update <- true
log.Printf("Award %s %s %d", teamid, category, points) log.Printf("Award %s %s %d", teamid, category, points)
return nil return nil
} }

View File

@ -121,7 +121,13 @@ func (ctx *Instance) CollectPoints() {
// maintenance is the goroutine that runs a periodic maintenance task // maintenance is the goroutine that runs a periodic maintenance task
func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) { func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
for ; ; time.Sleep(maintenanceInterval) { for {
ctx.Tidy() ctx.Tidy()
select {
case <-ctx.update:
log.Print("Forced update")
case <-time.After(maintenanceInterval):
log.Print("Housekeeping...")
}
} }
} }

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"time" "time"
) )
@ -15,6 +16,101 @@ type Mothball struct {
mtime time.Time 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) { func OpenMothball(filename string) (*Mothball, error) {
var m Mothball var m Mothball
@ -57,14 +153,30 @@ func (m *Mothball) Refresh() error {
return nil 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 { for _, f := range m.zf.File {
if filename == f.Name { if filename == f.Name {
ret, err := f.Open() return f, nil
return ret, err
} }
} }
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) { func (m *Mothball) ReadFile(filename string) ([]byte, error) {

View File

@ -3,6 +3,7 @@ package main
import ( import (
"flag" "flag"
"log" "log"
"mime"
"net/http" "net/http"
"time" "time"
) )
@ -61,6 +62,11 @@ func main() {
} }
ctx.BindHandlers(http.DefaultServeMux) 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) go ctx.Maintenance(*maintenanceInterval)
log.Printf("Listening on %s", *listen) log.Printf("Listening on %s", *listen)

View File

@ -67,7 +67,7 @@ func ShowHtml(w http.ResponseWriter, status Status, title string, body string) {
fmt.Fprintf(w, "<section>%s</section>", body) fmt.Fprintf(w, "<section>%s</section>", body)
fmt.Fprintf(w, "<nav>") fmt.Fprintf(w, "<nav>")
fmt.Fprintf(w, "<ul>") fmt.Fprintf(w, "<ul>")
fmt.Fprintf(w, "<li><a href=\"puzzles.html\">Puzzles</a></li>") fmt.Fprintf(w, "<li><a href=\"puzzle-list.html\">Puzzles</a></li>")
fmt.Fprintf(w, "<li><a href=\"scoreboard.html\">Scoreboard</a></li>") fmt.Fprintf(w, "<li><a href=\"scoreboard.html\">Scoreboard</a></li>")
fmt.Fprintf(w, "</ul>") fmt.Fprintf(w, "</ul>")
fmt.Fprintf(w, "</nav>") fmt.Fprintf(w, "</nav>")
@ -397,7 +397,7 @@ func ServeStatic(w http.ResponseWriter, req *http.Request, resourcesDir string)
staticIndex(w) staticIndex(w)
case "/scoreboard.html": case "/scoreboard.html":
staticScoreboard(w) staticScoreboard(w)
case "/puzzles.html": case "/puzzle-list.html":
staticPuzzles(w) staticPuzzles(w)
default: default:
http.NotFound(w, req) http.NotFound(w, req)