vodkas

Simple file sharing server
Log | Files | Refs

commit 362e8cd06673d46b3308554bf62c6056f25bb57e
parent e35d00f025f12cdd0d35fd563cf7d82c098082fd
Author: Vetle Haflan <vetle@haflan.dev>
Date:   Sun,  1 Mar 2020 17:26:03 +0100

Support multiline form data and continue refactoring

Diffstat:
Mreadme.md | 8++------
Atemplates.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mvodkas.go | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
3 files changed, 121 insertions(+), 26 deletions(-)

diff --git a/readme.md b/readme.md @@ -1,8 +1,4 @@ # vodka server -One time text dumps with an optional password. -No database needed for low-scale use, so this can be written entirely in Go and -probably be compiled statically. - -To run it in Docker anyway you can simply copy the binary into an alpine -container and run it. +Short time text and file dumps with an optional password. +Uses a simple `bbolt` database to store data. diff --git a/templates.go b/templates.go @@ -0,0 +1,53 @@ +// TODO: A better alternative to this inline HTML stuff: +// https://odino.org/bundling-static-files-within-your-golang-app/ +package main + +import "html/template" + + +// I think template data variables must be exported (upper case) in order to be usable +var uploadPageTemplate = template.Must(template.New("uploadPage").Parse(` +<!DOCTYPE html> +<html> +<head> + <style> + textarea { width: 100%; } + </style> + +</head> +<body> +<h1>Upload to /{{.ShotKey}}</h1> + +<form action="/{{.ShotKey}}" method="POST" onreset="formReset()" enctype="multipart/form-data"> + <label for="input-numdls">Number of downloads</label><br> + <input value="1" type="number" id="input-numdls" name="numdls"><br> + <p> + <label for="input-file">File dump</label><br> + <input type="file" id="input-file" name="file" style="width: 100%;" + oninput="inputChanged(this, '#input-text')"><br> + <b>or</b><br> + <label for="input-text">Text dump</label><br> + <textarea id="input-text" name="text" rows="15" + oninput="inputChanged(this, '#input-file')"></textarea><br> + </p> + <input type="reset" value="Reset"> + <input type="submit" value="Submit"> +</form> + +<script> + function formReset() { + document.querySelector("#input-text").removeAttribute("disabled"); + document.querySelector("#input-file").removeAttribute("disabled"); + } + function inputChanged(changedInput, otherInputId) { + const otherInput = document.querySelector(otherInputId); + if (changedInput.value) { + otherInput.setAttribute("disabled", true); + } else { + otherInput.removeAttribute("disabled"); + } + } +</script> +</body> +</html> +`)) diff --git a/vodkas.go b/vodkas.go @@ -9,17 +9,23 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" "go.etcd.io/bbolt" + "io" "io/ioutil" "log" - //"mime/multipart" + "mime/multipart" "net/http" "strings" + "strconv" "time" ) /**************** Handler Functions ****************/ +const FormNameFile = "file" +const FormNameText = "text" +const FormNameNumShots = "numdls" + const InfoMessage = `Usage: - POUR: curl vetle.vodka[/<requested-shot-key>] -d <data> - SHOT: curl vetle.vodka/<shot-key> @@ -73,14 +79,35 @@ func pour(key string, formdata []byte) { fmt.Println(string(formdata)) } -func ShotHandler() { -// TODO: Generate random key if no request given (https://flaviocopes.com/go-random/) - /*if !found { - res.WriteHeader(http.StatusNotFound) - if _, err := res.Write([]byte(fmt.Sprint("404 no shot here\n"))); err != nil { - log.Panicln("Error when trying to write response") - } - return*/ +func extractMultipart(mr *multipart.Reader) (contents []byte, num int, err error) { + for { + var part *multipart.Part + part, err = mr.NextPart() + if err == io.EOF { + err = nil + break + } + formName := part.FormName() + if err != nil { + log.Fatal(err) + } + if formName == FormNameText || formName == FormNameFile { + contents, err = ioutil.ReadAll(part) + if err != nil { + return + } + continue + } + if formName == FormNameNumShots { + var numShotsRaw []byte + numShotsRaw, err = ioutil.ReadAll(part) + if err != nil { return } + num, err = strconv.Atoi(string(numShotsRaw)) + if err != nil { return } + } + } + err = nil + return } @@ -88,33 +115,52 @@ func RootHandler(res http.ResponseWriter, r *http.Request) { // Detect whether Simple mode (text only) is active textOnly := r.Header.Get("Simple") != "" // for forcing textOnly mode textOnly = textOnly || strings.Contains(r.Header.Get("User-Agent"), "curl") - var responseText string + var response string if r.Method == http.MethodGet { if textOnly { - responseText = InfoMessage + response = InfoMessage + if _, err := res.Write([]byte(response)); err != nil { + log.Panicln("Error when trying to write response body") + } } else { - responseText = "This will be replaced by a template" + templateData := struct { ShotKey string }{ "" } + uploadPageTemplate.Execute(res, templateData) } + return } else if r.Method == http.MethodPost { - b, err := ioutil.ReadAll(r.Body) + var err error + var numshots int + var contents []byte + // Dumps can be both x-www-urlencoded and multipart/form-data. + // Try multipart first, then x-www-urlencoded + mpReader, _ := r.MultipartReader() + if mpReader != nil { + contents, numshots, err = extractMultipart(mpReader) + } else { + numshots = 1 + contents, err = ioutil.ReadAll(r.Body) + } if err != nil { res.WriteHeader(http.StatusInternalServerError) return } + fmt.Printf("Number of shots: %v\n", numshots) + fmt.Printf("Bytes in contents: %v\n", len(contents)) // Generate random shot key random := make([]byte, 16) rand.Read(random) randhex := hex.EncodeToString(random) - push(randhex, b) - if textOnly { - responseText = r.Host + "/" + randhex + push(randhex, contents) + if /*textOnly*/ true { + response = r.Host + "/" + randhex + if _, err := res.Write([]byte(response)); err != nil { + log.Panicln("Error when trying to write response body") + } } + return } - if _, err := res.Write([]byte(responseText)); err != nil { - log.Panicln("Error when trying to write response body") - } - return } + func KeyHandler(res http.ResponseWriter, r *http.Request) { key := mux.Vars(r)["shotKey"] contents, found := pop(key)