v1: Stabilize overhaul

This commit is contained in:
kayos@tcp.direct 2022-07-18 03:41:29 -07:00
parent 25b8e048fb
commit c3cd9e9f04
Signed by: kayos
GPG Key ID: 4B841471B4BEE979
13 changed files with 497 additions and 525 deletions

View File

@ -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
}

View File

@ -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, "/") {

View File

@ -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),
},
}
}

91
delete.go Normal file
View File

@ -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")
}

9
go.mod
View File

@ -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

10
go.sum
View File

@ -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=

304
img.go
View File

@ -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
}

View File

@ -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())
}

133
post.go Normal file
View File

@ -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
}

View File

@ -15,12 +15,12 @@ import (
"git.tcp.direct/tcp.direct/tcp.ac/config"
)
var favicon string = "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"
var staticIndex string = "PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KCTxtZXRhIGNoYXJzZXQ9IlVURi04Ij4KCTxtZXRhIG5hbWU9ImRlc2NyaXB0aW9uIiBjb250ZW50PSJ0Y3AuYWMgLSB1bmRlciBjb25zdHJ1Y3Rpb24gLSBpbWFnZSB1cGxvYWRpbmcsIHVybCBzaG9ydGVuaW5nLCB0ZXh0IGJpbiI+Cgk8bWV0YSBuYW1lPSJhdXRob3IiIGNvbnRlbnQ9InRjcC5kaXJlY3QiPgoJPHRpdGxlPnRjcC5hYzwvdGl0bGU+CgoJPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCQlib2R5IHsKCQkJYmFja2dyb3VuZC1jb2xvcjojMTAxMDEwOwoJCQljb2xvcjojOTQ4REI4OwoJCQlmb250LWZhbWlseTptb25vc3BhY2U7CgkJCXRleHQtYWxpZ246Y2VudGVyOwoJCX0KCQkuaGVsbG8gewoJCQlwb3NpdGlvbjogZml4ZWQ7CgkJCXRvcDogNTAlOwoJCQlsZWZ0OiA1MCU7CgkJCXRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpOwoJCX0KCQkuZmFkaW5nIHsKCQkJYW5pbWF0aW9uOmZhZGluZyA1cyBpbmZpbml0ZQoJCX0KCQlAa2V5ZnJhbWVzIGZhZGluZ3sKCQkJMCV7b3BhY2l0eTowfQoJCQk1MCV7b3BhY2l0eToxfQoJCQkxMDAle29wYWNpdHk6MH0KCQl9Cgk8L3N0eWxlPgo8L2hlYWQ+Cjxib2R5Pgo8ZGl2IGNsYXNzPSJoZWxsbyI+CjxwcmUgY2xhc3M9ImZhZGluZyI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAsZCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgODggICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIApNTTg4TU1NICxhZFBQWWJhLCA4YixkUFBZYmEsICAgICAgLGFkUFBZWWJhLCAgLGFkUFBZYmEsICAKICA4OCAgIGE4IiAgICAgIiIgODhQJyAgICAiOGEgICAgICIiICAgICBgWTggYTgiICAgICAiIiAgCiAgODggICA4YiAgICAgICAgIDg4ICAgICAgIGQ4ICAgICAsYWRQUFBQUDg4IDhiICAgICAgICAgIAogIDg4LCAgIjhhLCAgICxhYSA4OGIsICAgLGE4IiA4ODggODgsICAgICw4OCAiOGEsICAgLGFhICAKICAiWTg4OCBgIlliYmQ4IicgODhgWWJiZFAiJyAgODg4IGAiOGJiZFAiWTggIGAiWWJiZDgiJyAgCiAgICAgICAgICAgICAgICAgIDg4ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICA4OCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKCjwvcHJlPgo8L2Rpdj4KCjxhIGhyZWY9Imh0dHA6Ly9hZG1pbi50Y3AuYWMvIiBzdHlsZT0iZGlzcGxheTpub25lOyI+YWRtaW4gbG9naW48L2E+Cgo8L2JvZHk+Cgo8L2h0bWw+Cg=="
const favicon = "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"
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).

242
txt.go
View File

@ -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
}

38
util.go
View File

@ -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
}

62
view.go Normal file
View File

@ -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)
}