1
0
forked from tcp.direct/tcp.ac

implement termbin functionality

This commit is contained in:
kayos 2021-07-28 00:31:34 -07:00
parent d79575721a
commit 022073c9a7
9 changed files with 311 additions and 302 deletions

@ -2,12 +2,42 @@ package main
import ( import (
"fmt" "fmt"
"github.com/prologic/bitcask"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
"os"
"strconv" "strconv"
) )
////////////// global declarations
var (
// datastores
imgDB *bitcask.Bitcask
hashDB *bitcask.Bitcask
keyDB *bitcask.Bitcask
urlDB *bitcask.Bitcask
txtDB *bitcask.Bitcask
// config directives
debugBool bool
baseUrl string
webPort string
webIP string
dbDir string
logDir string
uidSize int
keySize int
txtPort string
// utilitarian globals
err error
fn string
s string
i int
f *os.File
)
func configRead() { func configRead() {
// name of the file // name of the file
// and the extension // and the extension
@ -34,34 +64,17 @@ func configRead() {
zerolog.SetGlobalLevel(zerolog.InfoLevel) zerolog.SetGlobalLevel(zerolog.InfoLevel)
} }
// base URL of site baseUrl = viper.GetString("http.baseurl")
s = "http.baseurl"
baseUrl = viper.GetString(s)
// bind port i := viper.GetInt("http.port")
s = "http.port" webPort = strconv.Itoa(i)
i := viper.GetInt(s)
webPort = strconv.Itoa(i) // int looks cleaner in config
// bind IP webIP = viper.GetString("http.bindip")
s = "http.bindip" dbDir = viper.GetString("files.data")
webIP = viper.GetString(s) logDir = viper.GetString("files.logs")
uidSize = viper.GetInt("global.uidsize")
// database location (main storage) keySize = viper.GetInt("global.delkeysize")
s = "files.data" txtPort = viper.GetString("txt.port")
dbDir = viper.GetString(s)
// logfile location
s = "files.logs"
logDir = viper.GetString(s)
// character count of unique IDs for posts
s = "img.uidsize"
uidSize = viper.GetInt(s)
// size of generated unique delete keys
s = "img.delkeysize"
keySize = viper.GetInt(s)
log.Debug().Str("baseUrl", baseUrl).Str("webIP", webIP).Str("webPort", webPort).Msg("Web") log.Debug().Str("baseUrl", baseUrl).Str("webIP", webIP).Str("webPort", webPort).Msg("Web")
log.Debug().Str("logDir", logDir).Str("dbDir", dbDir).Msg("Filesystem") log.Debug().Str("logDir", logDir).Str("dbDir", dbDir).Msg("Filesystem")

@ -1,16 +1,19 @@
title = "tcp.ac config" title = "tcp.ac config"
[global] [global]
debug = false debug = true
uidsize = 5
delkeysize = 12
[http] [http]
baseurl = "http://192.168.69.1/" baseurl = "http://127.0.0.1:8080/"
bindip = "192.168.69.1" basehost = "127.0.0.1"
port = 80 bindip = "127.0.0.1"
port = 8080
[img] [txt]
uidsize = 4 # string: port to listen on for termbin clone
delkeysize = 12 port = "9999"
[files] [files]
data = "/etc/tcpac/data/" data = "/etc/tcpac/data/"

@ -1,29 +0,0 @@
package main
import "github.com/prologic/bitcask"
import "os"
////////////// global declarations
// datastores
var imgDB *bitcask.Bitcask
var hashDB *bitcask.Bitcask
var keyDB *bitcask.Bitcask
var urlDB *bitcask.Bitcask
var txtDB *bitcask.Bitcask
// config directives
var debugBool bool
var baseUrl string
var webPort string
var webIP string
var dbDir string
var logDir string
var uidSize int
var keySize int
// utilitarian globals
var err error
var fn string
var s string
var i int
var f *os.File

112
img.go

@ -18,46 +18,21 @@ import (
var fExt string var fExt string
type Post struct {
Type string
Uid string
Key string
Priv bool
}
func (p Post) Serve(c *gin.Context) {
url := baseUrl + p.Type + "/" + string(p.Uid)
priv := "no"
keyurl := ""
if p.Key != "" {
keyurl = baseUrl + "d/" + p.Type + "/" + p.Key
}
if p.Priv == true {
priv = "yes"
}
log.Info().Str("type", p.Type).Str("uid", p.Uid).Str("key", p.Key).Str("private", priv).Msg("success")
c.JSON(201, gin.H{"Imgurl": url, "ToDelete": keyurl})
return
}
func imgDel(c *gin.Context) { func imgDel(c *gin.Context) {
fn = "imgDel" slog := log.With().Str("caller", "imgView").Logger()
log.Debug().Str("func", fn).Msg("Request received!") // received request slog.Debug().Msg("Request received!") // received request
rKey := c.Param("key") rKey := c.Param("key")
// if it doesn't match the key size or it isn't alphanumeric - throw it out if !validateKey(rKey) {
if len(rKey) != keySize || !valid.IsAlphanumeric(rKey) { slog.Error().Msg("delete request failed sanity check!")
log.Error().Str("func", fn).Msg("delete request failed sanity check!")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
targetImg, _ := keyDB.Get([]byte(rKey)) targetImg, _ := keyDB.Get([]byte(rKey))
if targetImg == nil || !strings.Contains(string(targetImg), "i.") { if targetImg == nil || !strings.Contains(string(targetImg), "i.") {
log.Error().Str("func", fn).Str("rkey", rKey).Msg("no img delete entry found with provided key") slog.Error().Str("rkey", rKey).Msg("no img delete entry found with provided key")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
@ -65,28 +40,28 @@ func imgDel(c *gin.Context) {
finalTarget := strings.Split(string(targetImg), ".") finalTarget := strings.Split(string(targetImg), ".")
if !imgDB.Has([]byte(finalTarget[1])) { if !imgDB.Has([]byte(finalTarget[1])) {
log.Error().Str("func", fn).Str("rkey", rKey).Msg("corresponding image not found in database!") slog.Error().Str("rkey", rKey).Msg("corresponding image not found in database!")
errThrow(c, 500, "500", "500") // this shouldn't happen...? errThrow(c, 500, "500", "500") // this shouldn't happen...?
return return
} }
err := imgDB.Delete([]byte(finalTarget[1])) err := imgDB.Delete([]byte(finalTarget[1]))
if err != nil { if err != nil {
log.Error().Str("func", fn).Str("rkey", finalTarget[1]).Msg("delete failed!") slog.Error().Str("rkey", finalTarget[1]).Msg("delete failed!")
errThrow(c, 500, "500", "500") errThrow(c, 500, "500", "500")
return return
} }
if imgDB.Has([]byte(finalTarget[1])) { if imgDB.Has([]byte(finalTarget[1])) {
log.Error().Str("func", fn).Str("rkey", finalTarget[1]).Msg("delete failed!?") slog.Error().Str("rkey", finalTarget[1]).Msg("delete failed!?")
errThrow(c, 500, "500", "500") errThrow(c, 500, "500", "500")
return return
} }
log.Info().Str("func", fn).Str("rkey", finalTarget[1]).Msg("Image file deleted successfully") slog.Info().Str("rkey", finalTarget[1]).Msg("Image file deleted successfully")
log.Debug().Str("func", fn).Str("rkey", finalTarget[1]).Msg("Removing delete key entry") slog.Debug().Str("rkey", finalTarget[1]).Msg("Removing delete key entry")
err = keyDB.Delete([]byte(rKey)) err = keyDB.Delete([]byte(rKey))
if err != nil { if err != nil {
log.Error().Str("func", fn).Str("rkey", finalTarget[1]).Msg("Couldn't delete key") slog.Error().Str("rkey", finalTarget[1]).Msg("Couldn't delete key")
// it would be insane to try and delete the hash here // it would be insane to try and delete the hash here
} // if someone is uploading this image again after del } // if someone is uploading this image again after del
c.JSON(200, "DELETE_SUCCESS") // and the file corresponding to the hash no longer exists c.JSON(200, "DELETE_SUCCESS") // and the file corresponding to the hash no longer exists
@ -94,18 +69,18 @@ func imgDel(c *gin.Context) {
} }
func imgView(c *gin.Context) { func imgView(c *gin.Context) {
fn = "imgView" slog := log.With().Str("caller", "imgView").Logger()
// the user can access their image with or without a file extension in URI // the user can access their image with or without a file extension in URI
log.Debug().Str("func", fn).Msg("request received") // however it must be a valid extension (more checks further down) slog.Debug().Msg("request received") // however it must be a valid extension (more checks further down)
sUid := strings.Split(c.Param("uid"), ".") sUid := strings.Split(c.Param("uid"), ".")
rUid := sUid[0] rUid := sUid[0]
if len(sUid) > 1 { if len(sUid) > 1 {
fExt = strings.ToLower(sUid[1]) fExt = strings.ToLower(sUid[1])
log.Debug().Str("func", fn).Str("ext", fExt).Msg("detected file extension") slog.Debug().Str("ext", fExt).Msg("detected file extension")
if fExt != "png" && fExt != "jpg" && fExt != "jpeg" && fExt != "gif" { if fExt != "png" && fExt != "jpg" && fExt != "jpeg" && fExt != "gif" {
log.Error().Str("func", fn).Msg("Bad file extension!") slog.Error().Msg("Bad file extension!")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
@ -115,18 +90,18 @@ func imgView(c *gin.Context) {
// if it doesn't match the key size or it isn't alphanumeric - throw it out // if it doesn't match the key size or it isn't alphanumeric - throw it out
if !valid.IsAlphanumeric(rUid) || len(rUid) != uidSize { if !valid.IsAlphanumeric(rUid) || len(rUid) != uidSize {
log.Error().Str("func", fn).Msg("request discarded as invalid") // these limits should be variables eventually slog.Error().Msg("request discarded as invalid") // these limits should be variables eventually
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
// now that we think its a valid request we will query // now that we think its a valid request we will query
log.Debug().Str("func", fn).Str("rUid", rUid).Msg("request validated") slog.Debug().Str("rUid", rUid).Msg("request validated")
// query bitcask for the id // query bitcask for the id
fBytes, _ := imgDB.Get([]byte(rUid)) fBytes, _ := imgDB.Get([]byte(rUid))
if fBytes == nil { if fBytes == nil {
log.Error().Str("func", fn).Str("rUid", rUid).Msg("no corresponding file for this id") slog.Error().Str("rUid", rUid).Msg("no corresponding file for this id")
errThrow(c, 404, "404", "File not found") errThrow(c, 404, "404", "File not found")
return return
} }
@ -137,16 +112,16 @@ func imgView(c *gin.Context) {
if !ok { if !ok {
// extra sanity check to make sure we don't serve non-image data that somehow got into the database // extra sanity check to make sure we don't serve non-image data that somehow got into the database
errThrow(c, http.StatusBadRequest, "400", "400") errThrow(c, http.StatusBadRequest, "400", "400")
log.Error().Str("func", fn).Str("rUid", rUid).Msg("the file we grabbed is not an image!?") slog.Error().Str("rUid", rUid).Msg("the file we grabbed is not an image!?")
return return
} else { } else {
// seems like its an image, we will proceed // seems like its an image, we will proceed
log.Debug().Str("func", fn).Str("rUid", rUid).Str("imageFormat", imageFormat).Msg("Image format detected") slog.Debug().Str("rUid", rUid).Str("imageFormat", imageFormat).Msg("Image format detected")
} }
// additional extension sanity check - if they're gonna use an extension it needs to be the right one // additional extension sanity check - if they're gonna use an extension it needs to be the right one
if fExt != "nil" && fExt != imageFormat { if fExt != "nil" && fExt != imageFormat {
log.Error().Str("func", fn).Str("rUid", rUid).Msg("requested file extension does not match filetype") slog.Error().Str("rUid", rUid).Msg("requested file extension does not match filetype")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
@ -156,14 +131,16 @@ func imgView(c *gin.Context) {
contentType := "image/" + imageFormat contentType := "image/" + imageFormat
c.Data(200, contentType, fBytes) c.Data(200, contentType, fBytes)
log.Info().Str("func", fn).Str("rUid", rUid).Msg("Successful upload") slog.Info().Str("rUid", rUid).Msg("Successful upload")
} }
func imgPost(c *gin.Context) { func imgPost(c *gin.Context) {
fn = "imgPost" slog := log.With().Str("caller", "imgPost").Logger()
var Scrubbed []byte var (
var priv bool = false Scrubbed []byte
var key string = "" priv bool = false
key string = ""
)
// check if incoming POST data is invalid // check if incoming POST data is invalid
f, err := c.FormFile("upload") f, err := c.FormFile("upload")
@ -171,7 +148,7 @@ func imgPost(c *gin.Context) {
errThrow(c, http.StatusBadRequest, err.Error(), "400") errThrow(c, http.StatusBadRequest, err.Error(), "400")
} }
log.Debug().Str("func", fn).Str("filename", f.Filename).Msg("[+] New upload") slog.Debug().Str("filename", f.Filename).Msg("[+] New upload")
// read the incoming file into an io file reader // read the incoming file into an io file reader
file, err := f.Open() file, err := f.Open()
@ -180,38 +157,38 @@ func imgPost(c *gin.Context) {
} }
// verify the incoming file is an image // verify the incoming file is an image
log.Debug().Str("func", fn).Msg("verifying file is an image") slog.Debug().Msg("verifying file is an image")
imageFormat, ok := checkImage(file) imageFormat, ok := checkImage(file)
if !ok { if !ok {
errThrow(c, http.StatusBadRequest, "400", "input does not appear to be an image") errThrow(c, http.StatusBadRequest, "400", "input does not appear to be an image")
return return
} else { } else {
log.Debug().Str("func", fn).Msg("image file type detected") slog.Debug().Msg("image file type detected")
} }
// dump this into a byte object and scrub it // dump this into a byte object and scrub it
// TO-DO: Write our own function for scrubbing exif // TO-DO: Write our own function for scrubbing exif
log.Debug().Str("func", fn).Msg("dumping byte form of file") slog.Debug().Msg("dumping byte form of file")
fbytes, err := ioutil.ReadAll(file) fbytes, err := ioutil.ReadAll(file)
if imageFormat != "gif" { if imageFormat != "gif" {
log.Debug().Str("func", fn).Err(err).Msg("error scrubbing exif") slog.Debug().Err(err).Msg("error scrubbing exif")
Scrubbed, err = exifremove.Remove(fbytes) Scrubbed, err = exifremove.Remove(fbytes)
if err != nil { if err != nil {
errThrow(c, http.StatusInternalServerError, err.Error(), "error scrubbing exif") errThrow(c, http.StatusInternalServerError, err.Error(), "error scrubbing exif")
return return
} }
} else { } else {
log.Debug().Str("func", fn).Msg("skipping exif scrub for gif image") slog.Debug().Msg("skipping exif scrub for gif image")
Scrubbed = fbytes Scrubbed = fbytes
} }
// time to start checking for duplicates // time to start checking for duplicates
log.Debug().Str("func", fn).Msg("calculating blake2b checksum") slog.Debug().Msg("calculating blake2b checksum")
Hashr, _ := blake2b.New(64, nil) Hashr, _ := blake2b.New(64, nil)
Hashr.Write(Scrubbed) Hashr.Write(Scrubbed)
hash := Hashr.Sum(nil) hash := Hashr.Sum(nil)
log.Debug().Str("func", fn).Msg("checking for duplicate files") slog.Debug().Msg("checking for duplicate files")
// the keys (stored in memory) for hashDb are hashes // the keys (stored in memory) for hashDb are hashes
// making it quick to find duplicates. the value is the uid // making it quick to find duplicates. the value is the uid
@ -220,9 +197,9 @@ func imgPost(c *gin.Context) {
// if we get a hit we redirect them to the original image file with no delete key // if we get a hit we redirect them to the original image file with no delete key
if imgRef != nil { if imgRef != nil {
log.Debug().Str("func", fn).Str("ogUid", ogUid).Msg("duplicate checksum in hash database, checking if file still exists...") slog.Debug().Str("ogUid", ogUid).Msg("duplicate checksum in hash database, checking if file still exists...")
if imgDB.Has(imgRef) { if imgDB.Has(imgRef) {
log.Debug().Str("func", fn).Str("ogUid", ogUid).Msg("duplicate file found! returning original URL") slog.Debug().Str("ogUid", ogUid).Msg("duplicate file found! returning original URL")
post := &Post{ post := &Post{
Type: "i", Type: "i",
@ -235,12 +212,12 @@ func imgPost(c *gin.Context) {
return return
} else { } else {
log.Debug().Str("func", fn).Str("ogUid", ogUid).Msg("stale hash found, deleting entry...") slog.Debug().Str("ogUid", ogUid).Msg("stale hash found, deleting entry...")
hashDB.Delete(hash) hashDB.Delete(hash)
} }
} }
log.Info().Str("func", fn).Msg("no duplicate images found, generating uid and delete key") slog.Info().Msg("no duplicate images found, generating uid and delete key")
// generate new uid and delete key // generate new uid and delete key
uid := gouid.String(uidSize, gouid.MixedCaseAlphaNum) uid := gouid.String(uidSize, gouid.MixedCaseAlphaNum)
@ -248,11 +225,11 @@ func imgPost(c *gin.Context) {
// lets make sure that we don't clash even though its highly unlikely // lets make sure that we don't clash even though its highly unlikely
for uidRef, _ := imgDB.Get([]byte(uid)); uidRef != nil; { for uidRef, _ := imgDB.Get([]byte(uid)); uidRef != nil; {
log.Info().Str("func", fn).Msg(" uid already exists! generating new...") slog.Info().Msg(" uid already exists! generating new...")
uid = gouid.String(uidSize, gouid.MixedCaseAlphaNum) uid = gouid.String(uidSize, gouid.MixedCaseAlphaNum)
} }
for keyRef, _ := keyDB.Get([]byte(key)); keyRef != nil; { for keyRef, _ := keyDB.Get([]byte(key)); keyRef != nil; {
log.Info().Str("func", fn).Msg(" delete key already exists! generating new...") slog.Info().Msg(" delete key already exists! generating new...")
key = gouid.String(keySize, gouid.MixedCaseAlphaNum) key = gouid.String(keySize, gouid.MixedCaseAlphaNum)
} }
@ -260,7 +237,7 @@ func imgPost(c *gin.Context) {
hashDB.Put([]byte(hash), []byte(uid)) hashDB.Put([]byte(hash), []byte(uid))
// insert actual file to database // insert actual file to database
log.Debug().Str("func", fn).Str("uid", uid).Msg("saving file to database") slog.Debug().Str("uid", uid).Msg("saving file to database")
err = imgDB.Put([]byte(uid), []byte(Scrubbed)) err = imgDB.Put([]byte(uid), []byte(Scrubbed))
if err != nil { if err != nil {
errThrow(c, 500, err.Error(), "upload failed") errThrow(c, 500, err.Error(), "upload failed")
@ -276,7 +253,7 @@ func imgPost(c *gin.Context) {
} }
// good to go, send them to the finisher function // good to go, send them to the finisher function
log.Debug().Str("func", fn).Str("uid", uid).Msg("saved to database successfully, sending to Serve") slog.Debug().Str("uid", uid).Msg("saved to database successfully, sending to Serve")
post := &Post{ post := &Post{
Type: "i", Type: "i",
@ -285,6 +262,7 @@ func imgPost(c *gin.Context) {
Priv: priv, Priv: priv,
} }
post.Serve(c) post.Serve(c)
} }

@ -62,6 +62,7 @@ func init() {
} }
func main() { func main() {
go serveTermbin()
// see router.go // see router.go
httpRouter() httpRouter()
} }

@ -3,7 +3,7 @@ package main
import ( import (
"encoding/base64" "encoding/base64"
"github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
"github.com/gin-contrib/logger" // "github.com/gin-contrib/logger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -41,7 +41,7 @@ func httpRouter() {
router := gin.New() router := gin.New()
router.Use(logger.SetLogger()) //router.Use(logger.SetLogger())
router.MaxMultipartMemory = 16 << 20 // crude POST limit (fix this) router.MaxMultipartMemory = 16 << 20 // crude POST limit (fix this)
@ -57,26 +57,33 @@ func httpRouter() {
imgR := router.Group("/i") imgR := router.Group("/i")
{ {
imgR.GET("/", func(c *gin.Context) { c.String(200, "") }) imgR.GET("/", placeHolder)
// put looks nicer even though its actually POST // put looks nicer even though its actually POST
imgR.POST("/put", imgPost) imgR.POST("/put", imgPost)
imgR.GET("/:uid", imgView) imgR.GET("/:uid", imgView)
} }
txtR := router.Group("/t")
{
txtR.GET("/", placeHolder)
txtR.GET("/:uid", txtView)
}
delR := router.Group("/d") delR := router.Group("/d")
{ {
delR.GET("/i/:key", imgDel) delR.GET("/i/:key", imgDel)
delR.GET("/t/:key", txtDel)
} }
// txtR := router.Group("/t") // txtR := router.Group("/t")
// { // {
// txtR.POST("/put", txtPost) // txtR.POST("/put", txtPost)
// } // }
// urlR := router.Group("/u") // urlR := router.Group("/u")
// { // {
// urlR.POST("/put", urlPost) // urlR.POST("/put", urlPost)
// } // }
log.Info().Str("webIP", webIP).Str("webPort", webPort).Msg("done; tcp.ac is live.") log.Info().Str("webIP", webIP).Str("webPort", webPort).Msg("done; tcp.ac is live.")
router.Run(webIP + ":" + webPort) router.Run(webIP + ":" + webPort)

@ -10,6 +10,8 @@ import (
var ( var (
// Msg is a channel for status information during concurrent server operations // Msg is a channel for status information during concurrent server operations
Msg chan Message Msg chan Message
// Reply is a channel to receive messages to send our client upon completion
Reply chan Message
// Timeout is the read timeout for incoming data in seconds // Timeout is the read timeout for incoming data in seconds
Timeout int = 4 Timeout int = 4
// Max size for incoming data (default: 3 << 30 or 3 MiB) // Max size for incoming data (default: 3 << 30 or 3 MiB)
@ -31,6 +33,7 @@ type Message struct {
func init() { func init() {
Msg = make(chan Message) Msg = make(chan Message)
Reply = make(chan Message)
} }
func termStatus(m Message) { func termStatus(m Message) {
@ -113,6 +116,13 @@ func serve(c net.Conn) {
} }
termStatus(Message{Type: "FINAL", RAddr: c.RemoteAddr().String(), Size: len(final), Bytes: final, Content: "SUCCESS"}) termStatus(Message{Type: "FINAL", RAddr: c.RemoteAddr().String(), Size: len(final), Bytes: final, Content: "SUCCESS"})
url := <- Reply
switch url.Type {
case "URL":
c.Write([]byte(url.Content))
case "ERROR":
c.Write([]byte("ERROR: " + url.Content))
}
c.Close() c.Close()
} }

@ -10,11 +10,11 @@ var (
// Users contains all remote addresses currently being ratelimited // Users contains all remote addresses currently being ratelimited
Users *cache.Cache Users *cache.Cache
// Rate is the amount of seconds between each post from an IP address // Rate is the amount of seconds between each post from an IP address
Rate time.Duration = 30 Rate time.Duration = 8
) )
func init() { func init() {
Users = cache.New(Rate*time.Second, 30*time.Second) Users = cache.New(Rate*time.Second, 10*time.Second)
} }
func isThrottled(addr string) bool { func isThrottled(addr string) bool {

346
txt.go

@ -3,226 +3,252 @@ package main
import ( import (
valid "github.com/asaskevich/govalidator" valid "github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
cache "github.com/patrickmn/go-cache"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/twharmon/gouid" "github.com/twharmon/gouid"
"golang.org/x/crypto/blake2b" "golang.org/x/crypto/blake2b"
"net/http"
"strings" "strings"
"tcp.ac/termbin"
"time"
) )
var hash []byte var (
var uid string Users *cache.Cache
var key string )
func txtFin(c *gin.Context, id string, key string) { func init() {
txturl := baseUrl + "t/" + string(id) termbin.UseChannel = true
keyurl := "duplicate" Users = cache.New(termbin.Rate*time.Second, 30*time.Second)
if key != "nil" {
keyurl = baseUrl + "d/t/" + string(key)
}
log.Info().Str("func", "txtPost").Str("id", id).Str("status", "201").Str("txturl", txturl).Str("keyurl", keyurl).Msg("success")
c.JSON(201, gin.H{"delkey": keyurl, "txturl": txturl})
return
} }
func txtDel(c *gin.Context) { func txtThrottled(addr string) bool {
fn = "txtDel" if _, ok := Users.Get(addr); !ok {
Users.Set(addr, 1, 0)
return false
} else {
return true
}
}
// make sure its the proper amount of characters and is alphanumeric func incoming() {
log.Info().Str("func", fn).Msg("delete request received") var msg termbin.Message
rKey := c.Param("key")
if len(rKey) != keySize || !valid.IsAlphanumeric(rKey) { select {
log.Error().Err(err).Str("func", fn).Msg("delete request failed sanity check!") case msg = <-termbin.Msg:
errThrow(c, 400, "400", "400") switch msg.Type {
return case "ERROR":
log.Error().
Str("RemoteAddr", msg.RAddr).
Int("Size", msg.Size).
Msg(msg.Content)
case "INCOMING_DATA":
log.Debug().
Str("RemoteAddr", msg.RAddr).
Int("Size", msg.Size).
Msg("termbin_data")
case "FINISH":
log.Debug().
Str("RemoteAddr", msg.RAddr).
Int("Size", msg.Size).
Msg(msg.Content)
case "DEBUG":
log.Debug().
Str("RemoteAddr", msg.RAddr).
Int("Size", msg.Size).
Msg(msg.Content)
case "FINAL":
log.Info().
Str("RemoteAddr", msg.RAddr).
Int("Size", msg.Size).
Msg(msg.Content)
termPost(msg.Bytes)
}
}
}
func termPost(b []byte) {
slog := log.With().Str("caller", "termPost").Logger()
Hashr, _ := blake2b.New(64, nil)
Hashr.Write(b)
hash := Hashr.Sum(nil)
if ogTxt, _ := hashDB.Get(hash); ogTxt != nil {
if txtDB.Has(ogTxt) {
slog.Debug().Str("ogUid", string(ogTxt)).Msg("duplicate file found! returning original URL")
post := &Post{
Type: "t",
Uid: string(ogTxt),
Key: "",
Priv: false,
}
termbin.Reply <- termbin.Message{Type: "URL", Content: post.URLString()}
return
}
} }
// first we see if the key even exists, and if it does, is it a txt key (vs txt or url) // generate new uid and delete key
targettxt, _ := keyDB.Get([]byte(rKey)) uid := gouid.String(uidSize, gouid.MixedCaseAlphaNum)
if targettxt == nil || !strings.Contains(string(targettxt), "t.") { key := gouid.String(keySize, gouid.MixedCaseAlphaNum)
log.Error().Err(err).Str("func", fn).Str("rkey", rKey).Msg("no txt delete entry found with provided key")
errThrow(c, 400, "400", "400") // lets make sure that we don't clash even though its highly unlikely
return for uidRef, _ := txtDB.Get([]byte(uid)); uidRef != nil; {
slog.Info().Msg(" uid already exists! generating new...")
uid = gouid.String(uidSize, gouid.MixedCaseAlphaNum)
}
for keyRef, _ := keyDB.Get([]byte(key)); keyRef != nil; {
slog.Info().Msg(" delete key already exists! generating new...")
key = gouid.String(keySize, gouid.MixedCaseAlphaNum)
} }
// seperate the key from the indicator to get the actual txt delete key hashDB.Put([]byte(hash), []byte(uid))
finalTarget := strings.Split(string(targettxt), ".")
// somehow we have a key that doesn't correspond with with an actual entry uid = gouid.String(uidSize, gouid.MixedCaseAlphaNum)
if !txtDB.Has([]byte(finalTarget[1])) { key = gouid.String(keySize, gouid.MixedCaseAlphaNum)
log.Error().Err(err).Str("func", fn).Str("rkey", rKey).Msg("corresponding text entry not found in database!")
errThrow(c, 500, "500", "500") for uidRef, _ := txtDB.Get([]byte(uid)); uidRef != nil; {
return slog.Info().Msg(" uid already exists! generating new...")
uid = gouid.String(uidSize, gouid.MixedCaseAlphaNum)
}
for keyRef, _ := keyDB.Get([]byte(key)); keyRef != nil; {
slog.Info().Msg(" delete key already exists! generating new...")
key = gouid.String(keySize, gouid.MixedCaseAlphaNum)
} }
// if we passed all those checks, delete the txt entry from the database hashDB.Put([]byte(hash), []byte(uid))
err := txtDB.Delete([]byte(finalTarget[1]))
// failed to delete it? shouldn't happen err = txtDB.Put([]byte(uid), b)
if err != nil { if err != nil {
log.Error().Err(err).Str("func", fn).Str("rkey", finalTarget[1]).Msg("delete failed!") slog.Error().Msg("failed to save text!")
errThrow(c, 500, "500", "500") termbin.Reply <- termbin.Message{Type: "ERROR", Content: "internal server error"}
return return
} }
err = keyDB.Put([]byte(key), []byte("t."+uid))
// make sure its actually gone
if txtDB.Has([]byte(finalTarget[1])) {
log.Error().Err(err).Str("func", fn).Str("rkey", finalTarget[1]).Msg("delete failed!?")
errThrow(c, 500, "500", "500")
return
}
// remove the delete key entry but not the hash (see below)
log.Info().Str("func", fn).Str("rkey", finalTarget[1]).Msg("text deleted successfully")
log.Debug().Str("func", fn).Str("rkey", finalTarget[1]).Msg("Removing delete key entry")
err = keyDB.Delete([]byte(rKey))
if err != nil { if err != nil {
log.Error().Err(err).Str("func", fn).Str("rkey", finalTarget[1]).Msg("Couldn't delete key") slog.Error().Msg("failed to save delete key!")
termbin.Reply <- termbin.Message{Type: "ERROR", Content: "internal server error"}
return
} }
c.JSON(200, "DELETE_SUCCESS")
// it would be insane to try and delete the hash here slog.Debug().Str("uid", uid).Msg("saved to database successfully, sending to Serve")
// if someone is uploading this text again after del
// and the file corresponding to the hash no longer exists post := &Post{
// we will delete the hash entry then and re-add then Type: "t",
Uid: uid,
Key: key,
Priv: false,
}
termbin.Reply <- termbin.Message{Type: "URL", Content: post.URLString()}
} }
func txtView(c *gin.Context) { func txtView(c *gin.Context) {
fn = "txtView" slog := log.With().Str("caller", "txtView").Logger()
// the user can access their text with or without a file extension in URI
// however it must be a valid extension (more checks further down) raddr, _ := c.RemoteIP()
log.Info().Str("func", fn).Msg("request received") if txtThrottled(raddr.String()) {
errThrow(c, 429, "ratelimited", "too many requests")
return
}
// the user can access their image with or without a file extension in URI
slog.Debug().Msg("request received") // however it must be a valid extension (more checks further down)
sUid := strings.Split(c.Param("uid"), ".") sUid := strings.Split(c.Param("uid"), ".")
rUid := sUid[0] rUid := sUid[0]
fExt = ""
if len(sUid) > 1 { if len(sUid) > 1 {
fExt = strings.ToLower(sUid[1]) fExt = strings.ToLower(sUid[1])
log.Debug().Str("func", fn).Str("ext", fExt).Msg("detected file extension")
if fExt != "txt" { if fExt != "txt" {
log.Error().Err(err).Str("func", fn).Msg("Bad file extension!") slog.Error().Msg("bad file extension")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return 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) != uidSize { if !valid.IsAlphanumeric(rUid) || len(rUid) != uidSize {
log.Error().Err(err).Str("func", fn).Msg("request discarded as invalid") slog.Error().Msg("request discarded as invalid") // these limits should be variables eventually
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
// now that we think its a valid request we will query // query bitcask for the id
log.Debug().Str("func", fn).Str("rUid", rUid).Msg("request validated")
fBytes, _ := txtDB.Get([]byte(rUid)) fBytes, _ := txtDB.Get([]byte(rUid))
if fBytes == nil { if fBytes == nil {
log.Error().Err(err).Str("func", fn).Str("rUid", rUid).Msg("no corresponding file for this id") slog.Error().Str("rUid", rUid).Msg("no corresponding file for this id")
errThrow(c, 404, "404", "File not found") errThrow(c, 404, "404", "file not found")
return return
} }
c.Data(200, "text/plain", fBytes) file, _ := termbin.Deflate(fBytes)
c.Data(200, "text/plain", file)
log.Info().Str("func", fn).Str("rUid", rUid).Msg("Success") slog.Info().Str("rUid", rUid).Msg("success")
} }
func txtPost(c *gin.Context) { func txtDel(c *gin.Context) {
fn = "txtPost" slog := log.With().
Str("caller", "txtDel").Logger()
t := c.PostForm("txt") slog.Debug().Msg("new_request") // received request
priv := c.PostForm("priv")
tbyte := []byte(t) if !validateKey(c.Param("key")) {
errThrow(c, 400, "400", "400")
return
}
if err != nil { rKey := c.Param("key")
log.Error().Err(err).Str("fn", fn).Msg("Oh?")
targetTxt, _ := keyDB.Get([]byte(rKey))
if targetTxt == nil || !strings.Contains(string(targetTxt), "t.") {
slog.Warn().Str("rkey", rKey).Msg("no txt delete entry found with provided key")
errThrow(c, 400, "400", "400")
return
}
t := strings.Split(string(targetTxt), ".")[1]
if !txtDB.Has([]byte(t)) {
slog.Warn().Str("rkey", rKey).Msg("corresponding image not found in database!")
errThrow(c, 500, "500", "500") // this shouldn't happen...?
return
}
if err := txtDB.Delete([]byte(t)); err != nil {
slog.Error().Str("rkey", t).Msg("delete failed!")
errThrow(c, 500, "500", "500") errThrow(c, 500, "500", "500")
return return
} }
if len(t) == 0 { if txtDB.Has([]byte(t)) {
log.Warn().Str("fn", fn).Msg("received an empty request") slog.Error().Str("rkey", t).Msg("delete failed!?")
errThrow(c, 400, "400", "400") errThrow(c, 500, "500", "500")
return return
} }
if c.ContentType() != "text/plain" { slog.Info().Str("rkey", t).Msg("Image file deleted successfully")
log.Warn().Str("fn", fn).Str("ContentType", c.ContentType()).Msg("received a non-text content-type") slog.Debug().Str("rkey", t).Msg("Removing delete key entry")
errThrow(c, 400, "400", "400") err = keyDB.Delete([]byte(rKey))
return
}
// an optional switch for a privnote style burn after read
// priv := c.GetBool("private")
if err != nil { if err != nil {
// incoming POST data is invalid slog.Error().Str("rkey", t).Msg("Couldn't delete key")
errThrow(c, http.StatusBadRequest, err.Error(), "400") // 400 bad request // 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
log.Debug().Str("func", fn).Msg("New paste") // we will delete the hash entry then and re-add then
}
if priv == "on" {
// check for dupes //func serveTermbin() {
log.Debug().Str("func", fn).Msg("calculating blake2b checksum") func serveTermbin() {
Hashr, _ := blake2b.New(64, nil) go func() {
Hashr.Write(tbyte) for {
hash := Hashr.Sum(nil) incoming()
log.Debug().Str("func", fn).Msg("Checking for duplicate's in database") }
txtRef, _ := txtDB.Get(hash) }()
ogUid := string(txtRef) err := termbin.Listen("", txtPort)
if err != nil {
if txtRef != nil { println(err.Error())
log.Debug().Str("func", fn).Str("ogUid", ogUid).Msg("duplicate checksum in hash database, checking if file still exists...") return
if txtDB.Has(txtRef) { }
log.Debug().Str("func", fn).Str("ogUid", ogUid).Msg("duplicate file found! returning original URL")
// they weren't the original uploader so they don't get a delete key
txtFin(c, ogUid, "nil")
return
} else {
log.Debug().Str("func", fn).Str("ogUid", ogUid).Msg("stale hash found, deleting entry...")
hashDB.Delete(hash)
}
}
log.Info().Str("func", fn).Msg("no duplicate txts found, generating uid and delete key")
// generate identifier and delete key based on configured sizes
uid := gouid.String(uidSize, gouid.MixedCaseAlphaNum)
key := gouid.String(keySize, gouid.MixedCaseAlphaNum)
// lets make sure that we don't clash even though its highly unlikely
for uidRef, _ := txtDB.Get([]byte(uid)); uidRef != nil; {
log.Info().Str("func", fn).Msg("uid already exists! generating another...")
uid = gouid.String(uidSize, gouid.MixedCaseAlphaNum)
}
for keyRef, _ := keyDB.Get([]byte(key)); keyRef != nil; {
log.Info().Str("func", fn).Msg(" delete key already exists! generating another...")
key = gouid.String(keySize, gouid.MixedCaseAlphaNum)
}
// save checksum to db to prevent dupes in the future
hashDB.Put([]byte(hash), []byte(uid))
log.Debug().Str("func", fn).Str("uid", uid).Msg("saving file to database")
err = txtDB.Put([]byte(uid), []byte(tbyte))
if err != nil {
errThrow(c, 500, err.Error(), "upload failed")
return
}
// add delete key to database with txt prefix
err = keyDB.Put([]byte(key), []byte("t."+uid))
if err != nil {
errThrow(c, http.StatusInternalServerError, err.Error(), "internal error")
return
}
}
log.Debug().Str("func", fn).Str("uid", uid).Msg("saved to database successfully, sending to txtFin")
txtFin(c, uid, key)
} }