package mailmux import ( "encoding/json" "errors" "fmt" "net/http" "path" "strconv" "strings" "time" ) func jerror(w http.ResponseWriter, msg string, status int) { w.WriteHeader(status) m := map[string]string{"error": msg} json.NewEncoder(w).Encode(m) } func NewWebServer(aliases AliasStore, users UserStore) http.Handler { mux := http.NewServeMux() aliaseshandler := &aliasesHandler{aliases} aliashandler := &aliasHandler{aliases} authhandler := &authHandler{users} // 8KB is definitely large enough for any reasonable registration // request and response. limitedAuthHandler := http.MaxBytesHandler(authhandler, 8*1024) mux.Handle("/register", limitedAuthHandler) mux.Handle("/aliases", authhandler.basicAuth(aliaseshandler)) mux.Handle("/aliases/", authhandler.basicAuth(aliashandler)) return mux } type aliasesHandler struct { AliasStore } func (h *aliasesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { username, _, ok := req.BasicAuth() if !ok || len(username) == 0 { jerror(w, "empty username", http.StatusForbidden) return } switch req.Method { case http.MethodGet: aliases, err := h.Aliases(username) if err != nil { jerror(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(aliases) case http.MethodPost: alias, err := h.Create(username) if err != nil { jerror(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(alias) default: jerror(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) } } type aliasHandler struct { AliasStore } func (h *aliasHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { username, _, ok := req.BasicAuth() if !ok || len(username) == 0 { jerror(w, "empty username", http.StatusForbidden) return } recipient := path.Base(req.URL.Path) aliases, err := h.Aliases(username) if err != nil { jerror(w, fmt.Sprintf("aliases for %s: %v", recipient, err), http.StatusInternalServerError) return } var alias Alias var found bool for _, a := range aliases { if a.Recipient == recipient { alias = a found = true } } if !found { jerror(w, "no such alias", http.StatusNotFound) return } switch req.Method { case http.MethodDelete: if err := h.Delete(recipient); err != nil { jerror(w, fmt.Sprintf("delete %s: %v", recipient, err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) case http.MethodGet: json.NewEncoder(w).Encode(alias) case http.MethodPost: if err := req.ParseForm(); err != nil { jerror(w, fmt.Sprintf("parse form: %v", err), http.StatusBadRequest) return } for param := range req.PostForm { switch param { case "expiry": i, err := strconv.Atoi(req.PostForm.Get(param)) if err != nil { jerror(w, fmt.Sprintf("parse expiry: %v", err), http.StatusBadRequest) return } alias.Expiry = time.Unix(int64(i), 0) case "note": alias.Note = req.PostForm.Get(param) default: jerror(w, fmt.Sprintf("invalid alias parameter %s", param), http.StatusBadRequest) return } } if err := h.Put(alias); err != nil { jerror(w, fmt.Sprintf("update alias %s: %v", alias.Recipient, err), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(alias) default: jerror(w, "not implemented yet", http.StatusMethodNotAllowed) } } type authHandler struct { UserStore } func (h *authHandler) basicAuth(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, req *http.Request) { username, password, ok := req.BasicAuth() if !ok || len(username) == 0 || len(password) == 0 { jerror(w, "unauthorised", http.StatusUnauthorized) return } err := h.Authenticate(username, Password(password)) if err != nil { jerror(w, "unauthorised", http.StatusUnauthorized) return } next.ServeHTTP(w, req) } return http.HandlerFunc(fn) } func (h *authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { code := http.StatusMethodNotAllowed http.Error(w, http.StatusText(code), code) return } if err := req.ParseForm(); err != nil { jerror(w, err.Error(), http.StatusBadRequest) return } username := req.PostForm.Get("username") if username == "" { jerror(w, "empty username", http.StatusBadRequest) return } password := req.PostForm.Get("password") if password == "" { jerror(w, "empty password", http.StatusBadRequest) return } _, err := h.Lookup(username) if err == nil { jerror(w, "user already exists", http.StatusBadRequest) return } else if !errors.Is(err, ErrUnknownUser) { jerror(w, fmt.Sprintf("lookup %s: %v", username, err), http.StatusInternalServerError) return } if err := h.Change(username, Password(password)); err != nil { code := http.StatusInternalServerError if strings.Contains(err.Error(), "invalid username") { code = http.StatusBadRequest } jerror(w, fmt.Sprintf("change %s: %v", username, err), code) } }