package main import ( "bytes" "errors" "image" _ "image/gif" _ "image/jpeg" _ "image/png" "io" "net/http" "strings" valid "github.com/asaskevich/govalidator" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" exifremove "github.com/scottleedavis/go-exif-remove" "github.com/twharmon/gouid" "golang.org/x/crypto/blake2b" "git.tcp.direct/tcp.direct/tcp.ac/config" ) var fExt string func imgDel(c *gin.Context) { slog := log.With().Str("caller", "imgView").Logger() rKey := c.Param("key") if !validateKey(rKey) { errThrow(c, 400, errors.New("failed to validate delete key"), "invalid request") return } targetImg, err := db.With("key").Get([]byte(rKey)) if err != nil { errThrow(c, 400, err, "invalid request") return } if targetImg == nil || !strings.Contains(string(targetImg), "i.") { errThrow(c, 400, errors.New("no img delete entry found with provided key"), "invalid request") return } finalTarget := strings.Split(string(targetImg), ".") if !db.With("img").Has([]byte(finalTarget[1])) { // this shouldn't happen...? errThrow(c, 500, errors.New("corresponding image todelete not found in database"), "internal server error") return } err = db.With("img").Delete([]byte(finalTarget[1])) if err != nil { errThrow(c, 500, err, "internal server error") return } if db.With("img").Has([]byte(finalTarget[1])) { slog.Error().Str("rkey", finalTarget[1]).Msg("delete failed!?") errThrow(c, 500, errors.New("failed to delete entry"), "internal server error") return } slog.Info().Str("rkey", finalTarget[1]).Msg("Image file deleted successfully") slog.Debug().Str("rkey", finalTarget[1]).Msg("Removing delete key entry") err = db.With("key").Delete([]byte(rKey)) if err != nil { slog.Error().Str("rkey", finalTarget[1]).Msg("Couldn't delete key") // it would be insane to try and delete the hash here } // if someone is uploading this image again after del c.JSON(200, "DELETE_SUCCESS") // and the file corresponding to the hash no longer exists // we will delete the hash entry then and re-add then } func imgView(c *gin.Context) { slog := log.With().Str("caller", "imgView").Logger() sUid := strings.Split(c.Param("uid"), ".") rUid := sUid[0] if len(sUid) > 1 { fExt = strings.ToLower(sUid[1]) slog.Debug().Str("ext", fExt).Msg("detected file extension") if fExt != "png" && fExt != "jpg" && fExt != "jpeg" && fExt != "gif" { errThrow(c, 400, errors.New("bad file extension"), "invalid request") return } } else { fExt = "nil" } // if it doesn't match the key size or it isn't alphanumeric - throw it out if !valid.IsAlphanumeric(rUid) || len(rUid) != config.UIDSize { slog.Warn(). Str("remoteaddr", c.ClientIP()). Msg("request discarded as invalid") errThrow(c, 400, errors.New("invalid request"), "invalid request") return } // now that we think its a valid request we will query slog.Debug().Str("rUid", rUid).Msg("request validated") // query bitcask for the id fBytes, _ := db.With("img").Get([]byte(rUid)) if fBytes == nil { slog.Error().Str("rUid", rUid).Msg("no corresponding file for this id") errThrow(c, 404, errors.New("entry not found"), "File not found") return } // read the data from bitcask into a reader file := bytes.NewReader(fBytes) imageFormat, err := checkImage(file) if err != nil { // extra sanity check to make sure we don't serve non-image data that somehow got into the database errThrow(c, http.StatusBadRequest, errors.New("entry in datbase is not an image: "+err.Error()), "invalid request") return } // additional extension sanity check - if they're gonna use an extension it needs to be the right one if fExt != "nil" && fExt != imageFormat { errThrow(c, 400, errors.New("requested file extension does not match filetype"), "invalid request") return } // extension or not (they are optional) // we give them the proper content type contentType := "image/" + imageFormat c.Data(200, contentType, fBytes) slog.Info().Str("rUid", rUid).Msg("Successful upload") } func newUIDandKey() (uid string, key string) { slog := log.With().Str("caller", "newUIDandKey").Logger() // generate new uid and delete key uid = gouid.String(config.UIDSize, gouid.MixedCaseAlphaNum) key = gouid.String(config.DeleteKeySize, gouid.MixedCaseAlphaNum) // lets make sure that we don't clash even though its highly unlikely for db.With("img").Has([]byte(uid)) { slog.Warn().Msg(" uid already exists! generating new...") uid = gouid.String(config.UIDSize, gouid.MixedCaseAlphaNum) } for db.With("key").Has([]byte(key)) { slog.Warn().Msg(" delete key already exists! generating new...") key = gouid.String(config.DeleteKeySize, gouid.MixedCaseAlphaNum) } return } func readAndScrubImage(file io.ReadSeeker) (scrubbed []byte, err error) { imageFormat, err := checkImage(file) if err != nil { return } // dump this into a byte object and scrub it // TO-DO: Write our own function for scrubbing exif fbytes, err := io.ReadAll(file) if err != nil { return } scrubbed = fbytes if imageFormat == "gif" { return } scrubbed, err = exifremove.Remove(fbytes) if err != nil { return } return } func imgPost(c *gin.Context) { slog := log.With().Str("caller", "imgPost"). Str("User-Agent", c.GetHeader("User-Agent")). Str("RemoteAddr", c.ClientIP()).Logger() var priv = false // check if incoming POST data is invalid f, err := c.FormFile("upload") if err != nil || f == nil { errThrow(c, http.StatusBadRequest, err, "invalid request") } slog.Debug().Str("filename", f.Filename).Msg("[+] New upload") // read the incoming file into an io file reader file, err := f.Open() if err != nil { errThrow(c, http.StatusInternalServerError, err, "error processing file\n") return } scrubbed, err := readAndScrubImage(file) if err != nil { errThrow(c, http.StatusBadRequest, err, "invalid request") return } Hashr, err := blake2b.New(64, nil) if err != nil { errThrow(c, http.StatusInternalServerError, err, "internal server error") } Hashr.Write(scrubbed) hash := Hashr.Sum(nil) // the keys (stored in memory) for db.With("hsh") are hashes // making it quick to find duplicates. the value is the uid if db.With("hsh").Has(hash) { imgRef, err := db.With("hsh").Get(hash) if err != nil { errThrow(c, http.StatusInternalServerError, err, "internal server error") return } slog.Debug().Msg("duplicate checksum in hash database, checking if file still exists...") if db.With("img").Has(imgRef) { slog.Debug().Str("ogUid", string(imgRef)).Msg("duplicate file found! returning original URL") post := &Post{entryType: Image, uid: string(imgRef), key: "", priv: false} post.Serve(c) return } slog.Debug(). Str("ogUid", string(imgRef)). Msg("stale hash found, deleting entry...") err = db.With("hsh").Delete(hash) if err != nil { slog.Warn().Err(err).Msg("failed to delete stale hash") } } uid, key := newUIDandKey() // save checksum to db to prevent dupes in the future err = db.With("hsh").Put([]byte(hash), []byte(uid)) if err != nil { errThrow(c, 500, err, "upload failed") return } // insert actual file to database slog.Debug().Str("uid", uid).Msg("saving file to database") err = db.With("img").Put([]byte(uid), []byte(scrubbed)) if err != nil { errThrow(c, 500, err, "upload failed") return } // add delete key to database with image prefix // there is a whole db for delete keys err = db.With("key").Put([]byte(key), []byte("i."+uid)) if err != nil { errThrow(c, http.StatusInternalServerError, err, "internal error") return } // good to go, send them to the finisher function slog.Debug().Str("uid", uid).Msg("saved to database successfully, sending to Serve") post := &Post{ entryType: Image, uid: uid, key: key, priv: priv, } post.Serve(c) } func checkImage(r io.ReadSeeker) (fmt string, err error) { // in theory this makes sure the file is an image via magic bytes _, fmt, err = image.Decode(r) if err != nil { return } _, err = r.Seek(0, 0) return } func getSize(s io.Seeker) (size int64, err error) { // get size of file if _, err = s.Seek(0, 0); err != nil { return } // 2 == from the end of the file if size, err = s.Seek(0, 2); err != nil { return } _, err = s.Seek(0, 0) return }