moth/src/handlers.go

312 lines
7.7 KiB
Go
Raw Normal View History

2018-05-02 15:45:45 -06:00
package main
import (
2018-09-17 17:00:08 -06:00
"bufio"
2018-09-24 14:00:18 -06:00
"encoding/json"
2018-05-02 15:45:45 -06:00
"fmt"
2018-09-17 18:02:44 -06:00
"io"
2018-09-17 17:00:08 -06:00
"log"
2018-05-02 15:45:45 -06:00
"net/http"
2018-05-04 17:20:51 -06:00
"os"
"strconv"
2018-09-17 17:00:08 -06:00
"strings"
2018-05-02 15:45:45 -06:00
)
2018-09-24 14:00:18 -06:00
type JSend struct {
Status string `json:"status"`
Data JSendData `json:"data"`
2018-09-24 14:00:18 -06:00
}
type JSendData struct {
Short string `json:"short"`
2018-09-24 14:00:18 -06:00
Description string `json:"description"`
}
// ShowJSend renders a JSend response to w
func ShowJSend(w http.ResponseWriter, status Status, short string, description string) {
resp := JSend{
Status: "success",
Data: JSendData{
Short: short,
2018-09-24 14:00:18 -06:00
Description: description,
},
}
switch status {
case Success:
resp.Status = "success"
case Fail:
resp.Status = "fail"
default:
resp.Status = "error"
}
respBytes, err := json.Marshal(resp)
if err != nil {
2018-09-24 14:00:18 -06:00
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
2018-09-24 14:00:18 -06:00
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) // RFC2616 makes it pretty clear that 4xx codes are for the user-agent
w.Write(respBytes)
}
type Status int
const (
Success = iota
Fail
Error
)
2018-09-24 14:00:18 -06:00
// ShowHtml delevers an HTML response to w
func ShowHtml(w http.ResponseWriter, status Status, title string, body string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
statusStr := ""
switch status {
case Success:
statusStr = "Success"
case Fail:
statusStr = "Fail"
default:
statusStr = "Error"
}
fmt.Fprintf(w, "<!DOCTYPE html>")
fmt.Fprintf(w, "<!-- If you put `application/json` in the `Accept` header of this request, you would have gotten a JSON object instead of HTML. -->\n")
2018-09-24 14:00:18 -06:00
fmt.Fprintf(w, "<html><head>")
fmt.Fprintf(w, "<title>%s</title>", title)
fmt.Fprintf(w, "<link rel=\"stylesheet\" href=\"basic.css\">")
fmt.Fprintf(w, "<meta name=\"viewport\" content=\"width=device-width\">")
fmt.Fprintf(w, "<link rel=\"icon\" href=\"res/icon.svg\" type=\"image/svg+xml\">")
fmt.Fprintf(w, "<link rel=\"icon\" href=\"res/icon.png\" type=\"image/png\">")
fmt.Fprintf(w, "</head><body><h1 class=\"%s\">%s</h1>", statusStr, title)
fmt.Fprintf(w, "<section>%s</section>", body)
fmt.Fprintf(w, "<nav>")
fmt.Fprintf(w, "<ul>")
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, "</ul>")
fmt.Fprintf(w, "</nav>")
fmt.Fprintf(w, "</body></html>")
}
func respond(w http.ResponseWriter, req *http.Request, status Status, short string, format string, a ...interface{}) {
long := fmt.Sprintf(format, a...)
2018-09-17 17:00:08 -06:00
// This is a kludge. Do proper parsing when this causes problems.
accept := req.Header.Get("Accept")
if strings.Contains(accept, "application/json") {
ShowJSend(w, status, short, long)
2018-09-17 17:00:08 -06:00
} else {
ShowHtml(w, status, short, long)
2018-09-14 18:24:48 -06:00
}
}
2018-09-17 21:32:24 -06:00
// hasLine returns true if line appears in r.
// The entire line must match.
func hasLine(r io.Reader, line string) bool {
2018-09-17 18:02:44 -06:00
scanner := bufio.NewScanner(r)
for scanner.Scan() {
2018-09-17 21:32:24 -06:00
if scanner.Text() == line {
2018-09-17 18:02:44 -06:00
return true
}
}
return false
}
2018-09-19 21:44:34 -06:00
func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
2018-09-17 17:00:08 -06:00
teamname := req.FormValue("name")
teamid := req.FormValue("id")
// Keep foolish operators from shooting themselves in the foot
// You would have to add a pathname to your list of Team IDs to open this vulnerability,
// but I have learned not to overestimate people.
if strings.Contains(teamid, "../") {
teamid = "rodney"
2018-05-04 17:20:51 -06:00
}
2018-09-17 17:00:08 -06:00
2018-05-04 17:20:51 -06:00
if (teamid == "") || (teamname == "") {
2018-09-17 17:00:08 -06:00
respond(
w, req, Fail,
"Invalid Entry",
"Either `id` or `name` was missing from this request.",
)
2018-05-04 17:20:51 -06:00
return
}
2018-09-17 17:00:08 -06:00
2018-09-17 21:32:24 -06:00
teamids, err := os.Open(ctx.StatePath("teamids.txt"))
if err != nil {
respond(
w, req, Fail,
"Cannot read valid team IDs",
"An error was encountered trying to read valid teams IDs: %v", err,
)
return
}
defer teamids.Close()
if !hasLine(teamids, teamid) {
2018-09-17 17:00:08 -06:00
respond(
w, req, Fail,
"Invalid Team ID",
"I don't have a record of that team ID. Maybe you used capital letters accidentally?",
)
2018-05-04 17:20:51 -06:00
return
}
2018-09-17 17:00:08 -06:00
f, err := os.OpenFile(ctx.StatePath("teams", teamid), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
2018-05-04 17:20:51 -06:00
if err != nil {
2018-09-17 17:00:08 -06:00
log.Print(err)
respond(
w, req, Fail,
2018-05-04 17:20:51 -06:00
"Registration failed",
"Unable to register. Perhaps a teammate has already registered?",
)
return
}
defer f.Close()
fmt.Fprintln(f, teamname)
2018-09-17 17:00:08 -06:00
respond(
w, req, Success,
"Team registered",
"Okay, your team has been named and you may begin using your team ID!",
)
2018-05-04 17:20:51 -06:00
}
2018-09-19 21:44:34 -06:00
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")
2018-05-04 17:20:51 -06:00
points, err := strconv.Atoi(pointstr)
if err != nil {
2018-09-17 17:00:08 -06:00
respond(
w, req, Fail,
"Cannot parse point value",
"This doesn't look like an integer: %s", pointstr,
2018-09-17 17:00:08 -06:00
)
2018-09-14 18:24:48 -06:00
return
2018-05-04 17:20:51 -06:00
}
haystack, err := ctx.OpenCategoryFile(category, "answers.txt")
2018-09-14 18:24:48 -06:00
if err != nil {
2018-09-17 17:00:08 -06:00
respond(
w, req, Fail,
"Cannot list answers",
"Unable to read the list of answers for this category.",
2018-09-17 17:00:08 -06:00
)
2018-09-14 18:24:48 -06:00
return
}
defer haystack.Close()
2018-09-17 17:00:08 -06:00
2018-09-14 18:24:48 -06:00
// Look for the answer
needle := fmt.Sprintf("%d %s", points, answer)
2018-09-17 21:32:24 -06:00
if !hasLine(haystack, needle) {
2018-09-17 17:00:08 -06:00
respond(
w, req, Fail,
"Wrong answer",
"That is not the correct answer for %s %d.", category, points,
2018-09-17 17:00:08 -06:00
)
2018-09-14 18:24:48 -06:00
return
2018-05-04 17:20:51 -06:00
}
if err := ctx.AwardPoints(teamid, category, points); err != nil {
2018-09-17 17:00:08 -06:00
respond(
w, req, Error,
"Cannot award points",
"The answer is correct, but there was an error awarding points: %v", err.Error(),
2018-09-17 17:00:08 -06:00
)
2018-05-04 17:20:51 -06:00
return
}
2018-09-17 17:00:08 -06:00
respond(
w, req, Success,
"Points awarded",
fmt.Sprintf("%d points for %s!", points, teamid),
)
}
2018-09-19 21:44:34 -06:00
func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
2018-09-17 17:00:08 -06:00
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
2018-09-19 21:44:34 -06:00
w.Write(ctx.jPuzzleList)
2018-05-08 12:45:50 -06:00
}
2018-09-19 21:44:34 -06:00
func (ctx *Instance) pointsHandler(w http.ResponseWriter, req *http.Request) {
2018-09-17 17:00:08 -06:00
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
2018-09-19 21:44:34 -06:00
w.Write(ctx.jPointsLog)
2018-09-17 17:00:08 -06:00
}
2018-09-19 21:44:34 -06:00
func (ctx *Instance) contentHandler(w http.ResponseWriter, req *http.Request) {
2018-09-19 17:56:26 -06:00
// Prevent directory traversal
if strings.Contains(req.URL.Path, "/.") {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
2018-09-19 21:44:34 -06:00
2018-09-19 17:56:26 -06:00
// 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
}
2018-09-19 21:44:34 -06:00
2018-09-19 17:56:26 -06:00
fileName := parts[len(parts)-1]
puzzleId := parts[len(parts)-2]
categoryName := parts[len(parts)-3]
2018-09-19 21:44:34 -06:00
2018-09-19 17:56:26 -06:00
mb, ok := ctx.Categories[categoryName]
if !ok {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
2018-09-19 21:44:34 -06:00
2018-09-19 17:56:26 -06:00
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()
2018-09-19 21:44:34 -06:00
2018-09-19 17:56:26 -06:00
http.ServeContent(w, req, fileName, mf.ModTime(), mf)
}
2018-09-19 21:44:34 -06:00
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.ResourcePath(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)
2018-05-06 21:37:52 -06:00
}
2018-09-14 18:24:48 -06:00
2018-09-19 21:44:34 -06:00
func (ctx *Instance) BindHandlers(mux *http.ServeMux) {
2018-09-17 17:00:08 -06:00
mux.HandleFunc(ctx.Base+"/", ctx.staticHandler)
mux.HandleFunc(ctx.Base+"/register", ctx.registerHandler)
mux.HandleFunc(ctx.Base+"/answer", ctx.answerHandler)
2018-09-19 17:56:26 -06:00
mux.HandleFunc(ctx.Base+"/content/", ctx.contentHandler)
2018-09-17 17:00:08 -06:00
mux.HandleFunc(ctx.Base+"/puzzles.json", ctx.puzzlesHandler)
mux.HandleFunc(ctx.Base+"/points.json", ctx.pointsHandler)
2018-09-14 18:24:48 -06:00
}