From e68a041f3367196db1678acb63648f30216938aa Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 21 Feb 2019 23:00:06 +0000 Subject: [PATCH] Add basic authentication and remove legacy html responses --- Dockerfile.moth | 10 ++-- src/handlers.go | 121 +++++++++++++++++++++++------------------------- src/instance.go | 10 +++- src/mothd.go | 19 ++++---- 4 files changed, 81 insertions(+), 79 deletions(-) diff --git a/Dockerfile.moth b/Dockerfile.moth index 23a16da..b7bfd37 100644 --- a/Dockerfile.moth +++ b/Dockerfile.moth @@ -1,7 +1,9 @@ -FROM alpine:3.8 AS builder -RUN apk --no-cache add go libc-dev -COPY src /src -RUN go build -o /mothd /src/*.go +FROM alpine:3.9 AS builder +RUN apk --no-cache add go libc-dev git +COPY src /root/go/src/github.com/dirtbags/moth/src +WORKDIR /root/go/src/github.com/dirtbags/moth/src +RUN go get . +RUN go build -o /mothd *.go FROM alpine COPY --from=builder /mothd /mothd diff --git a/src/handlers.go b/src/handlers.go index 3d3984b..b60b760 100644 --- a/src/handlers.go +++ b/src/handlers.go @@ -21,14 +21,20 @@ type JSendData struct { Description string `json:"description"` } -// ShowJSend renders a JSend response to w -func ShowJSend(w http.ResponseWriter, status Status, short string, description string) { +type Status int +const ( + Success = iota + Fail + Error +) + +func respond(w http.ResponseWriter, req *http.Request, status Status, short string, format string, a ...interface{}) { resp := JSend{ Status: "success", Data: JSendData{ Short: short, - Description: description, + Description: fmt.Sprintf(format, a...), }, } switch status { @@ -51,59 +57,6 @@ func ShowJSend(w http.ResponseWriter, status Status, short string, description s w.Write(respBytes) } -type Status int - -const ( - Success = iota - Fail - Error -) - -// 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, "") - fmt.Fprintf(w, "\n") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "%s", title) - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") - fmt.Fprintf(w, "

%s

", statusStr, title) - fmt.Fprintf(w, "
%s
", body) - fmt.Fprintf(w, "") - fmt.Fprintf(w, "") -} - -func respond(w http.ResponseWriter, req *http.Request, status Status, short string, format string, a ...interface{}) { - long := fmt.Sprintf(format, a...) - // 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) - } else { - ShowHtml(w, status, short, long) - } -} - // hasLine returns true if line appears in r. // The entire line must match. func hasLine(r io.Reader, line string) bool { @@ -301,11 +254,53 @@ func (ctx *Instance) staticHandler(w http.ResponseWriter, req *http.Request) { http.ServeContent(w, req, path, d.ModTime(), f) } -func (ctx *Instance) BindHandlers(mux *http.ServeMux) { - mux.HandleFunc(ctx.Base+"/", ctx.staticHandler) - mux.HandleFunc(ctx.Base+"/register", ctx.registerHandler) - 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+"/points.json", ctx.pointsHandler) +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), + } + w.Header().Set("WWW-Authenticate", "Basic") + _, password, _ := r.BasicAuth() + if password != ctx.Password { + http.Error(w, "Authentication Required", 401) + } else { + 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) +} + diff --git a/src/instance.go b/src/instance.go index 2794768..5b6cb61 100644 --- a/src/instance.go +++ b/src/instance.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "log" + "net/http" "os" "path" "strings" @@ -17,20 +18,24 @@ type Instance struct { MothballDir string StateDir string ResourcesDir string + Password string Categories map[string]*Mothball update chan bool jPuzzleList []byte jPointsLog []byte + mux *http.ServeMux } -func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, error) { +func NewInstance(base, mothballDir, stateDir, resourcesDir, password string) (*Instance, error) { ctx := &Instance{ Base: strings.TrimRight(base, "/"), MothballDir: mothballDir, StateDir: stateDir, ResourcesDir: resourcesDir, + Password: password, Categories: map[string]*Mothball{}, update: make(chan bool, 10), + mux: http.NewServeMux(), } // Roll over and die if directories aren't even set up @@ -40,7 +45,8 @@ func NewInstance(base, mothballDir, stateDir, resourcesDir string) (*Instance, e if _, err := os.Stat(stateDir); err != nil { return nil, err } - + + ctx.BindHandlers() ctx.MaybeInitialize() return ctx, nil diff --git a/src/mothd.go b/src/mothd.go index 5834f24..5f7fef5 100644 --- a/src/mothd.go +++ b/src/mothd.go @@ -1,24 +1,19 @@ package main import ( - "flag" + "github.com/namsral/flag" "log" "mime" "net/http" "time" ) -func logRequest(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("HTTP %s %s %s\n", r.RemoteAddr, r.Method, r.URL) - handler.ServeHTTP(w, r) - }) -} func setup() error { return nil } + func main() { base := flag.String( "base", @@ -40,6 +35,11 @@ func main() { "/theme", "Path to static theme resources (HTML, images, css, ...)", ) + password := flag.String( + "password", + "sesame", + "Pass Word (in the 1920s sense) to view the site. Not a secure passphrase.", + ) maintenanceInterval := flag.Duration( "maint", 20*time.Second, @@ -56,11 +56,10 @@ func main() { log.Fatal(err) } - ctx, err := NewInstance(*base, *mothballDir, *stateDir, *themeDir) + ctx, err := NewInstance(*base, *mothballDir, *stateDir, *themeDir, *password) if err != nil { log.Fatal(err) } - ctx.BindHandlers(http.DefaultServeMux) // Add some MIME extensions // Doing this avoids decompressing a mothball entry twice per request @@ -70,5 +69,5 @@ func main() { go ctx.Maintenance(*maintenanceInterval) log.Printf("Listening on %s", *listen) - log.Fatal(http.ListenAndServe(*listen, logRequest(http.DefaultServeMux))) + log.Fatal(http.ListenAndServe(*listen, ctx)) }