From c3cd9e9f0449b2b4a0e6828ed0a231b713455feb Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Mon, 18 Jul 2022 03:41:29 -0700 Subject: [PATCH] v1: Stabilize overhaul --- common.go | 81 ++++++------ config/config.go | 4 +- config/defaults.go | 6 +- delete.go | 91 ++++++++++++++ go.mod | 9 +- go.sum | 10 +- img.go | 304 +++++++++------------------------------------ main.go | 8 +- post.go | 133 ++++++++++++++++++++ router.go | 34 +++-- txt.go | 242 +++++++----------------------------- util.go | 38 ++++-- view.go | 62 +++++++++ 13 files changed, 497 insertions(+), 525 deletions(-) create mode 100644 delete.go create mode 100644 post.go create mode 100644 view.go diff --git a/common.go b/common.go index 0922c5a..b2c9dc3 100644 --- a/common.go +++ b/common.go @@ -7,9 +7,13 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "github.com/json-iterator/go" + "git.tcp.direct/tcp.direct/tcp.ac/config" ) +var json = jsoniter.ConfigCompatibleWithStandardLibrary + type EntryType uint8 const ( @@ -22,6 +26,7 @@ const ( type UserID uint64 +// Entry FIXME: not currently used type Entry interface { TypeCode() string UID() string @@ -48,9 +53,9 @@ func (p *Post) setLogger() { p.log = &pl } -func NewImg(data []byte, priv bool) *Post { +func newPost(entryType EntryType, data []byte, priv bool) *Post { p := &Post{ - entryType: Image, + entryType: entryType, priv: priv, data: data, } @@ -58,37 +63,35 @@ func NewImg(data []byte, priv bool) *Post { return p } -func NewTxt(data []byte, priv bool) *Post { - p := &Post{ - entryType: Text, - priv: priv, - data: data, +func typeToString(t EntryType, long bool) string { + switch t { + case Image: + if long { + return "img" + } + return "i" + case Text: + if long { + return "txt" + } + return "t" + case URL: + if long { + return "url" + } + return "u" + case Custom: + if long { + return "custom" + } + return "c" + default: + panic("unknown entry type") } - p.setLogger() - return p } func (p *Post) TypeCode(long bool) (code string) { - switch p.entryType { - case Image: - code = "i" - if long { - code += "mg" - } - case Text: - code = "t" - if long { - code += "xt" - } - case URL: - code = "u" - if long { - code += "rl" - } - default: - panic("not implemented") - } - return + return typeToString(p.entryType, long) } func (p *Post) UID() string { @@ -112,18 +115,15 @@ func (p *Post) Log() *zerolog.Logger { } func validateKey(rKey string) bool { - // if it doesn't match the key size or it isn't alphanumeric - throw it out if len(rKey) != config.DeleteKeySize || !valid.IsAlphanumeric(rKey) { - log.Warn().Str("rKey", rKey). - Msg("delete request failed sanity check!") return false } return true } func (p *Post) URLString() string { - var keyurl string = "" - url := config.BaseURL + p.TypeCode(false) + "/" + string(p.UID()) + var keyurl = "" + url := config.BaseURL + p.TypeCode(false) + "/" + p.UID() if p.DelKey() != "" { keyurl = config.BaseURL + "d/" + p.TypeCode(false) + "/" + p.DelKey() } @@ -137,8 +137,8 @@ func (p *Post) URLString() string { } func (p *Post) NewPostResponse(responder any) { - var keyurl string = "" - url := config.BaseURL + p.TypeCode(false) + "/" + string(p.UID()) + var keyurl = "" + url := config.BaseURL + p.TypeCode(false) + "/" + p.UID() if p.DelKey() != "" { keyurl = config.BaseURL + "d/" + p.TypeCode(false) + "/" + p.DelKey() } @@ -159,6 +159,15 @@ func (p *Post) NewPostResponse(responder any) { if cg, ginok := responder.(*gin.Context); ginok { cg.JSON(201, gin.H{urlString: url, delString: keyurl}) } + if ct, tdok := responder.(*textValidator); tdok { + js, err := json.Marshal(gin.H{urlString: url, delString: keyurl}) + if err != nil { + log.Error().Interface("post", p). + Err(err).Msg("json marshal failed") + ct.out = []byte("{\"error\":\"json marshal failed\"}") + } + ct.out = js + } return } diff --git a/config/config.go b/config/config.go index 7afc9bd..3b2f66c 100644 --- a/config/config.go +++ b/config/config.go @@ -46,7 +46,7 @@ func init() { var ( BaseURL, HTTPPort, HTTPBind, DBDir, LogDir, - TermbinListen, UnixSocketPath string + TermbinListen, UnixSocketPath, AdminKey string UIDSize, DeleteKeySize, KVMaxKeySizeMB, KVMaxValueSizeMB int UnixSocketPermissions uint32 @@ -220,6 +220,7 @@ func getConfigPaths() (paths []string) { return } +// TODO: use this? func loadCustomConfig(path string) { /* #nosec */ f, err := os.Open(path) @@ -267,6 +268,7 @@ func processOpts() { "logger.directory": &LogDir, "other.termbin_listen": &TermbinListen, "other.base_url": &BaseURL, + "admin.key": &AdminKey, } if !strings.HasSuffix(BaseURL, "/") { diff --git a/config/defaults.go b/config/defaults.go index e5fa07e..f54ddec 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -5,11 +5,12 @@ import ( "os" "runtime" + "git.tcp.direct/kayos/common/entropy" "github.com/spf13/afero" ) var ( - configSections = []string{"logger", "http", "data", "other"} + configSections = []string{"logger", "http", "data", "other", "admin"} defNoColor = false ) @@ -42,6 +43,9 @@ func initDefaults() { "termbin_listen": "127.0.0.1:9999", "base_url": "http://localhost:8080/", }, + "admin": { + "key": entropy.RandStrWithUpper(24), + }, } } diff --git a/delete.go b/delete.go new file mode 100644 index 0000000..9dc78a9 --- /dev/null +++ b/delete.go @@ -0,0 +1,91 @@ +package main + +import ( + "errors" + "strings" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "git.tcp.direct/tcp.direct/tcp.ac/config" +) + +func adminBypass(c *gin.Context, adminKey string, t EntryType) { + slog := log.With().Str("caller", "admin_del").Str("type", typeToString(t, true)).Logger() + slog.Trace().Msg("admin key attempt") + if config.AdminKey == "" { + errThrow(c, 404, errors.New("admin key not configured"), message404) + return + } + rKey := c.Param("key") + if adminKey != config.AdminKey { + if zerolog.GlobalLevel() == zerolog.TraceLevel { + slog.Warn().Str("wanted", config.AdminKey).Str("got", adminKey).Msg("bad admin key!") + } else { + slog.Warn().Msg("bad admin key!") + } + errThrow(c, 404, errors.New("bad key"), message404) + return + } + + slog.Trace().Msg("admin key accepted") + if !db.With(typeToString(t, true)).Has([]byte(rKey)) { + errThrow(c, 404, errors.New("failed to delete entry"), messageAdmin404) + return + } + err := db.With(typeToString(t, true)).Delete([]byte(rKey)) + if err != nil { + errThrow(c, 500, err, mustJson(map[string]string{"error": err.Error()})) + return + } + slog.Info().Msg("admin deleted entry") + c.JSON(200, "DELETE_SUCCESS") +} + +func del(c *gin.Context, t EntryType) { + slog := log.With().Str("caller", "del").Str("type", typeToString(t, true)).Logger() + rKey := c.Param("key") + adminKey, adminAttempt := c.GetQuery("admin") + if adminAttempt { + adminBypass(c, adminKey, t) + return + } + + if !validateKey(rKey) { + errThrow(c, 400, errors.New("failed to validate delete key"), message404) + return + } + + target, err := db.With("key").Get([]byte(rKey)) + if err != nil { + errThrow(c, 400, err, message400) + return + } + if target == nil || !strings.HasPrefix(string(target), typeToString(t, false)+".") { + errThrow(c, 400, errors.New("no delete entry found with provided key"), message404) + return + } + finalTarget := strings.Split(string(target), ".") + if !db.With(typeToString(t, true)).Has([]byte(finalTarget[1])) { + // this shouldn't happen...? + errThrow(c, 500, errors.New("corresponding image to delete not found in database"), message500) + return + } + err = db.With(typeToString(t, true)).Delete([]byte(finalTarget[1])) + if err != nil { + errThrow(c, 500, err, message500) + return + } + if db.With(typeToString(t, true)).Has([]byte(finalTarget[1])) { + errThrow(c, 500, errors.New("failed to delete entry"), message500) + return + } + slog.Info().Str("rkey", finalTarget[1]).Msg("Image file deleted successfully") + slog.Trace().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") + } + c.JSON(200, "DELETE_SUCCESS") +} diff --git a/go.mod b/go.mod index 888a91b..f503fb0 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,18 @@ go 1.18 require ( git.tcp.direct/kayos/common v0.7.0 - git.tcp.direct/kayos/putxt v0.0.0-20220707194005-5bc828145cc4 + git.tcp.direct/kayos/putxt v0.0.0-20220718092256-8851c78611cd git.tcp.direct/tcp.direct/database v0.0.0-20220610180603-058d36edd7f0 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/logger v0.2.2 github.com/gin-gonic/gin v1.8.1 + github.com/json-iterator/go v1.1.12 github.com/muesli/termenv v0.12.0 github.com/rs/zerolog v1.27.0 github.com/scottleedavis/go-exif-remove v0.0.0-20190908021517-58bdbaac8636 - github.com/spf13/afero v1.8.2 + github.com/spf13/afero v1.9.0 github.com/spf13/viper v1.12.0 - github.com/twharmon/gouid v0.5.2 - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d ) require ( @@ -36,7 +35,6 @@ require ( github.com/gofrs/flock v0.8.0 // indirect github.com/golang/geo v0.0.0-20190812012225-f41920e961ce // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect @@ -60,6 +58,7 @@ require ( github.com/yunginnanet/Rate5 v1.0.1 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85 // indirect golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect diff --git a/go.sum b/go.sum index 43508ae..d12ddf0 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ git.tcp.direct/Mirrors/bitcask-mirror v0.0.0-20220228092422-1ec4297c7e34 h1:tvoL git.tcp.direct/Mirrors/bitcask-mirror v0.0.0-20220228092422-1ec4297c7e34/go.mod h1:NX/Gqm/iy6Tg2CfcmmB/kW/qzKKrGR6o2koOKA5KWnc= git.tcp.direct/kayos/common v0.7.0 h1:KZDwoCzUiwQaYSWESr080N8wUVyLD27QYgzXgc7LiAQ= git.tcp.direct/kayos/common v0.7.0/go.mod h1:7tMZBVNPLFSZk+JXTA6pgXWpf/XHqYRfT7Q3OziI++Y= -git.tcp.direct/kayos/putxt v0.0.0-20220707194005-5bc828145cc4 h1:HhXghmJMzXSE/3clQRECP21OIcVv0za9dyzRlryaXno= -git.tcp.direct/kayos/putxt v0.0.0-20220707194005-5bc828145cc4/go.mod h1:WInY1F5uGGRQ6Bzq36OFrB240FvP9EVCDn0vqv4mEBM= +git.tcp.direct/kayos/putxt v0.0.0-20220718092256-8851c78611cd h1:s4dGya6D1KG7rOc6s7Jy7iZYkUVblbEuPAXtadoGDtA= +git.tcp.direct/kayos/putxt v0.0.0-20220718092256-8851c78611cd/go.mod h1:vtJNXcTDS9dn1nFEAG+Aprg/HLTgcXgyPvYs8Gp4FxY= git.tcp.direct/tcp.direct/database v0.0.0-20220610180603-058d36edd7f0 h1:p0DGzX6vm1xvj3OtmroTJ4eAX51FAcnYwpWmhkx6UA0= git.tcp.direct/tcp.direct/database v0.0.0-20220610180603-058d36edd7f0/go.mod h1:g5XsBf1G8T/ssqfYq65EBloB5NdRCTQBEC5dyqGHbQY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -376,8 +376,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.9.0 h1:sFSLUHgxdnN32Qy38hK3QkYBFXZj9DKjVjCUCtD7juY= +github.com/spf13/afero v1.9.0/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -414,8 +414,6 @@ github.com/tidwall/btree v0.4.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQF github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/redcon v1.4.1/go.mod h1:XwNPFbJ4ShWNNSA2Jazhbdje6jegTCcwFR6mfaADvHA= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/twharmon/gouid v0.5.2 h1:fqFUx700Ishb4dZaXpRv95CGGGR1CBuCkjM0t62XAxw= -github.com/twharmon/gouid v0.5.2/go.mod h1:m1SyQo0sYYbukI1yNZ1WRk980fV2XWBuYGAtMo/AmQ8= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= diff --git a/img.go b/img.go index bc28aba..41c8f80 100644 --- a/img.go +++ b/img.go @@ -3,6 +3,7 @@ package main import ( "bytes" "errors" + "image" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -11,296 +12,109 @@ import ( "strings" _ "git.tcp.direct/kayos/common" - "git.tcp.direct/kayos/common/entropy" - valid "github.com/asaskevich/govalidator" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" exifremove "github.com/scottleedavis/go-exif-remove" - - "git.tcp.direct/tcp.direct/tcp.ac/config" ) -var fExt string +type imageValidator struct{} -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.Trace().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 (i imageValidator) finalize(data []byte) ([]byte, error) { + return data, nil } -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.Trace().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.Trace().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 instantiateWithIDs(p *Post) *Post { - slog := log.With().Str("caller", "instantiateWithIDs").Logger() - // generate new uid and delete key - p.uid = entropy.RandStrWithUpper(config.UIDSize) - p.key = entropy.RandStrWithUpper(config.DeleteKeySize) - // lets make sure that we don't clash even though its highly unlikely - for db.With(p.TypeCode(true)).Has([]byte(p.UID())) { - slog.Warn().Msg(" uid already exists! generating new...") - p.uid = entropy.RandStrWithUpper(config.UIDSize) - } - for db.With("key").Has([]byte(p.DelKey())) { - slog.Warn().Msg(" delete key already exists! generating new...") - p.key = entropy.RandStrWithUpper(config.DeleteKeySize) - } - // save checksum to db to prevent dupes in the future - err := db.With("hsh").Put(p.Sum(), []byte(p.UID())) - if err != nil { - return nil - } - return p -} - -func savePost(p *Post) error { - // insert actual file to database - p.Log().Trace().Msg("saving file to database") - err := db.With(p.TypeCode(true)).Put([]byte(p.UID()), p.Bytes()) - if err != nil { - return err - } - return db. - With("key"). - Put( - []byte(p.DelKey()), - []byte(p.TypeCode(false)+"."+p.UID()), - ) -} - -func readAndScrubImage(file io.ReadSeeker) (scrubbed []byte, err error) { - imageFormat, err := checkImage(file) +func readAndScrubImage(c any, readHead io.ReadSeeker) (scrubbed []byte, err error) { + cg := c.(*gin.Context) + imageFormat, err := checkImage(readHead) if err != nil { return } - + cg.Set("real.extension", imageFormat) // dump this into a byte object and scrub it // TO-DO: Write our own function for scrubbing exif - fbytes, err := io.ReadAll(file) + fbytes, err := io.ReadAll(readHead) if err != nil { return } - scrubbed = fbytes - if imageFormat == "gif" { return } - scrubbed, err = exifremove.Remove(fbytes) if err != nil { return } - return } -type validatingScrubber func(c *gin.Context) ([]byte, error) +func (i imageValidator) getContentType(c *gin.Context) (string, error) { + imageType, ok := c.Get("real.extension") + if !ok { + return "", errors.New("no filetype in context") + } + return "image/" + imageType.(string), nil +} -func imgValidateAndScrub(c *gin.Context) ([]byte, error) { +func (i imageValidator) checkURL(c *gin.Context) error { + sUID := strings.Split(c.Param("uid"), ".") + var fExt string + if len(sUID) > 1 { + fExt = strings.ToLower(sUID[1]) + log.Trace().Str("caller", c.Request.RequestURI).Str("ext", fExt).Msg("detected file extension") + if fExt != "png" && fExt != "jpg" && fExt != "jpeg" && fExt != "gif" && fExt != "webm" { + return errors.New("bad file extension") + } + c.Set("url.extension", fExt) + } + return nil +} + +func (i imageValidator) checkContent(c *gin.Context, data []byte) error { + readHead := bytes.NewReader(data) + var err error + _, err = readAndScrubImage(c, readHead) + if err != nil { + return err + } + urlExt, uExists := c.Get("url.extension") + bytExt, bExists := c.Get("real.extension") + if uExists && bExists && urlExt != bytExt { + return errors.New("bad file extension") + } + return nil +} + +func (i imageValidator) checkAndScrubPost(c any) ([]byte, error) { + cg := c.(*gin.Context) slog := log.With().Str("caller", "imgPost"). - Str("User-Agent", c.GetHeader("User-Agent")). - Str("RemoteAddr", c.ClientIP()).Logger() + Str("User-Agent", cg.GetHeader("User-Agent")). + Str("RemoteAddr", cg.ClientIP()).Logger() // check if incoming POST data is invalid - f, err := c.FormFile("upload") + f, err := cg.FormFile("upload") if err != nil || f == nil { return nil, err } - 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") + errThrow(cg, http.StatusInternalServerError, err, message500) return nil, err } - - scrubbed, err := readAndScrubImage(file) + scrubbed, err := readAndScrubImage(c, file) if err != nil { - errThrow(c, http.StatusBadRequest, err, "invalid request") + errThrow(cg, http.StatusBadRequest, err, message400) return nil, err } return scrubbed, nil } -func getOldRef(p *Post) (*Post, error) { - var oldRef []byte - oldRef, err := db.With("hsh").Get(p.Sum()) +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 nil, err + return } - p.Log().Trace().Caller().Msg("duplicate checksum in hash database, checking if file still exists...") - if db.With(p.TypeCode(true)).Has(oldRef) { - p.Log().Debug().Str("ogUid", string(oldRef)). - Msg("duplicate file found! returning original URL") - p.uid = string(oldRef) - p.key = "" - p.priv = false - return p, nil - } - p.Log().Trace(). - Str("ogUid", string(oldRef)). - Msg("stale hash found, deleting entry...") - err = db.With("hsh").Delete(p.Sum()) - if err != nil { - p.Log().Error().Err(err).Msg("failed to delete stale hash") - p = nil - } - return p, err -} - -func post(c *gin.Context, vas validatingScrubber, t EntryType) error { - scrubbed, err := vas(c) - if err != nil { - if c != nil { - return errThrow(c, http.StatusBadRequest, err, "invalid request") - } - return err - } - var p *Post - switch t { - case Image: - p = NewImg(scrubbed, false) - case Text: - p = NewTxt(scrubbed, false) - default: - return errors.New("invalid entry type") - } - - // 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(p.Sum()) { - p, err = getOldRef(p) - if err != nil { - if c != nil { - return errThrow(c, http.StatusInternalServerError, err, "internal server error") - } - return err - } - } - - p = instantiateWithIDs(p) - if p == nil { - if c != nil { - return errThrow(c, 500, err, "upload failed") - } - return err - } - - err = savePost(p) - if err != nil { - if c != nil { - return errThrow(c, http.StatusInternalServerError, err, "internal error") - } - return err - } - - // good to go, send them to the finisher function - p.Log().Trace().Msg("saved to database successfully, sending to NewPostResponse") - - p.NewPostResponse(c) - return nil + _, err = r.Seek(0, 0) + return } diff --git a/main.go b/main.go index dd3aec4..cdc5df4 100644 --- a/main.go +++ b/main.go @@ -67,6 +67,12 @@ func main() { log.Warn().Err(err).Msg("sync failure!") } }() - go serveTermbin() + go func() { + err := serveTermbin() + if err != nil { + log.Fatal().Err(err).Msg("failed to start termbin") + } + }() + time.Sleep(50 * time.Millisecond) wait(httpRouter()) } diff --git a/post.go b/post.go new file mode 100644 index 0000000..a63915a --- /dev/null +++ b/post.go @@ -0,0 +1,133 @@ +package main + +import ( + "errors" + "net/http" + + "git.tcp.direct/kayos/common/entropy" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + + "git.tcp.direct/tcp.direct/tcp.ac/config" +) + +func mustJson(v any) []byte { + js, err := json.Marshal(v) + if err != nil { + panic(err) + } + return js +} + +var ( + message500 = mustJson(map[string]string{"error": "internal server error"}) + message400 = mustJson(map[string]string{"error": "bad request"}) + message404 = mustJson(map[string]string{"error": "file not found"}) + messageAdmin404 = mustJson(map[string]string{"error": "post id does not exist"}) +) + +type validator interface { + checkURL(c *gin.Context) error + checkContent(c *gin.Context, data []byte) error + checkAndScrubPost(c any) ([]byte, error) + getContentType(c *gin.Context) (string, error) + finalize(data []byte) ([]byte, error) +} + +func post(c any, vas validator, t EntryType, priv bool) error { + scrubbed, err := vas.checkAndScrubPost(c) + if err != nil { + switch c.(type) { + case *gin.Context: + return errThrow(c.(*gin.Context), http.StatusBadRequest, err, message400) + default: + return err + } + } + + p := newPost(t, scrubbed, priv) + + var exists bool + // the keyspace (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(p.Sum()) { + p, err, exists = getOldRef(p) + if err != nil { + switch c.(type) { + case *gin.Context: + return errThrow(c.(*gin.Context), http.StatusInternalServerError, err, message500) + default: + return err + } + } + } + + if exists { + p.NewPostResponse(c) + return nil + } + + p = instantiateWithIDs(p) + if p == nil { + switch c.(type) { + case *gin.Context: + return errThrow(c.(*gin.Context), 500, err, message500) + default: + return errors.New("upload failed") + } + } + + err = savePost(p) + if err != nil { + switch c.(type) { + case *gin.Context: + return errThrow(c.(*gin.Context), http.StatusInternalServerError, err, message500) + default: + return err + } + } + + // good to go, send them to the finisher function + p.Log().Trace().Msg("saved to database successfully, sending to NewPostResponse") + + p.NewPostResponse(c) + return nil +} + +func savePost(p *Post) error { + // insert actual file to database + p.Log().Trace().Msg("saving file to database") + err := db.With(p.TypeCode(true)).Put([]byte(p.UID()), p.Bytes()) + if err != nil { + return err + } + return db. + With("key"). + Put( + []byte(p.DelKey()), + []byte(p.TypeCode(false)+"."+p.UID()), + ) +} + +func instantiateWithIDs(p *Post) *Post { + slog := log.With().Str("caller", "instantiateWithIDs").Logger() + // generate new uid and delete key + p.uid = entropy.RandStrWithUpper(config.UIDSize) + p.key = entropy.RandStrWithUpper(config.DeleteKeySize) + // lets make sure that we don't clash even though its highly unlikely + for db.With(p.TypeCode(true)).Has([]byte(p.UID())) { + slog.Warn().Msg(" uid already exists! generating new...") + p.uid = entropy.RandStrWithUpper(config.UIDSize) + } + for db.With("key").Has([]byte(p.DelKey())) { + slog.Warn().Msg(" delete key already exists! generating new...") + p.key = entropy.RandStrWithUpper(config.DeleteKeySize) + } + // save checksum to db to prevent dupes in the future + err := db.With("hsh").Put(p.Sum(), []byte(p.UID())) + if err != nil { + slog.Error().Err(err).Msg("failed to save checksum to db") + return nil + } + return p +} diff --git a/router.go b/router.go index 9fbbef6..38c2eac 100644 --- a/router.go +++ b/router.go @@ -15,12 +15,12 @@ import ( "git.tcp.direct/tcp.direct/tcp.ac/config" ) -var favicon string = "AAABAAEAUFAQAAEABAC0CgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAABQAAAAUAgGAAAAjhHyrQAACntJREFUeJztXF1oG9kV/sbWeKTYkSxbUeufrIxjJxTi+CfdNBAX6gbSQE3thoJhQ8s2y5YNgYRlYSFst+6mLH4ptOsS1uziQlgSGlpCDF5oAo3zYEPWTezECYTUSprZKHaryJIlO5aUiTJ9mJ3xSDOauSONNHKa70lo7s+55557zrnnnnupCtrN88/X8Aq5gQLAW03ERkYZTTFW07Ch8UoC88QrCcwTNsq2CeCSAIDdh52wO8tN7SARS+HG2Rhx+db9dni3O0xr19tmQ2v3ZuL+s2F2LIq1hReK/21yC3zwvddQ5dEn3ghWQ3HcOHuHuPyB4w1o7KzRLReYDRMxcM+hGux7y0fcfzYE/3Ub8wsJxf82jk/m3Xi+2H3YiboWOwDA7rIR1bG7bOgd9GL8o2AhSdOFjaYYWMlEmmLQfqAGrT1bDNXzNDvhaXbi8h+f4FlU3Q4WQ79LOtAqY3LoEw8c1bnr3Z/+rl76ff74YzNIMgRJB1olhR399fqFCOtbwkCRcTTFwD8ZQaVrNWthR3W5pOBXQ3FUeRwIzIYRX05lrfM0ymkSILajBnnbdW1VmgZuNRTX7Efe1/zEE92yAIjUiqQDOT6JCydCmoX3vLNJYqA4mJt/D2N6ZN2Sc7wxdZDJFDlDQ2xCoun4RJMmA0m8B7HM+O8DiNzRVxvv39CeNCDDCmstYy2myOuRGCWaYvCj37jg69qskEBpYi4uwP/VijQh9yYiCMwJq0Nt2ZNIYDaas9Eox08+9CHQt6pQE6ZbYdK2KmvprP7eaiiOCydCUlscn8SlU1Hpe0u3WyEZZvuvmfA0O5GIPlf8X2aF8dDqMzAbxq2xoKKMqGYA4OrnjxGYDRedRjVdT7wXNpPRWn2G2ESatKnRMT2yhhCbviswuoT1oEajmrtFvBMxy0+kKQbHJ5pgd9KKb387eQ+x/zzLOlkVLgp8rEL1WyGWcCLG6RsRUh0olNlkCmF2J61K2Ny5ZFZaaIrBs2gSgGBUKl3KCTATHJ9UneRMfhVdArWgRUumt/CXk1+jqjYAl9eGaFCp3PMFTTEKCYwvpxQ0WiKB2ZaGEY9gbeEFuEUaEfDgePMZqCaBJaEDAaguDcC4oSqkB0E63pKJSK+G4pYFNIwgk0ZL/MBELH1/HJgN4+OuO5aG1dSQSaeaDiy6BGazbqUGYh0oPxMpNDbCEhVBSquNNCvBDCtsNFJTKPS8/S3DdQKzYYTYRGn4gSQefiGRSxD38vBjPLzC564DzVLwWh5+qUONB2WUjWxZ5jtAsb7o4ZMQtxFQZkwH5g55/Y1ghUlB7AearQPl0DpTKXVYFo2Rw1FdbmgfnC+mRlnDdaLB56o0EvuBhVbyxdSB0xfCRIdK6ShXpbGofmCpwYxJKwkdaAXMknhLzkQydWBjZw2Gvn7dtPZJYJZAlBW7Q+Alk8BidwiUhh9olkCQJeOZ2CFgzl44M5PVaCasWQJBzEAtK2zUhzNDArv6ahVBgRtn/5l3u0Zhig4kZV6FiwIALD+Kqx6E9w56sfuwU5eO3kGv4ljT7IN1UhRNBwrnukIm6Wf9AfgnI4oy+97yoeV7+gnh7X1eReqZVeGxovmB8kQhLTTuqsLAcIPqt92HnTj0iUeVWZZJoBV+YCbkg/c0O9HS7VaUoSkGdS32rMFQqySwJPbCaqlqR8404d61KBKxFNoPCGlwbp86DaEHMURYa+KJJbsXbu3ZguCDNUSgn2obYZP44s3F4hCWAUv8QADwf7UCYEE1WVJEe5+XeNdiVUTbkp0Ixycxd07IydbLe/Y0a7s1Iqw6U7FkLwysT8jUKJtXtunUKItbl8P/XxIob/PSqagi29QIxj8KGtrCmQ3LdKAcM2NLCNwVjFnHwRrV5HN5Nv/UKItI0PqIDmDSXjgfcHwSD68weHhlnYFqkOvKRX9CM5u1mKBoiuFJCKEpBpTzGXzfTZfEx3efqd6jzQU0xcC9MwWXV3te2evJrBcMM9uj6zhU1ZaltTn/j9xVRiaIGSgnSkQhJEBPVeTSZyFpNszAjQIxo7/QYyO2whsJYuRHi3mt++26oTMSvJQMJLkH9/1ffBs/G9qRd1/l5ZTtty9gLLVi31E3XnvdgUfXzVPGxcQLpLD9h5WwVfCo9ZVjc2M5Ukih56gHvC2F8L/Js/6J3Rg5fvB2AxIxDlOfKoOiGwUXToTg3vlfvPtlJ25eXIDdWf7N4xSsISttiIGt++3o6qvF8iMhfjcw3ICZsSWiDnsHvQBg+SMRcqwuCe6Xxyc8eBGYDRt20A3pQO92Bzr668HOrICdWUFHfz3RGy8AsKPHjfY+ryHiCgmOT6KqVhh+Y2cNWrrdON13P+3yOAly2spFghwSsRTmJ55gx14X3F5aU7JoipEuS5cKaIrB6hKH+YknqGurkv6XG6CB4QY8XeI0x5ZTMMHtpTF3Tghi1rVVwdelfRAkPidgxaMQ2cDxSXCLNL54c1FSSZlo6Xbrjo1YAgeGG+Dx2XHz4gIW/Yn0jNMsj+WIdQDhbYWpT4vrsB8505R2tyPEJtImURwDO7OC+HIKx8a2pdWv8jiyMlcEEQNpioHHZ0f1VgdO991XbLfUrsIDgnIWIyvszApJV6bC7WMyArLqccdLp6LY8w6H3pMtim96LynpMtDbZsPP/9SECJsEOxOUshD2HXVj74AX/smIFIqSQ87kqVEW9ybVY3bHxrZJkvHeVcGxTUSf43Tf/aw0HRvbljawwJzyMQhAyIBYDcVxa0zQYVoWdn5yBVOjLNr7vGkPX6iNTQ5NBtIUA5eXgqfZicDcAi6dikpi7/bS8DQ7ce18UNUfFMuthuKaSliQUEEy0qUlOwOrtzrSwltqJ3LiBCZi2kZApDV4Gxi/HcSOnvUzmsDdNV1fV1cC2etJjA/50XGwBscnmjDc8xC/utgIABgf8mN+Untpap150BSD8SE/dux14YOZnRgf8gMQJueDmZ24NRZMG3zvoFeSkNCDGK6dD6ouOxHyAynS51iufr4Iu1Poc35yRbeeJgM5PglEgemRNXQcrIHdSQsXZVw22J00pkfWdC87a2UMCA9IAI3f2YQqj0PywXa9wWCfx4HK2vX8F5piUFm7/lRAIvoc0yNr6D2ZvW+7k5aYmMkENcYIh13IKK89PmIr/Ndfs9hzqEaSjHuTsawzs6m+DB9e2w1AP+WC45NSopDeXbqnS+sS1dhZg/dvOPDnX97OeuU/EePSMsEGhhvQ0V+PP/x4FsHb6rQbDX8RMVDUEZHu9by+h1cK//SqnGEcn0yTSEBQD+z15DcveaQzUbxStvwoDm9bTlt+IhjaiUyPrGF6ZI741uXNiwu6zrNaO3PnkopcP5pi0hgamA3js/4AOJ4HoG5EPu66g95BL979slOX1lxBfFtTTV9ogTRbiuOTaS+8qbUr11fy07ldbzAA1idAfrwplo8EuYJmbhUkpC9KFWm7R840obVnC06+pp1hKrZ7it2l+l2tvpqEmznegigHowQGH6zB7dM/HBfbDcyGUb3VIRkJrfyZQp+JbLhDpWy616oxFM48FQilNtkv5aFSMUFV0O5Xb+nngf8BUt0zxPrVVgIAAAAASUVORK5CYII=" -var staticIndex string = "PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KCTxtZXRhIGNoYXJzZXQ9IlVURi04Ij4KCTxtZXRhIG5hbWU9ImRlc2NyaXB0aW9uIiBjb250ZW50PSJ0Y3AuYWMgLSB1bmRlciBjb25zdHJ1Y3Rpb24gLSBpbWFnZSB1cGxvYWRpbmcsIHVybCBzaG9ydGVuaW5nLCB0ZXh0IGJpbiI+Cgk8bWV0YSBuYW1lPSJhdXRob3IiIGNvbnRlbnQ9InRjcC5kaXJlY3QiPgoJPHRpdGxlPnRjcC5hYzwvdGl0bGU+CgoJPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCQlib2R5IHsKCQkJYmFja2dyb3VuZC1jb2xvcjojMTAxMDEwOwoJCQljb2xvcjojOTQ4REI4OwoJCQlmb250LWZhbWlseTptb25vc3BhY2U7CgkJCXRleHQtYWxpZ246Y2VudGVyOwoJCX0KCQkuaGVsbG8gewoJCQlwb3NpdGlvbjogZml4ZWQ7CgkJCXRvcDogNTAlOwoJCQlsZWZ0OiA1MCU7CgkJCXRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpOwoJCX0KCQkuZmFkaW5nIHsKCQkJYW5pbWF0aW9uOmZhZGluZyA1cyBpbmZpbml0ZQoJCX0KCQlAa2V5ZnJhbWVzIGZhZGluZ3sKCQkJMCV7b3BhY2l0eTowfQoJCQk1MCV7b3BhY2l0eToxfQoJCQkxMDAle29wYWNpdHk6MH0KCQl9Cgk8L3N0eWxlPgo8L2hlYWQ+Cjxib2R5Pgo8ZGl2IGNsYXNzPSJoZWxsbyI+CjxwcmUgY2xhc3M9ImZhZGluZyI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAsZCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgODggICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIApNTTg4TU1NICxhZFBQWWJhLCA4YixkUFBZYmEsICAgICAgLGFkUFBZWWJhLCAgLGFkUFBZYmEsICAKICA4OCAgIGE4IiAgICAgIiIgODhQJyAgICAiOGEgICAgICIiICAgICBgWTggYTgiICAgICAiIiAgCiAgODggICA4YiAgICAgICAgIDg4ICAgICAgIGQ4ICAgICAsYWRQUFBQUDg4IDhiICAgICAgICAgIAogIDg4LCAgIjhhLCAgICxhYSA4OGIsICAgLGE4IiA4ODggODgsICAgICw4OCAiOGEsICAgLGFhICAKICAiWTg4OCBgIlliYmQ4IicgODhgWWJiZFAiJyAgODg4IGAiOGJiZFAiWTggIGAiWWJiZDgiJyAgCiAgICAgICAgICAgICAgICAgIDg4ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICA4OCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKCjwvcHJlPgo8L2Rpdj4KCjxhIGhyZWY9Imh0dHA6Ly9hZG1pbi50Y3AuYWMvIiBzdHlsZT0iZGlzcGxheTpub25lOyI+YWRtaW4gbG9naW48L2E+Cgo8L2JvZHk+Cgo8L2h0bWw+Cg==" +const favicon = "AAABAAEAUFAQAAEABAC0CgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAABQAAAAUAgGAAAAjhHyrQAACntJREFUeJztXF1oG9kV/sbWeKTYkSxbUeufrIxjJxTi+CfdNBAX6gbSQE3thoJhQ8s2y5YNgYRlYSFst+6mLH4ptOsS1uziQlgSGlpCDF5oAo3zYEPWTezECYTUSprZKHaryJIlO5aUiTJ9mJ3xSDOauSONNHKa70lo7s+55557zrnnnnupCtrN88/X8Aq5gQLAW03ERkYZTTFW07Ch8UoC88QrCcwTNsq2CeCSAIDdh52wO8tN7SARS+HG2Rhx+db9dni3O0xr19tmQ2v3ZuL+s2F2LIq1hReK/21yC3zwvddQ5dEn3ghWQ3HcOHuHuPyB4w1o7KzRLReYDRMxcM+hGux7y0fcfzYE/3Ub8wsJxf82jk/m3Xi+2H3YiboWOwDA7rIR1bG7bOgd9GL8o2AhSdOFjaYYWMlEmmLQfqAGrT1bDNXzNDvhaXbi8h+f4FlU3Q4WQ79LOtAqY3LoEw8c1bnr3Z/+rl76ff74YzNIMgRJB1olhR399fqFCOtbwkCRcTTFwD8ZQaVrNWthR3W5pOBXQ3FUeRwIzIYRX05lrfM0ymkSILajBnnbdW1VmgZuNRTX7Efe1/zEE92yAIjUiqQDOT6JCydCmoX3vLNJYqA4mJt/D2N6ZN2Sc7wxdZDJFDlDQ2xCoun4RJMmA0m8B7HM+O8DiNzRVxvv39CeNCDDCmstYy2myOuRGCWaYvCj37jg69qskEBpYi4uwP/VijQh9yYiCMwJq0Nt2ZNIYDaas9Eox08+9CHQt6pQE6ZbYdK2KmvprP7eaiiOCydCUlscn8SlU1Hpe0u3WyEZZvuvmfA0O5GIPlf8X2aF8dDqMzAbxq2xoKKMqGYA4OrnjxGYDRedRjVdT7wXNpPRWn2G2ESatKnRMT2yhhCbviswuoT1oEajmrtFvBMxy0+kKQbHJ5pgd9KKb387eQ+x/zzLOlkVLgp8rEL1WyGWcCLG6RsRUh0olNlkCmF2J61K2Ny5ZFZaaIrBs2gSgGBUKl3KCTATHJ9UneRMfhVdArWgRUumt/CXk1+jqjYAl9eGaFCp3PMFTTEKCYwvpxQ0WiKB2ZaGEY9gbeEFuEUaEfDgePMZqCaBJaEDAaguDcC4oSqkB0E63pKJSK+G4pYFNIwgk0ZL/MBELH1/HJgN4+OuO5aG1dSQSaeaDiy6BGazbqUGYh0oPxMpNDbCEhVBSquNNCvBDCtsNFJTKPS8/S3DdQKzYYTYRGn4gSQefiGRSxD38vBjPLzC564DzVLwWh5+qUONB2WUjWxZ5jtAsb7o4ZMQtxFQZkwH5g55/Y1ghUlB7AearQPl0DpTKXVYFo2Rw1FdbmgfnC+mRlnDdaLB56o0EvuBhVbyxdSB0xfCRIdK6ShXpbGofmCpwYxJKwkdaAXMknhLzkQydWBjZw2Gvn7dtPZJYJZAlBW7Q+Alk8BidwiUhh9olkCQJeOZ2CFgzl44M5PVaCasWQJBzEAtK2zUhzNDArv6ahVBgRtn/5l3u0Zhig4kZV6FiwIALD+Kqx6E9w56sfuwU5eO3kGv4ljT7IN1UhRNBwrnukIm6Wf9AfgnI4oy+97yoeV7+gnh7X1eReqZVeGxovmB8kQhLTTuqsLAcIPqt92HnTj0iUeVWZZJoBV+YCbkg/c0O9HS7VaUoSkGdS32rMFQqySwJPbCaqlqR8404d61KBKxFNoPCGlwbp86DaEHMURYa+KJJbsXbu3ZguCDNUSgn2obYZP44s3F4hCWAUv8QADwf7UCYEE1WVJEe5+XeNdiVUTbkp0Ixycxd07IydbLe/Y0a7s1Iqw6U7FkLwysT8jUKJtXtunUKItbl8P/XxIob/PSqagi29QIxj8KGtrCmQ3LdKAcM2NLCNwVjFnHwRrV5HN5Nv/UKItI0PqIDmDSXjgfcHwSD68weHhlnYFqkOvKRX9CM5u1mKBoiuFJCKEpBpTzGXzfTZfEx3efqd6jzQU0xcC9MwWXV3te2evJrBcMM9uj6zhU1ZaltTn/j9xVRiaIGSgnSkQhJEBPVeTSZyFpNszAjQIxo7/QYyO2whsJYuRHi3mt++26oTMSvJQMJLkH9/1ffBs/G9qRd1/l5ZTtty9gLLVi31E3XnvdgUfXzVPGxcQLpLD9h5WwVfCo9ZVjc2M5Ukih56gHvC2F8L/Js/6J3Rg5fvB2AxIxDlOfKoOiGwUXToTg3vlfvPtlJ25eXIDdWf7N4xSsISttiIGt++3o6qvF8iMhfjcw3ICZsSWiDnsHvQBg+SMRcqwuCe6Xxyc8eBGYDRt20A3pQO92Bzr668HOrICdWUFHfz3RGy8AsKPHjfY+ryHiCgmOT6KqVhh+Y2cNWrrdON13P+3yOAly2spFghwSsRTmJ55gx14X3F5aU7JoipEuS5cKaIrB6hKH+YknqGurkv6XG6CB4QY8XeI0x5ZTMMHtpTF3Tghi1rVVwdelfRAkPidgxaMQ2cDxSXCLNL54c1FSSZlo6Xbrjo1YAgeGG+Dx2XHz4gIW/Yn0jNMsj+WIdQDhbYWpT4vrsB8505R2tyPEJtImURwDO7OC+HIKx8a2pdWv8jiyMlcEEQNpioHHZ0f1VgdO991XbLfUrsIDgnIWIyvszApJV6bC7WMyArLqccdLp6LY8w6H3pMtim96LynpMtDbZsPP/9SECJsEOxOUshD2HXVj74AX/smIFIqSQ87kqVEW9ybVY3bHxrZJkvHeVcGxTUSf43Tf/aw0HRvbljawwJzyMQhAyIBYDcVxa0zQYVoWdn5yBVOjLNr7vGkPX6iNTQ5NBtIUA5eXgqfZicDcAi6dikpi7/bS8DQ7ce18UNUfFMuthuKaSliQUEEy0qUlOwOrtzrSwltqJ3LiBCZi2kZApDV4Gxi/HcSOnvUzmsDdNV1fV1cC2etJjA/50XGwBscnmjDc8xC/utgIABgf8mN+Untpap150BSD8SE/dux14YOZnRgf8gMQJueDmZ24NRZMG3zvoFeSkNCDGK6dD6ouOxHyAynS51iufr4Iu1Poc35yRbeeJgM5PglEgemRNXQcrIHdSQsXZVw22J00pkfWdC87a2UMCA9IAI3f2YQqj0PywXa9wWCfx4HK2vX8F5piUFm7/lRAIvoc0yNr6D2ZvW+7k5aYmMkENcYIh13IKK89PmIr/Ndfs9hzqEaSjHuTsawzs6m+DB9e2w1AP+WC45NSopDeXbqnS+sS1dhZg/dvOPDnX97OeuU/EePSMsEGhhvQ0V+PP/x4FsHb6rQbDX8RMVDUEZHu9by+h1cK//SqnGEcn0yTSEBQD+z15DcveaQzUbxStvwoDm9bTlt+IhjaiUyPrGF6ZI741uXNiwu6zrNaO3PnkopcP5pi0hgamA3js/4AOJ4HoG5EPu66g95BL979slOX1lxBfFtTTV9ogTRbiuOTaS+8qbUr11fy07ldbzAA1idAfrwplo8EuYJmbhUkpC9KFWm7R840obVnC06+pp1hKrZ7it2l+l2tvpqEmznegigHowQGH6zB7dM/HBfbDcyGUb3VIRkJrfyZQp+JbLhDpWy616oxFM48FQilNtkv5aFSMUFV0O5Xb+nngf8BUt0zxPrVVgIAAAAASUVORK5CYII=" +const staticIndex = "PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KCTxtZXRhIGNoYXJzZXQ9IlVURi04Ij4KCTxtZXRhIG5hbWU9ImRlc2NyaXB0aW9uIiBjb250ZW50PSJ0Y3AuYWMgLSB1bmRlciBjb25zdHJ1Y3Rpb24gLSBpbWFnZSB1cGxvYWRpbmcsIHVybCBzaG9ydGVuaW5nLCB0ZXh0IGJpbiI+Cgk8bWV0YSBuYW1lPSJhdXRob3IiIGNvbnRlbnQ9InRjcC5kaXJlY3QiPgoJPHRpdGxlPnRjcC5hYzwvdGl0bGU+CgoJPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCQlib2R5IHsKCQkJYmFja2dyb3VuZC1jb2xvcjojMTAxMDEwOwoJCQljb2xvcjojOTQ4REI4OwoJCQlmb250LWZhbWlseTptb25vc3BhY2U7CgkJCXRleHQtYWxpZ246Y2VudGVyOwoJCX0KCQkuaGVsbG8gewoJCQlwb3NpdGlvbjogZml4ZWQ7CgkJCXRvcDogNTAlOwoJCQlsZWZ0OiA1MCU7CgkJCXRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpOwoJCX0KCQkuZmFkaW5nIHsKCQkJYW5pbWF0aW9uOmZhZGluZyA1cyBpbmZpbml0ZQoJCX0KCQlAa2V5ZnJhbWVzIGZhZGluZ3sKCQkJMCV7b3BhY2l0eTowfQoJCQk1MCV7b3BhY2l0eToxfQoJCQkxMDAle29wYWNpdHk6MH0KCQl9Cgk8L3N0eWxlPgo8L2hlYWQ+Cjxib2R5Pgo8ZGl2IGNsYXNzPSJoZWxsbyI+CjxwcmUgY2xhc3M9ImZhZGluZyI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAsZCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgODggICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIApNTTg4TU1NICxhZFBQWWJhLCA4YixkUFBZYmEsICAgICAgLGFkUFBZWWJhLCAgLGFkUFBZYmEsICAKICA4OCAgIGE4IiAgICAgIiIgODhQJyAgICAiOGEgICAgICIiICAgICBgWTggYTgiICAgICAiIiAgCiAgODggICA4YiAgICAgICAgIDg4ICAgICAgIGQ4ICAgICAsYWRQUFBQUDg4IDhiICAgICAgICAgIAogIDg4LCAgIjhhLCAgICxhYSA4OGIsICAgLGE4IiA4ODggODgsICAgICw4OCAiOGEsICAgLGFhICAKICAiWTg4OCBgIlliYmQ4IicgODhgWWJiZFAiJyAgODg4IGAiOGJiZFAiWTggIGAiWWJiZDgiJyAgCiAgICAgICAgICAgICAgICAgIDg4ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICA4OCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKCjwvcHJlPgo8L2Rpdj4KCjxhIGhyZWY9Imh0dHA6Ly9hZG1pbi50Y3AuYWMvIiBzdHlsZT0iZGlzcGxheTpub25lOyI+YWRtaW4gbG9naW48L2E+Cgo8L2JvZHk+Cgo8L2h0bWw+Cg==" func favIcon(c *gin.Context) { ico, _ := base64.StdEncoding.DecodeString(favicon) - c.Data(200, "image/ico", []byte(ico)) + c.Data(200, "image/ico", ico) } func placeHolder(c *gin.Context) { @@ -28,10 +28,6 @@ func placeHolder(c *gin.Context) { c.Data(200, "text/html", html) } -func urlPost(c *gin.Context) { - return -} - func httpRouter() *http.Server { if !config.Trace { log.Debug().Caller().Msg("running gin in release mode, enable trace to run gin in debug mode") @@ -45,6 +41,12 @@ func httpRouter() *http.Server { router.Use(logger.SetLogger( logger.WithLogger( func(c *gin.Context, w io.Writer, d time.Duration) zerolog.Logger { + if zerolog.GlobalLevel() > zerolog.DebugLevel { + // because this spams the logs + if c.Request.URL.String() == "/ip" { + return zerolog.Nop() + } + } return log.With(). Str("caller", c.ClientIP()). Str("url", c.Request.URL.String()). @@ -59,29 +61,23 @@ func httpRouter() *http.Server { // use gzip compression unless someone requests something with an explicit extension router.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPathsRegexs([]string{".*"}))) - // static html and such - // workaround the issue where the router tries to handle /* - // router.Static("/h", "../public") - router.GET("/favicon.ico", favIcon) router.GET("/", placeHolder) - router.GET("/ip", func(c *gin.Context) { - c.String(200, c.ClientIP()) - }) + router.GET("/ip", func(c *gin.Context) { c.String(200, c.ClientIP()) }) imgR := router.Group("/i") imgR.GET("/", placeHolder) - imgR.POST("/put", imgPost) - imgR.GET("/:uid", imgView) + imgR.POST("/put", func(c *gin.Context) { post(c, imageValidator{}, Image, false) }) + imgR.GET("/:uid", func(c *gin.Context) { view(c, imageValidator{}, Image) }) txtR := router.Group("/t") txtR.GET("/", placeHolder) - txtR.GET("/:uid", txtView) + txtR.GET("/:uid", func(c *gin.Context) { view(c, textValidator{}, Text) }) delR := router.Group("/d") - delR.GET("/i/:key", imgDel) - delR.GET("/t/:key", txtDel) + delR.GET("/i/:key", func(c *gin.Context) { del(c, Image) }) + delR.GET("/t/:key", func(c *gin.Context) { del(c, Text) }) log.Info().Str("Host", config.HTTPBind). Str("Port", config.HTTPPort). diff --git a/txt.go b/txt.go index 5df6225..63c1516 100644 --- a/txt.go +++ b/txt.go @@ -2,231 +2,73 @@ package main import ( "errors" - "net" "strings" - valid "github.com/asaskevich/govalidator" + "git.tcp.direct/kayos/common/squish" + termdumpster "git.tcp.direct/kayos/putxt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" - "github.com/twharmon/gouid" - "golang.org/x/crypto/blake2b" - - termbin "git.tcp.direct/kayos/putxt" "git.tcp.direct/tcp.direct/tcp.ac/config" ) -func init() { - termbin.UseChannel = true +type textValidator struct { + in []byte + out []byte } -func incoming() { - var msg termbin.Message - select { - case msg = <-termbin.Msg: - switch msg.Type { - case termbin.Error: - log.Warn(). - Str("RemoteAddr", msg.RAddr). - Int("Size", msg.Size). - Msg(msg.Content) - case termbin.IncomingData: - log.Trace(). - Str("RemoteAddr", msg.RAddr). - Int("Size", msg.Size). - Msg("termbin_data") - case termbin.Finish: - log.Debug(). - Str("RemoteAddr", msg.RAddr). - Int("Size", msg.Size). - Msg(msg.Content) - case termbin.Debug: - log.Trace(). - Str("RemoteAddr", msg.RAddr). - Int("Size", msg.Size). - Msg(msg.Content) - case termbin.Final: - log.Trace(). - Str("RemoteAddr", msg.RAddr). - Int("Size", msg.Size). - Msg(msg.Content) +type textIngestor struct{} - termPost(msg) - } - } -} - -func termPost(msg) { - slog := log.With().Str("caller", "imgPost"). - Str("User-Agent", c.GetHeader("User-Agent")). - Str("RemoteAddr", c.ClientIP()).Logger() - - Hashr, _ := blake2b.New(64, nil) - Hashr.Write(b) - hash := Hashr.Sum(nil) - if ogTxt, _ := db.With("hsh").Get(hash); ogTxt != nil { - if db.With("txt").Has(ogTxt) { - slog.Debug().Str("ogUid", string(ogTxt)).Msg("duplicate file found! returning original URL") - post := &Post{ - entryType: Text, - uid: string(ogTxt), - key: "", - priv: false, - } - termbin.Reply <- termbin.Message{Type: termbin.ReturnURL, Content: post.URLString()} - return - } - } - - // 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 uidRef, _ := db.With("txt").Get([]byte(uid)); uidRef != nil; { - slog.Info().Msg(" uid already exists! generating new...") - uid = gouid.String(config.UIDSize, gouid.MixedCaseAlphaNum) - } - for keyRef, _ := db.With("key").Get([]byte(key)); keyRef != nil; { - slog.Info().Msg(" delete key already exists! generating new...") - key = gouid.String(config.DeleteKeySize, gouid.MixedCaseAlphaNum) - } - - db.With("hsh").Put(hash, []byte(uid)) - - uid = gouid.String(config.UIDSize, gouid.MixedCaseAlphaNum) - key = gouid.String(config.DeleteKeySize, gouid.MixedCaseAlphaNum) - - for uidRef, _ := db.With("txt").Get([]byte(uid)); uidRef != nil; { - slog.Info().Msg(" uid already exists! generating new...") - uid = gouid.String(config.UIDSize, gouid.MixedCaseAlphaNum) - } - for keyRef, _ := db.With("key").Get([]byte(key)); keyRef != nil; { - slog.Info().Msg(" delete key already exists! generating new...") - key = gouid.String(config.DeleteKeySize, gouid.MixedCaseAlphaNum) - } - - db.With("hsh").Put([]byte(hash), []byte(uid)) - - err := db.With("txt").Put([]byte(uid), b) +func (i textIngestor) Ingest(data []byte) ([]byte, error) { + tp := &textValidator{in: data} + err := post(tp, tp, Text, false) if err != nil { - slog.Error().Err(err).Msg("failed to save text!") - termbin.Reply <- termbin.Message{Type: termbin.ReturnURL, Content: "internal server error"} - return + return nil, err } - err = db.With("key").Put([]byte(key), []byte("t."+uid)) - if err != nil { - slog.Error().Msg("failed to save delete key!") - termbin.Reply <- termbin.Message{Type: termbin.ReturnError, Content: "internal server error"} - return - } - - slog.Debug().Str("uid", uid).Msg("saved to database successfully, sending to NewPostResponse") - - post := &Post{ - entryType: Text, - uid: uid, - key: key, - priv: false, - } - - termbin.Reply <- termbin.Message{Type: termbin.ReturnURL, Content: post.URLString()} + return tp.out, nil } -func txtView(c *gin.Context) { - raddr := net.ParseIP(c.RemoteIP()) - if termbin.Rater.Check(&termbin.Identity{Actual: raddr}) { - errThrow(c, 429, errors.New("ratelimitted"), "too many requests") - return - } +func (i textValidator) finalize(data []byte) ([]byte, error) { + return squish.Gunzip(data) +} - sUid := strings.Split(c.Param("uid"), ".") - rUid := sUid[0] - fExt = "" +func (i textValidator) getContentType(c *gin.Context) (string, error) { + return "text/plain", nil +} - if len(sUid) > 1 { - fExt = strings.ToLower(sUid[1]) +func (i textValidator) checkURL(c *gin.Context) error { + sUID := strings.Split(c.Param("uid"), ".") + var fExt string + if len(sUID) > 1 { + fExt = strings.ToLower(sUID[1]) + log.Trace().Str("caller", c.Request.RequestURI).Str("ext", fExt).Msg("detected file extension") if fExt != "txt" { - errThrow(c, 400, errors.New("bad file extension"), "400") - return + return errors.New("bad file extension") } + c.Set("url.extension", fExt) } - - // if it doesn't match the key size or it isn't alphanumeric - throw it out - if !valid.IsAlphanumeric(rUid) || len(rUid) != config.UIDSize { - errThrow(c, 400, errors.New("request discarded as invalid"), "400") - return - } - - // query bitcask for the id - fBytes, _ := db.With("txt").Get([]byte(rUid)) - if fBytes == nil { - errThrow(c, 404, errors.New("file not found"), "file not found") - return - } - - file, err := termbin.Deflate(fBytes) - if err != nil { - errThrow(c, 500, err, "internal server error") - } - c.Data(200, "text/plain", file) - + return nil } -func txtDel(c *gin.Context) { - slog := log.With(). - Str("caller", "txtDel").Logger() - slog.Debug().Msg("new_request") - if !validateKey(c.Param("key")) { - errThrow(c, 400, errors.New("bad key"), "400") - return - } - - rKey := c.Param("key") - - targetTxt, _ := db.With("key").Get([]byte(rKey)) - if targetTxt == nil || !strings.Contains(string(targetTxt), "t.") { - errThrow(c, 400, errors.New("no txt delete entry found with provided key"), "400") - return - } - - t := strings.Split(string(targetTxt), ".")[1] - - if !db.With("txt").Has([]byte(t)) { - - errThrow(c, 500, errors.New("image not found in database"), "500") // this shouldn't happen...? - return - } - if err := db.With("txt").Delete([]byte(t)); err != nil { - errThrow(c, 500, errors.New("delete failed"), "500") - return - } - - if db.With("txt").Has([]byte(t)) { - slog.Error().Str("rkey", t).Msg("delete failed!?") - errThrow(c, 500, errors.New("delete failed, this shouldn't happen"), "500") - return - } - - slog.Info().Str("rkey", t).Msg("Text file deleted successfully") - slog.Debug().Str("rkey", t).Msg("Removing delete key entry") - err := db.With("key").Delete([]byte(rKey)) - if err != nil { - slog.Error().Str("rkey", t).Msg("Couldn't delete key") - } - c.JSON(200, "DELETE_SUCCESS") +func (i textValidator) checkContent(c *gin.Context, data []byte) error { + return nil } -func serveTermbin() { - go func() { - for { - incoming() - } - }() +func (i textValidator) checkAndScrubPost(c any) ([]byte, error) { + if i.in == nil { + return nil, errors.New("no data") + } + return i.in, nil +} + +func serveTermbin() error { + td := termdumpster.NewTermDumpster(textIngestor{}).WithGzip().WithLogger(&log.Logger). + WithMaxSize(int64(config.KVMaxValueSizeMB * 1024 * 1024)) split := strings.Split(config.TermbinListen, ":") - err := termbin.Listen(split[0], split[1]) + log.Info().Str("listen", config.TermbinListen).Msg("starting termbin") + err := td.Listen(split[0], split[1]) if err != nil { - println(err.Error()) - return + return err } + return nil } diff --git a/util.go b/util.go index 5101e75..6450269 100644 --- a/util.go +++ b/util.go @@ -2,26 +2,26 @@ package main import ( "fmt" - "image" "io" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) -func errThrow(c *gin.Context, respcode int, thrown error, msg string) error { +func errThrow(c *gin.Context, respcode int, thrown error, msg []byte) error { log.Error(). Str("IP", c.ClientIP()). Str("User-Agent", c.GetHeader("User-Agent")). - Err(thrown).Msg(msg) - c.String(respcode, msg) + Err(thrown).Msg(string(msg)) + c.Data(respcode, "application/json", msg) var err error if thrown != nil { - err = fmt.Errorf("%s: %s", msg, thrown) + err = fmt.Errorf("%s: %w", msg, thrown) } return err } +// TODO: do we need this? func getSize(s io.Seeker) (size int64, err error) { // get size of file if _, err = s.Seek(0, 0); err != nil { @@ -35,12 +35,28 @@ func getSize(s io.Seeker) (size int64, err error) { return } -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) +func getOldRef(p *Post) (*Post, error, bool) { + var oldRef []byte + oldRef, err := db.With("hsh").Get(p.Sum()) if err != nil { - return + return nil, err, false } - _, err = r.Seek(0, 0) - return + p.Log().Trace().Caller().Msg("duplicate checksum in hash database, checking if file still exists...") + if db.With(p.TypeCode(true)).Has(oldRef) { + p.Log().Debug().Str("ogUid", string(oldRef)). + Msg("duplicate file found! returning original URL") + p.uid = string(oldRef) + p.key = "" + p.priv = false + return p, nil, true + } + p.Log().Trace(). + Str("ogUid", string(oldRef)). + Msg("stale hash found, deleting entry...") + err = db.With("hsh").Delete(p.Sum()) + if err != nil { + p.Log().Error().Err(err).Msg("failed to delete stale hash") + p = nil + } + return p, err, false } diff --git a/view.go b/view.go new file mode 100644 index 0000000..2dc0d53 --- /dev/null +++ b/view.go @@ -0,0 +1,62 @@ +package main + +import ( + "errors" + "net/http" + "strings" + + "github.com/asaskevich/govalidator" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + + "git.tcp.direct/tcp.direct/tcp.ac/config" +) + +func view(c *gin.Context, validate validator, t EntryType) { + slog := log.With().Str("caller", "view").Logger() + if err := validate.checkURL(c); err != nil { + errThrow(c, 400, err, message400) + return + } + sUID := strings.Split(c.Param("uid"), ".") + rUID := sUID[0] + // if it doesn't match the key size or it isn't alphanumeric - throw it out + if !govalidator.IsAlphanumeric(rUID) || len(rUID) != config.UIDSize { + slog.Warn(). + Str("remoteaddr", c.ClientIP()). + Msg("request discarded as invalid") + errThrow(c, 400, errors.New(string(message400)), message400) + return + } + slog.Trace().Str("rUid", rUID).Msg("request validated") + // query bitcask for the id + fBytes, err := db.With(typeToString(t, true)).Get([]byte(rUID)) + if fBytes == nil || err != nil { + slog.Error().Str("rUid", rUID).Msg("no corresponding file for this id") + realErr := err + if err == nil { + realErr = errors.New("no corresponding file for this id") + } + errThrow(c, 404, realErr, message404) + return + } + err = validate.checkContent(c, fBytes) + if err != nil { + errThrow(c, http.StatusBadRequest, err, message400) + return + } + var contentType string + contentType, err = validate.getContentType(c) + if err != nil { + errThrow(c, http.StatusBadRequest, err, message400) + return + } + + fBytes, err = validate.finalize(fBytes) + if err != nil { + errThrow(c, http.StatusInternalServerError, err, message500) + return + } + + c.Data(200, contentType, fBytes) +}