diff options
| author | 2019-11-23 20:11:10 -0500 | |
|---|---|---|
| committer | 2019-11-23 20:11:10 -0500 | |
| commit | 79707242bd59cf4deb7f999a7a953275b323bb3b (patch) | |
| tree | 491c76768efd849aa7ebc0cb7601ff8561a1e60b | |
| parent | Remove D stuff from gitignore (diff) | |
Initial golang rewrite
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | go.mod | 13 | ||||
| -rw-r--r-- | go.sum | 61 | ||||
| -rw-r--r-- | http/get.go | 36 | ||||
| -rw-r--r-- | http/index.go | 12 | ||||
| -rw-r--r-- | http/put.go | 50 | ||||
| -rw-r--r-- | http/router.go | 23 | ||||
| -rw-r--r-- | http/type.go | 5 | ||||
| -rw-r--r-- | http/ua_detector.go | 23 | ||||
| -rw-r--r-- | main.go | 45 | ||||
| -rw-r--r-- | storage/redis.go | 40 | ||||
| -rw-r--r-- | storage/type.go | 20 | ||||
| -rw-r--r-- | template/code.qtpl | 28 | ||||
| -rw-r--r-- | template/index.qtpl | 58 | ||||
| -rw-r--r-- | template/layout.qtpl | 49 |
15 files changed, 466 insertions, 0 deletions
@@ -1,2 +1,5 @@ /doc/brpaste.1 /brpaste + +# must be added manually for releases +/template/*.go @@ -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 +) @@ -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 +} @@ -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) %} + <link rel='stylesheet' crossorigin='anonymous' href='{%s prefix %}/themes/prism.css' /> +{% endfunc %} + +The code bodyscripts. +{% func code_bodyscripts(lang string) %} + {% stripspace %} + <script src='{%s prefix %}/prism.js'></script> + {% if lang != "" && lang != "none" %} + <script src='{%s prefix %}/components/prism-{%s lang %}.js'></script> + {% endif %} + {% endstripspace %} +{% endfunc %} + +The code contents. +{% func code_contents(lang, data string) %} + <pre><code class='language-{%s lang %}'>{%s data %}</code></pre> +{% 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 %} + <h1>Burning Rubber Paste</h1> + <h2>Usage</h2> + <table border=1> + <thead> + <tr> + <th>Method - Endpoint</th> + <th>Effect</th> + </tr> + </thead> + <tbody> + <tr> + <td><pre><code>POST / data=foo</code></pre></td> + <td>Pastebin foo</td> + </tr> + <tr> + <td><pre><code>PUT /id data=foo</code></pre></td> + <td>Write foo into /id. Collisions disallowed. If a POST id coincides with your PUT content, it will be overwritten.</td> + </tr> + <tr> + <td><pre><code>GET /id</code></pre></td> + <td>Read paste with ID "id"</td> + </tr> + <tr> + <td><pre><code>GET /id/raw</code></pre></td> + <td>Get the raw contents of paste with ID "id"</td> + </tr> + <tr> + <td><pre><code>GET /id/lang</code></pre></td> + <td>Read paste with ID "id", and highlight it as "lang"</td> + </tr> + </tbody> + </table> + + <h2>Examples</h2> + {% endstripspace %} + <pre><code class='language-sh'>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</code></pre> + {% stripspace %} + + <h2>Paste from a browser</h2> + <form action='/' method='post'> + <textarea name='data' autocomplete='off' required autofocus cols='80' rows='27'></textarea> + <br> + <button type='submit'>Paste it!</button> + </form> + {% 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) %} + <!DOCTYPE html> + {% stripspace %} + <html lang='en'> + <head> + <meta charset='utf-8' /> + <meta name='viewport' content='width=device-width, initial-scale=1' /> + + {% if len(css) == 0 %} + <style> + body { + margin: 40px auto; + max-width: 650px; + line-height: 1.6; + font-size: 18px; + color: #444; + padding: 0 10px; + } + h1, h2, h3 { line-length: 1.2; } + td { text-align: left; } + </style> + {% else %} + {%s= css %} + {% endif %} + + {% if len(scripts) != 0 %} + {%s= scripts %} + {% endif %} + + {% if len(title) == 0 %} + <title>Burning Rubber Paste</title> + {% else %} + {%s= title %} + {% endif %} + </head> + <body> + <div id='main'> + {% if len(contents) != 0 %} + {%s= contents %} + {% endif %} + </div> + {% if len(bodyscripts) != 0 %} + {%s= bodyscripts %} + {% endif %} + </body> + </html> + {% endstripspace %} +{% endfunc %} |
