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 }