moth

Monarch Of The Hill game server
git clone https://git.woozle.org/neale/moth.git

moth / cmd / mothd
Neale Pickett  ·  2023-04-11

httpd.go

  1package main
  2
  3import (
  4	"bytes"
  5	"log"
  6	"net/http"
  7	"strconv"
  8	"strings"
  9	"time"
 10
 11	"github.com/dirtbags/moth/v4/pkg/jsend"
 12)
 13
 14// HTTPServer is a MOTH HTTP server
 15type HTTPServer struct {
 16	*http.ServeMux
 17	server *MothServer
 18	base   string
 19}
 20
 21// NewHTTPServer creates a MOTH HTTP server, with handler functions registered
 22func NewHTTPServer(base string, server *MothServer) *HTTPServer {
 23	base = strings.TrimRight(base, "/")
 24	h := &HTTPServer{
 25		ServeMux: http.NewServeMux(),
 26		server:   server,
 27		base:     base,
 28	}
 29	h.HandleMothFunc("/", h.ThemeHandler)
 30	h.HandleMothFunc("/state", h.StateHandler)
 31	h.HandleMothFunc("/register", h.RegisterHandler)
 32	h.HandleMothFunc("/answer", h.AnswerHandler)
 33	h.HandleMothFunc("/content/", h.ContentHandler)
 34
 35	if server.Config.Devel {
 36		h.HandleMothFunc("/mothballer/", h.MothballerHandler)
 37	}
 38	return h
 39}
 40
 41// HandleMothFunc binds a new handler function which creates a new MothServer with every request
 42func (h *HTTPServer) HandleMothFunc(
 43	pattern string,
 44	mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
 45) {
 46	handler := func(w http.ResponseWriter, req *http.Request) {
 47		teamID := req.FormValue("id")
 48		mh := h.server.NewHandler(teamID)
 49		mothHandler(mh, w, req)
 50	}
 51	h.HandleFunc(h.base+pattern, handler)
 52}
 53
 54// ServeHTTP provides the http.Handler interface
 55func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 56	w := StatusResponseWriter{
 57		statusCode:     new(int),
 58		ResponseWriter: wOrig,
 59	}
 60	h.ServeMux.ServeHTTP(w, r)
 61	log.Printf(
 62		"%s %s %s %d\n",
 63		r.RemoteAddr,
 64		r.Method,
 65		r.URL,
 66		*w.statusCode,
 67	)
 68}
 69
 70// StatusResponseWriter provides a ResponseWriter that remembers what the status code was
 71type StatusResponseWriter struct {
 72	statusCode *int
 73	http.ResponseWriter
 74}
 75
 76// WriteHeader sends an HTTP response header with the provided status code
 77func (w StatusResponseWriter) WriteHeader(statusCode int) {
 78	*w.statusCode = statusCode
 79	w.ResponseWriter.WriteHeader(statusCode)
 80}
 81
 82// Run binds to the provided bindStr, and serves incoming requests until failure
 83func (h *HTTPServer) Run(bindStr string) {
 84	log.Printf("Listening on %s", bindStr)
 85	log.Fatal(http.ListenAndServe(bindStr, h))
 86}
 87
 88// ThemeHandler serves up static content from the theme directory
 89func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
 90	path := req.URL.Path
 91	if path == "/" {
 92		path = "/index.html"
 93	}
 94
 95	f, mtime, err := mh.ThemeOpen(path)
 96	if err != nil {
 97		http.NotFound(w, req)
 98		return
 99	}
100	defer f.Close()
101	http.ServeContent(w, req, path, mtime, f)
102}
103
104// StateHandler returns the full JSON-encoded state of the event
105func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
106	jsend.JSONWrite(w, mh.ExportState())
107}
108
109// RegisterHandler handles attempts to register a team
110func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
111	teamName := req.FormValue("name")
112	teamName = strings.TrimSpace(teamName)
113	if teamName == "" {
114		jsend.Sendf(w, jsend.Fail, "empty name", "Team name may not be empty")
115		return
116	}
117
118	if err := mh.Register(teamName); err == ErrAlreadyRegistered {
119		jsend.Sendf(w, jsend.Success, "already registered", "team ID has already been registered")
120	} else if err != nil {
121		jsend.Sendf(w, jsend.Fail, "not registered", err.Error())
122	} else {
123		jsend.Sendf(w, jsend.Success, "registered", "team ID registered")
124	}
125}
126
127// AnswerHandler checks answer correctness and awards points
128func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
129	cat := req.FormValue("cat")
130	pointstr := req.FormValue("points")
131	answer := req.FormValue("answer")
132
133	points, _ := strconv.Atoi(pointstr)
134
135	if err := mh.CheckAnswer(cat, points, answer); err != nil {
136		jsend.Sendf(w, jsend.Fail, "not accepted", err.Error())
137	} else {
138		jsend.Sendf(w, jsend.Success, "accepted", "%d points awarded in %s", points, cat)
139	}
140}
141
142// ContentHandler returns static content from a given puzzle
143func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
144	parts := strings.SplitN(req.URL.Path[len(h.base)+1:], "/", 4)
145	if len(parts) < 4 {
146		http.NotFound(w, req)
147		return
148	}
149
150	// parts[0] == "content"
151	cat := parts[1]
152	pointsStr := parts[2]
153	filename := parts[3]
154
155	if filename == "" {
156		filename = "puzzle.json"
157	}
158
159	points, _ := strconv.Atoi(pointsStr)
160
161	mf, mtime, err := mh.PuzzlesOpen(cat, points, filename)
162	if err != nil {
163		http.Error(w, err.Error(), http.StatusNotFound)
164		return
165	}
166	defer mf.Close()
167
168	http.ServeContent(w, req, filename, mtime, mf)
169}
170
171// MothballerHandler returns a mothball
172func (h *HTTPServer) MothballerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
173	parts := strings.SplitN(req.URL.Path[len(h.base)+1:], "/", 2)
174	if len(parts) < 2 {
175		http.NotFound(w, req)
176		return
177	}
178
179	// parts[0] == "mothballer"
180	filename := parts[1]
181	cat := strings.TrimSuffix(filename, ".mb")
182	mb := new(bytes.Buffer)
183	if err := mh.Mothball(cat, mb); err != nil {
184		http.Error(w, err.Error(), http.StatusInternalServerError)
185		return
186	}
187
188	mbReader := bytes.NewReader(mb.Bytes())
189	http.ServeContent(w, req, filename, time.Now(), mbReader)
190}