package web import ( "encoding/json" "errors" "io" "net/http" "strconv" "git.kapelle.org/niklas/s3share/internal/client" "git.kapelle.org/niklas/s3share/internal/types" "github.com/gorilla/mux" "github.com/sirupsen/logrus" ) type createShare struct { Key string `json:"key"` } func StartWebserver(addr string, client *client.Client, username, password string) error { if username == "" || password == "" { return errors.New("API username and password must be set") } r := CreateRouter(client, username, password) return http.ListenAndServe(addr, r) } func CreateRouter(client *client.Client, username, password string) *mux.Router { r := mux.NewRouter() r.HandleFunc("/{slug:[a-zA-Z0-9]{6}}", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./public/index.html") }) r.HandleFunc("/s/{path:.{6,}}", func(w http.ResponseWriter, r *http.Request) { share := getShareHead(client, w, r) if share == nil { return } obj, err := client.GetObjectFromShare(r.Context(), share) if err != nil { logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } _, err = io.Copy(w, obj) if err != nil { logrus.Error(err.Error()) return } }) r.HandleFunc("/s/{path:.{6,}}", func(w http.ResponseWriter, r *http.Request) { getShareHead(client, w, r) }).Methods("HEAD") r.HandleFunc("/api/share", func(w http.ResponseWriter, r *http.Request) { if !checkAuth(w, r, username, password) { return } shares, err := client.GetAllShares(r.Context()) if err != nil { logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(shares) }).Methods("GET") r.HandleFunc("/api/share/{slug:[a-zA-Z0-9]{6}}", func(w http.ResponseWriter, r *http.Request) { if !checkAuth(w, r, username, password) { return } vars := mux.Vars(r) slug := vars["slug"] share, err := client.GetShare(r.Context(), slug) if err != nil { logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } if share == nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(share) }).Methods("GET") r.HandleFunc("/api/share", func(w http.ResponseWriter, r *http.Request) { if !checkAuth(w, r, username, password) { return } var shareParams createShare err := json.NewDecoder(r.Body).Decode(&shareParams) if err != nil { logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return } share, err := client.CreateShare(r.Context(), shareParams.Key) if err != nil { if err == types.ErrKeyNotFound { http.Error(w, "The specified key does not exist", http.StatusBadRequest) return } logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(share) }).Methods("POST", "PUT") r.HandleFunc("/api/share/{slug:[a-zA-Z0-9]{6}}", func(w http.ResponseWriter, r *http.Request) { if !checkAuth(w, r, username, password) { return } vars := mux.Vars(r) err := client.DeleteShare(r.Context(), vars["slug"]) if err != nil { if err == types.ErrShareNotFound { http.NotFound(w, r) return } logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) }).Methods("DELETE") r.PathPrefix("/").Handler(http.FileServer(http.Dir("./public/"))) return r } func getShareHead(client *client.Client, w http.ResponseWriter, r *http.Request) *types.Share { vars := mux.Vars(r) slug := vars["path"][0:6] share, err := client.GetShare(r.Context(), slug) if err != nil { logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return nil } if share == nil { http.NotFound(w, r) return nil } metadata, err := client.GetObjectMetadata(r.Context(), share.Key) if err != nil { logrus.Error(err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return nil } // Prevent using the default Content-Type. Can cause some confusion when the browser tries to open images in a new tab. if metadata.ContentType != "application/octet-stream" { w.Header().Set("Content-Type", metadata.ContentType) } w.Header().Set("Content-Length", strconv.FormatInt(metadata.Size, 10)) w.Header().Set("Content-Disposition", "inline; filename=\""+metadata.Filename+"\"") if metadata.ETag != "" { w.Header().Set("ETag", metadata.ETag) } return share } func checkAuth(w http.ResponseWriter, r *http.Request, username, password string) bool { authUsername, authPassword, ok := r.BasicAuth() if !ok { w.WriteHeader(http.StatusUnauthorized) return false } if username != authUsername || password != authPassword { w.WriteHeader(http.StatusUnauthorized) return false } return true }