From 830f43f467ad4f9dc0aef9335bb2756e57dfed4c Mon Sep 17 00:00:00 2001 From: Mitchell Riedstra Date: Tue, 27 Jul 2021 00:16:31 -0400 Subject: Initial --- .gitignore | 1 + error.tpl | 22 ++++++ go.mod | 5 ++ go.sum | 2 + index.tpl | 32 +++++++++ main.go | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 18 +++++ style.css | 71 +++++++++++++++++++ view.tpl | 21 ++++++ 9 files changed, 407 insertions(+) create mode 100644 .gitignore create mode 100644 error.tpl create mode 100644 go.mod create mode 100644 go.sum create mode 100644 index.tpl create mode 100644 main.go create mode 100644 readme.md create mode 100644 style.css create mode 100644 view.tpl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc10a06 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bpaste diff --git a/error.tpl b/error.tpl new file mode 100644 index 0000000..c53fa4f --- /dev/null +++ b/error.tpl @@ -0,0 +1,22 @@ + + + + + + Error {{.Short}} + + + + +

{{.Short}}

+ +{{.Long}} + + + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f3a6dab --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module riedstra.dev/mitch/bpaste + +go 1.16 + +require github.com/gorilla/mux v1.8.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5350288 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/index.tpl b/index.tpl new file mode 100644 index 0000000..a117af9 --- /dev/null +++ b/index.tpl @@ -0,0 +1,32 @@ + + + + + + Brutally Simple Pastebin + + + + + +

Brutally simple pastebin

+ +
+ + + + + + +
+ + +
+ + + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..ee53f7e --- /dev/null +++ b/main.go @@ -0,0 +1,235 @@ +package main + +import ( + "compress/gzip" + "crypto/rand" + _ "embed" + "encoding/base64" + "encoding/gob" + "errors" + "flag" + "html/template" + "log" + "net/http" + "os" + "time" + + "github.com/gorilla/mux" +) + +var logger = log.New(os.Stderr, "", 0) + +// const ID_BYTES = 5 +const ID_BYTES = 16 + +//go:embed index.tpl +var indexTemplateContent string +var indexTemplate = template.Must(template.New("index").Parse(indexTemplateContent)) + +//go:embed view.tpl +var viewTemplateContent string +var viewTemplate = template.Must(template.New("view").Parse(viewTemplateContent)) + +//go:embed error.tpl +var errorTemplateContent string +var errorTemplate = template.Must(template.New("error").Parse(errorTemplateContent)) + +//go:embed style.css +var stylesheetContent []byte + +type Paste struct { + Id string + Title string + Content []byte +} + +func (p Paste) GetContent() string { + return string(p.Content) +} + +func (p *Paste) Load() error { + fh, err := os.Open(p.Id) + if err != nil { + return err + } + + zrdr, err := gzip.NewReader(fh) + if err != nil { + return err + } + + dec := gob.NewDecoder(zrdr) + err = dec.Decode(&p) + if err != nil { + return err + } + + zrdr.Close() + + return fh.Close() +} + +func (p Paste) Save() error { + fh, err := os.OpenFile(p.Id, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return err + } + + zwr := gzip.NewWriter(fh) + + enc := gob.NewEncoder(zwr) + err = enc.Encode(&p) + if err != nil { + return err + } + + zwr.Close() + + return fh.Close() +} + +func GenId() string { + r := make([]byte, ID_BYTES) + _, err := rand.Read(r) + if err != nil { + logger.Fatal(err) + } + + return base64.RawURLEncoding.EncodeToString(r) +} + +func main() { + fl := flag.NewFlagSet("Brutally simple pastebin", flag.ExitOnError) + listen := fl.String("listen", ":6130", "Address to bind to, LISTEN_ADDR environment variable overrides") + debug := fl.Bool("d", false, "debugging add information to the logging output DEBUG=true|false controls this as well") + storage := fl.String("s", "", "Directory to serve, must be supplied via flag or STORAGE_DIR environment variable") + _ = fl.Parse(os.Args[1:]) + + if addr := os.Getenv("LISTEN_ADDR"); addr != "" { + *listen = addr + } + if d := os.Getenv("DEBUG"); d == "true" || *debug { + logger.SetFlags(log.LstdFlags | log.Llongfile) + } + if d := os.Getenv("STORAGE_DIR"); d != "" { + *storage = d + } + + if *storage == "" { + logger.Fatal("Cannot continue without storage directory, set `-s` flag or STORAGE_DIR environment variable") + } + + err := os.Chdir(*storage) + if err != nil { + logger.Fatal(err) + } + + mux := mux.NewRouter() + + mux.HandleFunc("/new", newPaste) + mux.HandleFunc("/view/{id}", loadPaste) + mux.HandleFunc("/style.css", stylesheet) + mux.HandleFunc("/", index) + + logger.Println("listening on: ", *listen) + + srv := &http.Server{ + Handler: mux, + Addr: *listen, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + logger.Fatal(srv.ListenAndServe()) +} + +func newPaste(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + logger.Println(err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + title := r.FormValue("title") + content := r.FormValue("content") + + if title == "" || content == "" { + logger.Println("Empty title or content") + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + p := &Paste{ + Id: GenId(), + Title: title, + Content: []byte(content), + } + + err = p.Save() + if err != nil { + logger.Println(err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/view/"+p.Id, http.StatusFound) +} + +func loadPaste(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + id, ok := vars["id"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + err := errorTemplate.Execute(w, map[string]string{ + "Short": "ID Not found", + "Long": "There was no ID supplied", + }) + if err != nil { + logger.Println(err) + } + return + } + + p := &Paste{Id: id} + + err := p.Load() + if err != nil { + logger.Println(err) + if errors.Is(err, os.ErrNotExist) { + w.WriteHeader(http.StatusNotFound) + err = errorTemplate.Execute(w, map[string]string{ + "Short": "ID Not found", + "Long": "ID: " + p.Id + "Was not found", + }) + } else { + w.WriteHeader(http.StatusInternalServerError) + err = errorTemplate.Execute(w, map[string]string{ + "Short": "ID Not found", + "Long": "There was an issue reading ID: " + p.Id, + }) + } + if err != nil { + logger.Println(err) + } + return + } + + err = viewTemplate.Execute(w, p) + if err != nil { + logger.Println(err) + } +} + +func stylesheet(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-type", "text/css") + _, _ = w.Write(stylesheetContent) +} + +func index(w http.ResponseWriter, r *http.Request) { + err := indexTemplate.Execute(w, nil) + if err != nil { + logger.Println(err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5b6f128 --- /dev/null +++ b/readme.md @@ -0,0 +1,18 @@ +# Brutally simple pastebin + +Decided to challenge myself to write a really simple pastebin in go, utilizing +almost nothing but the standard library in less than an hour. + +This is the result of about an hour and a half of work. + +It will create a binary with embedded templates. Simply point it at the +directory in which you'd like to store your pastes. + +Each paste is a file, a random id assigned at creation. There's no way to edit +or change a paste. The only way to delete it is to remove it from the +filesystem. + +There's no indexing function or search either. + +There's also absolutely no protection against people spamming the API until +your disk is full, no authentication either. diff --git a/style.css b/style.css new file mode 100644 index 0000000..472b18c --- /dev/null +++ b/style.css @@ -0,0 +1,71 @@ +nav { + border-bottom: 3px solid #000; + padding-bottom: 10px; +} +a { + text-decoration: none; + color: #268bd2; +} +a:visited { + color: #d22653; +} +a:hover { + text-decoration: underline; +} + +code { + color: #9672d5; + /* background-color: #ff000020; */ + /* padding: 2px; */ + font-size: .8em; + font-family: "Roboto Mono", "Monaco", "Lucida Console", "DejaVu Sans Mono", "monospace"; +} + +pre code { + color: #000; + background-color: #FFFFEA; + display: block; + padding: 10px; + border: 1px solid; + line-height: 1.1; + overflow: auto; +} + + +blockquote { + border-left: 4px solid #aaa; + padding-left: 1em; +} + +/* + * The following was shamelessly ripped from: + * http://bettermotherfuckingwebsite.com/ + * And subsequently modified to suit my needs + */ + +body { + margin: 40px auto; + max-width: 80%; + line-height: 1.6; + font-size: 1em; + color: #444; + padding: 0 10px; + /* Added because some browsers don't default to white */ + background-color: #fff; +} + +img { + width: 100%; + height: auto; +} + +h1,h2,h3 { + line-height: 1.2 +} + +@media screen and (min-width: 960px) { + body { + max-width: 768px; + } +} + diff --git a/view.tpl b/view.tpl new file mode 100644 index 0000000..84c5ebf --- /dev/null +++ b/view.tpl @@ -0,0 +1,21 @@ + + + + + + Paste: {{.Title}} + + + + +

Paste: "{{.Title}}"

+ +
{{.GetContent}}
+ + + + -- cgit v1.2.3