diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..ce6dab6f6dd317865a04204959363cf7c3eb8e65 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,49 @@ +package auth + +import ( + "context" + "net/http" +) + +type contextKey string + +type Provider interface { + // Returns true if pass is a valid password for the given user + Valid(user, pass string) bool +} + +// Require wraps hdlr so that it requires authentication to use. Requests handled by hdlr will have the user +// name attached to their context. Use the User function to retrieve it. +func Require(hdlr http.HandlerFunc, provider Provider) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok { + w.Header().Add("WWW-Authenticate", `Basic realm="In Vino Veritas"`) + w.WriteHeader(http.StatusUnauthorized) + + return + } + + if !provider.Valid(user, pass) { + w.Header().Add("WWW-Authenticate", `Basic realm="In Vino Veritas"`) + w.WriteHeader(http.StatusUnauthorized) + + return + } + + key := contextKey("user") + ctx := context.WithValue(r.Context(), key, user) + + hdlr(w, r.WithContext(ctx)) + }) +} + +// User returns the user from r's context. If r has no user in its context, the empty string will be returned. +func User(r *http.Request) string { + val := r.Context().Value(contextKey("user")) + if val == nil { + return "" + } + + return val.(string) +} diff --git a/handler-details.go b/handler-details.go index 634627a5b1d359e208feabe5456dc1659bdfae0c..932f44c7a44991396f9241ea4eafd986ca07ed2c 100644 --- a/handler-details.go +++ b/handler-details.go @@ -15,7 +15,7 @@ import ( var detailsTemplate = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/details.tpl")) func (h Handler) img(w http.ResponseWriter, r *http.Request) { - log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr) + logRequest(r) if r.Method != "GET" { httpError(w, "bad method", errors.New(r.Method), http.StatusMethodNotAllowed) diff --git a/handler-index.go b/handler-index.go index 2beab742b4fb89b93301027c1abbe1ca48463883..fa35bc55911e79408dc406bc11254bbc8adddb4f 100644 --- a/handler-index.go +++ b/handler-index.go @@ -17,7 +17,7 @@ func (h Handler) index(w http.ResponseWriter, r *http.Request) { return } - log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr) + logRequest(r) if r.Method == "GET" { wines, err := ListWines(r.Context(), h.db) diff --git a/main.go b/main.go index 0ab495736f39a9fca9f9b43580c59c969190970d..0cd87131dc45e52a5175fd1cfa409068b01b925a 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" // Imported for side effects: registers DB driver + + "git.c3pb.de/gbe/invinoveritas/auth" ) //go:embed templates/*.tpl @@ -29,6 +31,20 @@ func httpError(w http.ResponseWriter, msg string, err error, status int) { http.Error(w, msg, status) } +type authProvider struct{} + +func (a authProvider) Valid(user, pass string) bool { + if user == "wine" && pass == "potatoe" { + return true + } + + return false +} + +func logRequest(r *http.Request) { + log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr, "by", auth.User(r)) +} + func main() { db, err := sqlx.Open("sqlite", "vino.sqlite") if err != nil { @@ -41,15 +57,19 @@ func main() { log.Fatalln("can't initialize DB:", err) } + http.HandleFunc("/favicon.ico", http.NotFound) + http.Handle("/static/", http.FileServer(http.FS(staticFS))) handler := Handler{ db: db, } - http.HandleFunc("/details/img", handler.img) - http.HandleFunc("/details/", handler.details) - http.HandleFunc("/", handler.index) + ap := authProvider{} + + http.HandleFunc("/details/img", auth.Require(http.HandlerFunc(handler.img), ap)) + http.HandleFunc("/details/", auth.Require(http.HandlerFunc(handler.details), ap)) + http.HandleFunc("/", auth.Require(http.HandlerFunc(handler.index), ap)) const listenAddr = ":7878"