moth/cmd/mothdv3/handlers.go

263 lines
5.8 KiB
Go

package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"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) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(ctx.jPointsLog)
}
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)
}
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),
}
ctx.mux.ServeHTTP(w, r)
log.Printf(
"%s %s %s %d\n",
r.RemoteAddr,
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)
}