From 79707242bd59cf4deb7f999a7a953275b323bb3b Mon Sep 17 00:00:00 2001 From: Chloe Kudryavtsev Date: Sat, 23 Nov 2019 20:11:10 -0500 Subject: Initial golang rewrite --- .gitignore | 3 +++ go.mod | 13 +++++++++++ go.sum | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ http/get.go | 36 +++++++++++++++++++++++++++++++ http/index.go | 12 +++++++++++ http/put.go | 50 ++++++++++++++++++++++++++++++++++++++++++ http/router.go | 23 ++++++++++++++++++++ http/type.go | 5 +++++ http/ua_detector.go | 23 ++++++++++++++++++++ main.go | 45 ++++++++++++++++++++++++++++++++++++++ storage/redis.go | 40 ++++++++++++++++++++++++++++++++++ storage/type.go | 20 +++++++++++++++++ template/code.qtpl | 28 ++++++++++++++++++++++++ template/index.qtpl | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ template/layout.qtpl | 49 +++++++++++++++++++++++++++++++++++++++++ 15 files changed, 466 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 http/get.go create mode 100644 http/index.go create mode 100644 http/put.go create mode 100644 http/router.go create mode 100644 http/type.go create mode 100644 http/ua_detector.go create mode 100644 main.go create mode 100644 storage/redis.go create mode 100644 storage/type.go create mode 100644 template/code.qtpl create mode 100644 template/index.qtpl create mode 100644 template/layout.qtpl diff --git a/.gitignore b/.gitignore index a1c573e..23e9582 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /doc/brpaste.1 /brpaste + +# must be added manually for releases +/template/*.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1cd1ad7 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module toast.cafe/x/brpaste/v2 + +go 1.13 + +require ( + github.com/fasthttp/router v0.5.2 + github.com/go-redis/redis/v7 v7.0.0-beta.4 + github.com/golang/protobuf v1.3.1 // indirect + github.com/twmb/murmur3 v1.0.0 + github.com/valyala/fasthttp v1.6.0 + github.com/valyala/quicktemplate v1.4.1 + golang.org/x/text v0.3.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0fc0fac --- /dev/null +++ b/go.sum @@ -0,0 +1,61 @@ +github.com/fasthttp/router v0.5.2 h1:xdmx8uYc9IFDtlbG2/FhE1Gyowv7/sqMgMonRjoW0Yo= +github.com/fasthttp/router v0.5.2/go.mod h1:Y5JAeRTSPwSLoUgH4x75UnT1j1IcAgVshMDMMrnNmKQ= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-redis/redis/v7 v7.0.0-beta.4 h1:p6z7Pde69EGRWvlC++y8aFcaWegyrKHzOBGo0zUACTQ= +github.com/go-redis/redis/v7 v7.0.0-beta.4/go.mod h1:xhhSbUMTsleRPur+Vgx9sUHtyN33bdjxY+9/0n9Ig8s= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500 h1:9Pi10H7E8E79/x2HSe1FmMGd7BJ1WAqDKzwjpv+ojFg= +github.com/savsgio/gotils v0.0.0-20190925070755-524bc4f47500/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY= +github.com/twmb/murmur3 v1.0.0 h1:MLMwMEQRKsu94uJnoveYjjHmcLwI3HNcWXP4LJuNe3I= +github.com/twmb/murmur3 v1.0.0/go.mod h1:5Y5m8Y8WIyucaICVP+Aep5C8ydggjEuRQHDq1icoOYo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/quicktemplate v1.4.1 h1:tEtkSN6mTCJlYVT7As5x4wjtkk2hj2thsb0M+AcAVeM= +github.com/valyala/quicktemplate v1.4.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/http/get.go b/http/get.go new file mode 100644 index 0000000..459d18f --- /dev/null +++ b/http/get.go @@ -0,0 +1,36 @@ +package http + +import ( + "github.com/valyala/fasthttp" + "toast.cafe/x/brpaste/v2/storage" + "toast.cafe/x/brpaste/v2/template" +) + +func Get(store storage.CHR) handler { + return func(ctx *fasthttp.RequestCtx) { + ukey := ctx.UserValue("key") + ulang := ctx.UserValue("lang") + + var key, lang string + key = ukey.(string) // there's no recovering otherwise + if ulang != nil { + lang = ulang.(string) + } + + res, err := store.Read(key) + switch err { + case storage.Unhealthy: + ctx.Error("Backend did not respond", fasthttp.StatusInternalServerError) + case nil: // all good + if lang == "raw" { + ctx.SuccessString("text/plain", res) + } else { + //b := new(bytes.Buffer) + //template.WriteCode(b, lang, res) + ctx.SuccessString("text/html", template.Code(lang, res)) // render template + } + default: + ctx.NotFound() + } + } +} diff --git a/http/index.go b/http/index.go new file mode 100644 index 0000000..4bb9567 --- /dev/null +++ b/http/index.go @@ -0,0 +1,12 @@ +package http + +import ( + "github.com/valyala/fasthttp" + "toast.cafe/x/brpaste/v2/template" +) + +func Index(ctx *fasthttp.RequestCtx) { + //b := new(bytes.Buffer) + //template.Index(b) + ctx.SuccessString("text/html", template.Index()) // render template +} diff --git a/http/put.go b/http/put.go new file mode 100644 index 0000000..bc49e74 --- /dev/null +++ b/http/put.go @@ -0,0 +1,50 @@ +package http + +import ( + "encoding/base64" + "fmt" + + "github.com/twmb/murmur3" + "github.com/valyala/fasthttp" + "toast.cafe/x/brpaste/v2/storage" +) + +func Put(store storage.CHR, put bool) handler { + return func(ctx *fasthttp.RequestCtx) { + data := ctx.FormValue("data") + if len(data) == 0 { // works with nil + ctx.Error("Missing data field", fasthttp.StatusBadRequest) + return + } + + ukey := ctx.UserValue("key") + var key string + if ukey != nil { + key = ukey.(string) + } else { + hasher := murmur3.New32() + hasher.Write(data) + keybuf := hasher.Sum(nil) + key = base64.RawURLEncoding.EncodeToString(keybuf) + } + val := string(data) + + err := store.Create(key, val, put) + + switch err { + case storage.Collision: + ctx.Error("Collision detected when undesired", fasthttp.StatusConflict) + case storage.Unhealthy: + ctx.Error("Backend did not respond", fasthttp.StatusInternalServerError) + case nil: // everything succeeded + if isBrowser(string(ctx.UserAgent())) { + ctx.Redirect(fmt.Sprintf("/%s", key), fasthttp.StatusSeeOther) + } else { + ctx.SetStatusCode(fasthttp.StatusCreated) + ctx.SetBodyString(key) + } + default: + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + } + } +} diff --git a/http/router.go b/http/router.go new file mode 100644 index 0000000..f1a0b47 --- /dev/null +++ b/http/router.go @@ -0,0 +1,23 @@ +package http + +import ( + "github.com/fasthttp/router" + "github.com/valyala/fasthttp" + "toast.cafe/x/brpaste/v2/storage" +) + +// GenHandler generates the brpaste handler +func GenHandler(store storage.CHR) func(ctx *fasthttp.RequestCtx) { + get := Get(store) + post := Put(store, false) + put := Put(store, true) + + r := router.New() + r.GET("/", Index) + r.GET("/:key", get) + r.GET("/:key/:lang", get) + r.POST("/", post) + r.PUT("/:key", put) + + return r.Handler +} diff --git a/http/type.go b/http/type.go new file mode 100644 index 0000000..22aab0d --- /dev/null +++ b/http/type.go @@ -0,0 +1,5 @@ +package http + +import "github.com/valyala/fasthttp" + +type handler = fasthttp.RequestHandler diff --git a/http/ua_detector.go b/http/ua_detector.go new file mode 100644 index 0000000..aa25025 --- /dev/null +++ b/http/ua_detector.go @@ -0,0 +1,23 @@ +package http + +import "strings" + +var ( + browsers = []string{ + "Firefox/", + "Chrome/", + "Safari/", + "OPR/", + "Edge/", + "Trident/", + } +) + +func isBrowser(ua string) bool { + for _, el := range browsers { + if strings.Contains(ua, el) { + return true + } + } + return false +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..adce93e --- /dev/null +++ b/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/go-redis/redis/v7" + "github.com/valyala/fasthttp" + "toast.cafe/x/brpaste/v2/http" + "toast.cafe/x/brpaste/v2/storage" +) + +var S settings + +type settings struct { + Bind string + Redis string +} + +func main() { + // ---- Flags + flag.StringVar(&S.Bind, "bind", ":8080", "address to bind to") + flag.StringVar(&S.Redis, "redis", "redis://localhost:6379", "redis connection string") + flag.Parse() + + // ---- Storage system + redisOpts, err := redis.ParseURL(S.Redis) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not parse redis connection string %s\n", S.Redis) + os.Exit(1) + } + client := redis.NewClient(redisOpts) + storage := (*storage.Redis)(client) + + // ---- Is storage healthy? + if !storage.Healthy() { + fmt.Fprintf(os.Stderr, "Storage is unhealthy, cannot proceed.\n") + os.Exit(1) + } + + // ---- Start! + handler := http.GenHandler(storage) + fasthttp.ListenAndServe(S.Bind, handler) +} diff --git a/storage/redis.go b/storage/redis.go new file mode 100644 index 0000000..865c0bb --- /dev/null +++ b/storage/redis.go @@ -0,0 +1,40 @@ +package storage + +import "github.com/go-redis/redis/v7" + +// Redis storage engine +type Redis redis.Client + +// Create an entry in redis +func (r *Redis) Create(key, value string, checkcollision bool) error { + if !r.Healthy() { + return Unhealthy + } + if checkcollision { + col, err := r.Exists(key).Result() + if err != nil { + return Unhealthy + } + if col > 0 { + return Collision + } + } + _, err := r.Set(key, value, 0).Result() + return err +} + +func (r *Redis) Read(key string) (string, error) { + if !r.Healthy() { + return "", Unhealthy + } + return r.Get(key).Result() +} + +// Healthy determines whether redis is responding to pings +func (r *Redis) Healthy() bool { + _, err := r.Ping().Result() + if err != nil { + return false + } + return true +} diff --git a/storage/type.go b/storage/type.go new file mode 100644 index 0000000..4f78a46 --- /dev/null +++ b/storage/type.go @@ -0,0 +1,20 @@ +package storage + +const ( + // Collision is a fatal collision error, only triggered when it fails + Collision = Error("collision detected") + // Unhealthy is a fatal error when the backend ceases to be healthy + Unhealthy = Error("backend unhealthy") +) + +// Error is a sentinel error type for storage engines +type Error string + +func (e Error) Error() string { return string(e) } + +// CHR - Create, Health, Read +type CHR interface { + Create(key, value string, checkcollision bool) error + Healthy() bool + Read(key string) (string, error) +} diff --git a/template/code.qtpl b/template/code.qtpl new file mode 100644 index 0000000..426da8e --- /dev/null +++ b/template/code.qtpl @@ -0,0 +1,28 @@ +The code layout. +{% func Code(lang, data string) %} + {%= layout(" ", code_scripts(lang), "", code_contents(lang, data), code_bodyscripts(lang)) %} +{% endfunc %} + +{% code + const prefix = "https://unpkg.com/prismjs" +%} + +The code scripts. +{% func code_scripts(lang string) %} + +{% endfunc %} + +The code bodyscripts. +{% func code_bodyscripts(lang string) %} + {% stripspace %} + + {% if lang != "" && lang != "none" %} + + {% endif %} + {% endstripspace %} +{% endfunc %} + +The code contents. +{% func code_contents(lang, data string) %} +
{%s data %}
+{% endfunc %} diff --git a/template/index.qtpl b/template/index.qtpl new file mode 100644 index 0000000..b3b9721 --- /dev/null +++ b/template/index.qtpl @@ -0,0 +1,58 @@ +Index's contents. +{% func index_contents() %} + {% stripspace %} +

Burning Rubber Paste

+

Usage

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Method - EndpointEffect
POST / data=foo
Pastebin foo
PUT /id data=foo
Write foo into /id. Collisions disallowed. If a POST id coincides with your PUT content, it will be overwritten.
GET /id
Read paste with ID "id"
GET /id/raw
Get the raw contents of paste with ID "id"
GET /id/lang
Read paste with ID "id", and highlight it as "lang"
+ +

Examples

+ {% endstripspace %} +
http -f https://brpaste.example.com data=@file.txt
+http -f https://brpaste.example.com data=abcd
+http -f PUT https://brpaste.example.com/myPaste data=contents
+http https://brpaste.example.com/some_id/raw
+xdg-open https://brpaste.example.com/some_id/cpp
+ {% stripspace %} + +

Paste from a browser

+
+ +
+ +
+ {% endstripspace %} +{% endfunc %} + +The index layout. +{% func Index() %} + {%= layout("", "", "", index_contents(), "") %} +{% endfunc %} diff --git a/template/layout.qtpl b/template/layout.qtpl new file mode 100644 index 0000000..ffb31d2 --- /dev/null +++ b/template/layout.qtpl @@ -0,0 +1,49 @@ +The main layout function. +{% func layout(css, scripts, title, contents, bodyscripts string) %} + + {% stripspace %} + + + + + + {% if len(css) == 0 %} + + {% else %} + {%s= css %} + {% endif %} + + {% if len(scripts) != 0 %} + {%s= scripts %} + {% endif %} + + {% if len(title) == 0 %} + Burning Rubber Paste + {% else %} + {%s= title %} + {% endif %} + + +
+ {% if len(contents) != 0 %} + {%s= contents %} + {% endif %} +
+ {% if len(bodyscripts) != 0 %} + {%s= bodyscripts %} + {% endif %} + + + {% endstripspace %} +{% endfunc %} -- cgit v1.2.3