package routes import ( "crypto/sha256" "encoding/base64" "io" "io/ioutil" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/gorilla/mux" "github.com/h2non/filetype" "sour.is/x/toolbox/httpsrv" "sour.is/x/toolbox/log" "sour.is/x/paste/src/pkg/readutil" ) func init() { a := &image{} httpsrv.RegisterModule("image", a.config) httpsrv.HttpRegister("image", httpsrv.HttpRoutes{ {Name: "getImage", Method: "GET", Pattern: "/i/{name}", HandlerFunc: a.get}, {Name: "putImage", Method: "PUT", Pattern: "/i", HandlerFunc: a.put}, {Name: "getStyle", Method: "GET", Pattern: "/{style:avatar|bg|cover}/", HandlerFunc: a.getStyle}, }) } type image struct { store string maxSize int64 } func (a *image) config(config map[string]string) { a.store = "data/" if config["store"] != "" { a.store = config["store"] } if !chkStore(a.store) { log.Criticalf("Image: store location [%s] does not exist or is not writable.", a.store) } log.Noticef("Image: store location set to [%s]", a.store) a.maxSize = 10 * 1024 * 1024 if s, ok := config["max-size"]; ok { if i, err := strconv.Atoi(s); err == nil { a.maxSize = int64(i) } } log.Noticef("Image: store max-size set to [%d]", a.maxSize) } func (a *image) get(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["name"] ext := filepath.Ext(name) id := strings.TrimSuffix(name, ext) fname := filepath.Join(a.store, id) if !chkFile(fname) { httpsrv.WriteError(w, http.StatusNotFound, "Not Found") return } if chkGone(fname) { httpsrv.WriteError(w, http.StatusGone, "Gone") return } f, err := os.Open(fname) if err != nil { log.Fatal(err) } defer f.Close() pr := readutil.NewPreviewReader(f) mime, _ := readutil.ReadMIME(pr, name) w.Header().Set("Content-Type", mime) w.Header().Set("X-Content-Type-Options", "nosniff") _, _ = io.Copy(w, pr.Drain()) } func (a *image) put(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if h := r.Header.Get("Content-Length"); h != "" { log.Debugs("Artifact:", "content-length", h, "max-length", a.maxSize) if i, err := strconv.Atoi(h); err == nil { if int64(i) > a.maxSize { log.Error("ERR Size Too Large") http.Error(w, "ERR Size Too Large", http.StatusRequestEntityTooLarge) return } } } rdr := io.LimitReader(r.Body, a.maxSize) pr := readutil.NewPreviewReader(rdr) if !isImageOrVideo(pr) { httpsrv.WriteError(w, http.StatusUnsupportedMediaType, "ERR Not Image") return } rdr = pr.Drain() s256 := sha256.New() tmp, err := ioutil.TempFile(a.store, "image-") if err != nil { httpsrv.WriteError(w, 400, err.Error()) return } defer os.Remove(tmp.Name()) m := io.MultiWriter(s256, tmp) if _, err = io.Copy(m, rdr); err != nil { httpsrv.WriteError(w, 400, err.Error()) return } tmp.Close() id := base64.RawURLEncoding.EncodeToString(s256.Sum(nil)[12:]) fname := filepath.Join(a.store, id) log.Debugs("Image: moving file", "src", tmp.Name(), "dst", fname) err = os.Rename(tmp.Name(), fname) if err != nil { httpsrv.WriteError(w, 400, err.Error()) return } httpsrv.WriteText(w, http.StatusCreated, id) } func isImageOrVideo(in io.Reader) bool { buf := make([]byte, 320) _, err := in.Read(buf) if err != nil { return false } return filetype.IsImage(buf) || filetype.IsVideo(buf) } func (a *image) getStyle(w http.ResponseWriter, r *http.Request) { }