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}