vodkas

Simple file sharing server
Log | Files | Refs

vodkas.go (11736B)


      1 //  Seems to work: `go build -ldflags "-linkmode external -extldflags -static"`
      2 package main
      3 
      4 import (
      5 	"crypto/rand"
      6 	"encoding/binary"
      7 	"encoding/hex"
      8 	"flag"
      9 	"fmt"
     10 	"io"
     11 	"io/ioutil"
     12 	"log"
     13 	"mime/multipart"
     14 	"net/http"
     15 	"strconv"
     16 	"strings"
     17 	"sync"
     18 	"time"
     19 
     20 	"github.com/gorilla/mux"
     21 	"github.com/pkg/errors"
     22 	"go.etcd.io/bbolt"
     23 )
     24 
     25 /**************** Handler Functions ****************/
     26 
     27 const (
     28 	Version          = "1.0"
     29 	FormNameFile     = "file"
     30 	FormNameText     = "text"
     31 	FormNameNumShots = "numdls"
     32 	AdminKeyHeader   = "Admin-Key"
     33 )
     34 
     35 var (
     36 	keyTakenMessage    = []byte("The requested key is taken. Try another.\n")
     37 	serverErrorMessage = []byte("Internal server error!")
     38 	pourSuccessMessage = []byte("Successfully submitted data")
     39 )
     40 
     41 // TODO: Might want to update this with info about vv.sh instead?
     42 var InfoMessage = []byte(`Usage:
     43 - POUR:     curl vetle.vodka[/<requested-shot-key>] -d <data>
     44 - SHOT:     curl vetle.vodka/<shot-key>
     45 
     46 A shot key is an at-the-moment unique ID that's linked to the dumped data.
     47 As soon as a specific shot has been accessed both the link and the contents 
     48 are removed completely.
     49 `)
     50 
     51 var db *bbolt.DB
     52 var dataBucketKey = "data"
     53 var numsBucketKey = "nums"
     54 var shotNumsLimit int
     55 var adminKey string
     56 var storageCTRL struct {
     57 	bytesMax  int
     58 	bytesUsed int
     59 	sync.Mutex
     60 }
     61 
     62 // Check if the key is taken without touching the contents
     63 func smell(shotKey string) (found bool) {
     64 	_ = db.View(func(tx *bbolt.Tx) error {
     65 		nb := tx.Bucket([]byte(numsBucketKey))
     66 		if nb == nil {
     67 			log.Fatal("Failed to open essential bucket")
     68 		}
     69 		found = nb.Get([]byte(shotKey)) != nil
     70 		return nil
     71 	})
     72 	return
     73 }
     74 
     75 // take a shot, i.e. load the contents and decrement the nums
     76 func shot(shotKey string) (contents []byte, err error) {
     77 	storageCTRL.Lock()
     78 	defer storageCTRL.Unlock()
     79 	err = db.Update(func(tx *bbolt.Tx) error {
     80 		bShotKey := []byte(shotKey)
     81 		datab := tx.Bucket([]byte(dataBucketKey))
     82 		numsb := tx.Bucket([]byte(numsBucketKey))
     83 		if datab == nil || numsb == nil {
     84 			log.Fatal("Failed to open essential bucket")
     85 		}
     86 		// Find if key exists and, in that case, find number of shots left
     87 		bnums := numsb.Get(bShotKey)
     88 		if bnums == nil {
     89 			return errors.New("no shots available for key " + shotKey)
     90 		}
     91 		nums, _ := binary.Varint(bnums)
     92 		nums--
     93 		log.Printf("Found contents for key '%v'. Shots left: %v", shotKey, nums)
     94 		// Get contents
     95 		contents = datab.Get(bShotKey)
     96 		if contents == nil {
     97 			log.Fatal("a key was found in the nums bucket but not the data bucket")
     98 		}
     99 		// Delete nums and data if this was the last shot. Otherwise decrement nums
    100 		if nums == 0 {
    101 			if err = numsb.Delete(bShotKey); err != nil {
    102 				return err
    103 			}
    104 			return datab.Delete(bShotKey)
    105 		}
    106 		// bnums must be 'remade' to avoid segmentation fault when going from eg. 0 to -1
    107 		// I guess because the buffer used to store 0 is too small for -1 (2's complement?).
    108 		// Size of the buffer is returned by binary.Varint btw, so this is easy to check.
    109 		bnums = make([]byte, binary.MaxVarintLen64)
    110 		binary.PutVarint(bnums, nums)
    111 		numsb.Put(bShotKey, bnums)
    112 		return nil
    113 	})
    114 	if err == nil {
    115 		storageCTRL.bytesUsed -= len(contents)
    116 	}
    117 	return
    118 }
    119 
    120 // fixNumShots checks that numshots is valid, i.e. between 1 and max
    121 // (unless an admin header is given), otherwise adjusts to legal values
    122 func legalNumshots(numshots int, r *http.Request) int {
    123 	if r.Header.Get(AdminKeyHeader) == adminKey {
    124 		return numshots
    125 	}
    126 	if numshots < 1 {
    127 		return 1
    128 	}
    129 	if numshots > shotNumsLimit {
    130 		return shotNumsLimit
    131 	}
    132 	return numshots
    133 }
    134 
    135 func pour(shotKey string, r *http.Request) (err error) {
    136 	storageCTRL.Lock()
    137 	defer storageCTRL.Unlock()
    138 	var contents []byte
    139 	var numshots int
    140 	// Dumps can be both x-www-urlencoded and multipart/form-data.
    141 	// Try multipart first, then x-www-urlencoded if no mpReader is returned
    142 	mpReader, _ := r.MultipartReader()
    143 	if mpReader != nil {
    144 		contents, numshots, err = extractMultipart(mpReader)
    145 	} else {
    146 		numshots = 1
    147 		contents, err = ioutil.ReadAll(r.Body)
    148 	}
    149 	numshots = legalNumshots(numshots, r)
    150 	if err != nil {
    151 		return err
    152 	}
    153 	if storageCTRL.bytesUsed+len(contents) > storageCTRL.bytesMax {
    154 		return errors.New("database is full")
    155 	}
    156 	fmt.Printf("Number of shots: %v\n", numshots)
    157 	err = db.Update(func(tx *bbolt.Tx) error {
    158 		datab := tx.Bucket([]byte(dataBucketKey))
    159 		numsb := tx.Bucket([]byte(numsBucketKey))
    160 		if datab == nil || numsb == nil {
    161 			log.Fatal("failed to open essential bucket")
    162 		}
    163 		// Put number of shots
    164 		bnums64 := make([]byte, binary.MaxVarintLen64)
    165 		binary.PutVarint(bnums64, int64(numshots))
    166 		err = numsb.Put([]byte(shotKey), bnums64)
    167 		if err != nil {
    168 			return err
    169 		}
    170 		return datab.Put([]byte(shotKey), contents)
    171 	})
    172 	if err == nil {
    173 		storageCTRL.bytesUsed += len(contents)
    174 	}
    175 	return err
    176 }
    177 
    178 func extractMultipart(mr *multipart.Reader) (contents []byte, num int, err error) {
    179 	for {
    180 		var part *multipart.Part
    181 		part, err = mr.NextPart()
    182 		if err == io.EOF {
    183 			err = nil
    184 			break
    185 		}
    186 		formName := part.FormName()
    187 		if err != nil {
    188 			log.Fatal(err)
    189 		}
    190 		if formName == FormNameText || formName == FormNameFile {
    191 			contents, err = ioutil.ReadAll(part)
    192 			if err != nil {
    193 				return
    194 			}
    195 			continue
    196 		}
    197 		if formName == FormNameNumShots {
    198 			var numShotsRaw []byte
    199 			numShotsRaw, err = ioutil.ReadAll(part)
    200 			if err != nil {
    201 				return
    202 			}
    203 			num, err = strconv.Atoi(string(numShotsRaw))
    204 			if err != nil {
    205 				return
    206 			}
    207 		}
    208 	}
    209 	err = nil
    210 	return
    211 }
    212 
    213 // TODO: Extract post page too
    214 func writeUploadPage(res http.ResponseWriter, textOnly bool, shotKey string) (err error) {
    215 	if textOnly {
    216 		_, err = res.Write(InfoMessage)
    217 	} else {
    218 		templateData := struct{ ShotKey string }{shotKey}
    219 		err = uploadPageTemplate.Execute(res, templateData)
    220 	}
    221 	if err != nil {
    222 		res.WriteHeader(http.StatusInternalServerError)
    223 	}
    224 	return
    225 }
    226 
    227 // TODO: Make function that handles responses based on mode?? like
    228 //              makeResponse(rw *http.ResponseWriter, textOnly bool, data, responseKey)
    229 //       where textOnly and responseKey maps to response messages or templates
    230 
    231 func rootHandler(res http.ResponseWriter, r *http.Request) {
    232 	// Detect whether Simple mode (text only) is active
    233 	textOnly := r.Header.Get("Simple") != "" // for forcing textOnly mode
    234 	textOnly = textOnly || strings.Contains(r.Header.Get("User-Agent"), "curl")
    235 	if r.Method == http.MethodGet {
    236 		writeUploadPage(res, textOnly, "")
    237 	} else if r.Method == http.MethodPost {
    238 		// Generate random shot key
    239 		random := make([]byte, 16)
    240 		rand.Read(random)
    241 		shotKey := hex.EncodeToString(random)
    242 		// Try to pour
    243 		if err := pour(shotKey, r); err != nil {
    244 			log.Println(err)
    245 			res.WriteHeader(http.StatusInternalServerError)
    246 			// TODO: Error based on err
    247 			res.Write([]byte("An error occurred"))
    248 			return
    249 		}
    250 		// TODO: Error template. The current handling is the opposite of helpful
    251 		if /*textOnly*/ true {
    252 			response := r.Host + "/" + shotKey
    253 			if _, err := res.Write([]byte(response)); err != nil {
    254 				log.Panicln("Error when trying to write response body")
    255 			}
    256 		}
    257 	}
    258 }
    259 
    260 func keyHandler(res http.ResponseWriter, r *http.Request) {
    261 	key := mux.Vars(r)["shotKey"]
    262 	textOnly := r.Header.Get("Simple") != "" // for forcing textOnly mode
    263 	textOnly = textOnly || strings.Contains(r.Header.Get("User-Agent"), "curl")
    264 	var err error
    265 	if r.Method == http.MethodGet {
    266 		// Return upload page if the key is available
    267 		if !smell(key) {
    268 			writeUploadPage(res, textOnly, key)
    269 			return
    270 		}
    271 		// Otherwise return contents
    272 		contents, err := shot(key)
    273 		if err != nil {
    274 			log.Println(err)
    275 			res.Write(keyTakenMessage)
    276 			res.WriteHeader(http.StatusInternalServerError)
    277 		}
    278 		if _, err = res.Write(contents); err != nil {
    279 			log.Panicln("Error when trying to write response")
    280 			res.WriteHeader(http.StatusInternalServerError)
    281 		}
    282 	} else if r.Method == http.MethodPost {
    283 		// Admin should be able to overwrite anything
    284 		if r.Header.Get(AdminKeyHeader) == adminKey {
    285 			if err = pour(key, r); err != nil {
    286 				goto commonerror
    287 			}
    288 			return
    289 		}
    290 		if smell(key) {
    291 			// POSTs from website to taken shouldn't happen, so use textOnly always
    292 			if _, err = res.Write(keyTakenMessage); err != nil {
    293 				res.WriteHeader(http.StatusInternalServerError)
    294 			}
    295 		} else {
    296 			if err = pour(key, r); err != nil {
    297 				goto commonerror
    298 			}
    299 			res.Write(pourSuccessMessage)
    300 		}
    301 		return
    302 	commonerror:
    303 		// TODO: Notify if database is full, not just server error for everything
    304 		res.WriteHeader(http.StatusInternalServerError)
    305 		res.Write(serverErrorMessage)
    306 		log.Println(err)
    307 	}
    308 	//fmt.Printf("Request from client: %v\n", r.Header.Get("User-Agent"))
    309 	return
    310 }
    311 
    312 // Returns summary of the database in the form of 'number of elements, numBytes, err'
    313 // If 'speak' is true, all keys and the size of their corresponding data are printed
    314 func statDB(speak bool) (int, int, error) {
    315 	var numElements, numBytes int
    316 	err := db.View(func(tx *bbolt.Tx) error {
    317 		root := tx.Bucket([]byte(dataBucketKey))
    318 		if root == nil {
    319 			return errors.New("Failed to open root bucket")
    320 		}
    321 		// Not sure if bocket.Stats().LeafInUse equals the actual number of bytes
    322 		// in use, but I think it should be approximately the same
    323 		if !speak {
    324 			numElements = root.Stats().KeyN
    325 			numBytes = root.Stats().LeafInuse
    326 			return nil
    327 		}
    328 		// For 'speak', the bucket must be iterated through anyway, so might as well
    329 		// count the number of elements and bytes manually
    330 		err := root.ForEach(func(k, v []byte) error {
    331 			fmt.Printf("%v %v\n", string(k), len(v))
    332 			numElements++
    333 			numBytes += len(v)
    334 			return nil
    335 		})
    336 		return err
    337 	})
    338 	return numElements, numBytes, err
    339 }
    340 
    341 // *init* is a special function, hence the name of this one
    342 // https://tutorialedge.net/golang/the-go-init-function/
    343 func initialize(dbFile string, limitStorage, limitNums, port int, admKey string) error {
    344 	var err error // Because ':=' can't be used on the line below without declaring db as a new *local* variable, making the global one nil
    345 	db, err = bbolt.Open(dbFile, 0600, &bbolt.Options{Timeout: 1 * time.Second})
    346 	if err != nil {
    347 		return err
    348 	}
    349 	err = db.Update(func(tx *bbolt.Tx) error {
    350 		_, err := tx.CreateBucketIfNotExists([]byte(dataBucketKey))
    351 		_, err = tx.CreateBucketIfNotExists([]byte(numsBucketKey))
    352 		return err
    353 	})
    354 	if err != nil {
    355 		return err
    356 	}
    357 	_, numBytes, err := statDB(false)
    358 	if err != nil {
    359 		return err
    360 	}
    361 	storageCTRL.bytesMax = 1000 * limitStorage
    362 	storageCTRL.bytesUsed = numBytes
    363 	shotNumsLimit = limitNums
    364 	adminKey = admKey
    365 	fmt.Printf("%v / %v KBs used\n", numBytes/1000, limitStorage)
    366 	fmt.Println("Server started listening at port", port)
    367 	return nil
    368 }
    369 
    370 /**************** Main ****************/
    371 func main() {
    372 	port := flag.Int("p", 8080, "Port")
    373 	version := flag.Bool("v", false, "Print version and exit")
    374 	dbFile := flag.String("d", "vodka.db", "Database file")
    375 	stat := flag.Bool("s", false, "View database keys and size of associated contents")
    376 	storageLimit := flag.Int("l", 10000, "Storage limit in kilobytes (1000 bytes)")
    377 	numsLimit := flag.Int("n", 10, "Maximum number of shots per key")
    378 	admKey := flag.String("a", "vodkas", "Admin key to allow unlimited shots")
    379 	flag.Parse()
    380 	if *version {
    381 		fmt.Println("vodkas " + Version)
    382 		return
    383 	}
    384 	err := initialize(*dbFile, *storageLimit, *numsLimit, *port, *admKey)
    385 	defer db.Close()
    386 	if err != nil {
    387 		log.Fatal(err)
    388 	}
    389 	if *stat {
    390 		fmt.Println("Elements in database:")
    391 		numElements, numBytes, err := statDB(true)
    392 		if err != nil {
    393 			log.Fatal(err)
    394 		}
    395 		fmt.Printf("\n%v elements in database\n", numElements)
    396 		fmt.Printf("\n%v bytes used\n", numBytes)
    397 		return
    398 	}
    399 	router := mux.NewRouter()
    400 	router.HandleFunc("/", rootHandler)
    401 	router.HandleFunc("/{shotKey}", keyHandler)
    402 	http.Handle("/", router)
    403 
    404 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", *port), nil))
    405 }