package main import ( "net/http" "html/template" "log" "fmt" "embed" "io" "os" "path" "path/filepath" "flag" "bufio" "errors" "io/fs" "strings" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) //go:embed templates var tmplFS embed.FS //go:embed favicon.ico var favicon []byte const PROXYPREFIXKEY string = "BOX_PROXY_PREFIX" type BoxHandler struct { filesPath string token []byte deleteEnabled bool index bool } func serve(w http.ResponseWriter, token []byte, views ...string) { t, err := template.New("index.html").ParseFS(tmplFS, views...) if err != nil { log.Fatal(err) } if err := t.Execute(w, struct { Token bool ProxyPrefix template.URL }{ Token: token != nil, ProxyPrefix: template.URL(os.Getenv(PROXYPREFIXKEY)), }); err != nil { log.Fatal(err) } } func (handler BoxHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { reqPath := strings.TrimPrefix(r.URL.Path, os.Getenv(PROXYPREFIXKEY)) switch r.Method { case http.MethodGet: if reqPath == "/favicon.ico" { w.Write(favicon) return } if reqPath == "/" { serve(w, handler.token, "templates/index.html") } else { resourceId := path.Base(reqPath) resourcePath := filepath.Join(handler.filesPath, resourceId) fi, err := os.Stat(resourcePath) if errors.Is(err, fs.ErrNotExist) { log.Println(err) w.WriteHeader(http.StatusNotFound) return } else if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, err) return } if fi.IsDir() && !handler.index { log.Println("Index requested but not enabled") w.WriteHeader(http.StatusNotFound) return } http.ServeFile(w, r, resourcePath) } return case http.MethodDelete: if !handler.deleteEnabled { w.WriteHeader(http.StatusForbidden) return } token := r.Header.Get("X-Token") if bcrypt.CompareHashAndPassword(handler.token, []byte(token)) != nil { log.Println("unauthorized") w.WriteHeader(http.StatusUnauthorized) return } resourceId := path.Base(reqPath) filename := filepath.Join(handler.filesPath, resourceId) err := os.Remove(filename) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, err.Error()) return } log.Printf("Deleted %s", filename) w.WriteHeader(http.StatusNoContent) case http.MethodPost: if reqPath != "/upload" { w.WriteHeader(http.StatusBadRequest) return } token := r.Header.Get("X-Token") if bcrypt.CompareHashAndPassword(handler.token, []byte(token)) != nil { log.Println("unauthorized") w.WriteHeader(http.StatusUnauthorized) return } filename := r.Header.Get("X-ResourceMeta-Filename") if filename == "" { ext := r.Header.Get("X-ResourceMeta-Extension") u, err := uuid.NewRandom() if err != nil { log.Println(err) fmt.Fprint(w, err.Error()) w.WriteHeader(http.StatusInternalServerError) return } filename = filepath.Join(handler.filesPath, u.String()) + ext } log.Printf("Boxing %s...\n", filename) f, err := os.Create(filename) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, err.Error()) return } defer f.Close() defer r.Body.Close() io.Copy(f, r.Body) w.Header().Add("X-Resource-ID", filepath.Base(filename)) log.Printf("Boxed %s\n", filename) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func main() { host := flag.String("n", "", "The hostname to listen on") port := flag.Int("p", 8080, "The port to listen on") isToken := flag.Bool("t", false, "Use a token to protect uploads/deletes") filesPath := flag.String("d", "", "The path to the files") deleteEnabled := flag.Bool("D", false, "Enable deleting resources") index := flag.Bool("i", false, "Enable displaying the resource folder index") flag.Parse() var token []byte = nil if *isToken { tok := os.Getenv("BOX_TOKEN") if tok == "" { fmt.Print("Token: ") sc := bufio.NewScanner(os.Stdin) sc.Scan() tok = sc.Text() } var err error = nil token, err = bcrypt.GenerateFromPassword([]byte(tok), bcrypt.DefaultCost) if err != nil { panic(err) } } boxHandler := BoxHandler { *filesPath, token, *deleteEnabled, *index, } if boxHandler.filesPath != "" { err := os.MkdirAll(boxHandler.filesPath, 0750) if err != nil { log.Fatal(err) } } log.Printf("Listening on %s:%d", *host, *port) log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *host, *port), boxHandler)) }