From 440c7abfed1d366b88e3463d6120770814dc552a Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 24 Dec 2018 15:34:24 -0600 Subject: [PATCH] switch local rest middlewares to go-pkgz/rest --- backend/Gopkg.lock | 15 +- backend/app/rest/api/admin.go | 15 +- backend/app/rest/api/admin_test.go | 11 +- backend/app/rest/api/middleware.go | 190 ----------------- backend/app/rest/api/middleware_test.go | 84 -------- backend/app/rest/api/migrator.go | 9 +- backend/app/rest/api/migrator_test.go | 6 +- backend/app/rest/api/rest.go | 36 +--- backend/app/rest/api/rest_private.go | 9 +- backend/app/rest/api/rest_private_test.go | 9 +- backend/app/rest/api/rest_public.go | 37 +++- backend/app/rest/api/rest_public_test.go | 5 +- backend/app/rest/api/rest_test.go | 7 +- backend/app/rest/api/ssl.go | 5 +- .../vendor/github.com/go-pkgz/rest/.gitignore | 13 ++ .../github.com/go-pkgz/rest/.travis.yml | 18 ++ .../github.com/davecgh/go-spew/LICENSE | 15 -- .../.vendor/github.com/go-chi/chi/LICENSE | 20 -- .../.vendor/github.com/go-chi/render/LICENSE | 20 -- .../.vendor/github.com/pkg/errors/LICENSE | 23 --- .../github.com/pmezard/go-difflib/LICENSE | 27 --- .../github.com/stretchr/testify/LICENSE | 22 -- .../vendor/github.com/go-pkgz/rest/README.md | 71 +++++++ .../github.com/go-pkgz/rest/blackwords.go | 34 ++++ backend/vendor/github.com/go-pkgz/rest/go.mod | 9 + backend/vendor/github.com/go-pkgz/rest/go.sum | 10 + .../github.com/go-pkgz/rest/httperrors.go | 39 ++++ .../github.com/go-pkgz/rest/logger/logger.go | 192 ++++++++++++++++++ .../github.com/go-pkgz/rest/logger/options.go | 45 ++++ .../vendor/github.com/go-pkgz/rest/metrics.go | 30 +++ .../github.com/go-pkgz/rest/middleware.go | 58 ++++++ .../github.com/go-pkgz/rest/onlyfrom.go | 58 ++++++ .../vendor/github.com/go-pkgz/rest/rest.go | 54 +++++ 33 files changed, 721 insertions(+), 475 deletions(-) delete mode 100644 backend/app/rest/api/middleware.go delete mode 100644 backend/app/rest/api/middleware_test.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/.gitignore create mode 100644 backend/vendor/github.com/go-pkgz/rest/.travis.yml delete mode 100644 backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/davecgh/go-spew/LICENSE delete mode 100644 backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/chi/LICENSE delete mode 100644 backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/render/LICENSE delete mode 100644 backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pkg/errors/LICENSE delete mode 100644 backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pmezard/go-difflib/LICENSE delete mode 100644 backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/stretchr/testify/LICENSE create mode 100644 backend/vendor/github.com/go-pkgz/rest/README.md create mode 100644 backend/vendor/github.com/go-pkgz/rest/blackwords.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/go.mod create mode 100644 backend/vendor/github.com/go-pkgz/rest/go.sum create mode 100644 backend/vendor/github.com/go-pkgz/rest/httperrors.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/logger/logger.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/logger/options.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/metrics.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/middleware.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/onlyfrom.go create mode 100644 backend/vendor/github.com/go-pkgz/rest/rest.go diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 995c89c0..add74efb 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -131,12 +131,16 @@ version = "v1.0.0" [[projects]] - branch = "master" - digest = "1:c6b263f17e06fcc612b40d1a4ca6d29588ae536170810bddd78227b5ea21f1f1" + digest = "1:71dc1e5b19e179495d2e2ca63454a9204753c5ecb3faa4a842ea5859355a968f" name = "github.com/go-pkgz/rest" - packages = ["cache"] + packages = [ + ".", + "cache", + "logger", + ] pruneopts = "UT" - revision = "88a256cf379018b68f9ef9a35c79fd336ae503fe" + revision = "c0e09a7a640e54001aed8bad117d60ad8971958e" + version = "v1.1.1" [[projects]] digest = "1:ffc060c551980d37ee9e428ef528ee2813137249ccebb0bfc412ef83071cac91" @@ -384,11 +388,12 @@ "github.com/go-chi/render", "github.com/go-pkgz/mongo", "github.com/go-pkgz/repeater", + "github.com/go-pkgz/rest", "github.com/go-pkgz/rest/cache", + "github.com/go-pkgz/rest/logger", "github.com/google/uuid", "github.com/gorilla/feeds", "github.com/hashicorp/go-multierror", - "github.com/hashicorp/golang-lru", "github.com/hashicorp/logutils", "github.com/jessevdk/go-flags", "github.com/microcosm-cc/bluemonday", diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index 5ee0dad6..d2be7c9e 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -9,6 +9,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/umputun/remark/backend/app/rest" @@ -60,7 +61,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { } a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, lastCommentsScope)) render.Status(r, http.StatusOK) - render.JSON(w, r, JSON{"id": id, "locator": locator}) + render.JSON(w, r, R.JSON{"id": id, "locator": locator}) } // DELETE /user/{userid}?site=side-id - delete all user comments for requested userid @@ -76,7 +77,7 @@ func (a *admin) deleteUserCtrl(w http.ResponseWriter, r *http.Request) { } a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope)) render.Status(r, http.StatusOK) - render.JSON(w, r, JSON{"user_id": userID, "site_id": siteID}) + render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID}) } // GET /user/{userid}?site=side-id - get user info for requested userid @@ -129,7 +130,7 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { a.cache.Flush(cache.Flusher(claims.SiteID).Scopes(claims.SiteID, claims.User.ID, lastCommentsScope)) render.Status(r, http.StatusOK) - render.JSON(w, r, JSON{"user_id": claims.User.ID, "site_id": claims.SiteID}) + render.JSON(w, r, R.JSON{"user_id": claims.User.ID, "site_id": claims.SiteID}) } // PUT /user/{userid}?site=side-id&block=1&ttl=7d - block or unblock user @@ -150,7 +151,7 @@ func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope)) - render.JSON(w, r, JSON{"user_id": userID, "site_id": siteID, "block": blockStatus}) + render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID, "block": blockStatus}) } // GET /blocked?site=siteID - list blocked users @@ -187,7 +188,7 @@ func (a *admin) setReadOnlyCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, locator.SiteID)) - render.JSON(w, r, JSON{"locator": locator, "read-only": roStatus}) + render.JSON(w, r, R.JSON{"locator": locator, "read-only": roStatus}) } // PUT /verify?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post @@ -201,7 +202,7 @@ func (a *admin) setVerifyCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(siteID).Scopes(siteID, userID)) - render.JSON(w, r, JSON{"user": userID, "verified": verifyStatus}) + render.JSON(w, r, R.JSON{"user": userID, "verified": verifyStatus}) } // PUT /pin/{id}?site=siteID&url=post-url&pin=1 @@ -216,7 +217,7 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL)) - render.JSON(w, r, JSON{"id": commentID, "locator": locator, "pin": pinStatus}) + render.JSON(w, r, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus}) } func (a *admin) checkBlocked(siteID string, user store.User) bool { diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index 8258cadc..2652ccb4 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -12,7 +12,8 @@ import ( "testing" "time" - jwt "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" + R "github.com/go-pkgz/rest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -177,14 +178,14 @@ func TestAdmin_Block(t *testing.T) { require.Nil(t, e) body, e = ioutil.ReadAll(resp.Body) assert.Nil(t, e) - resp.Body.Close() + require.Nil(t, resp.Body.Close()) return resp.StatusCode, body } // block permanently code, body := block(1, "") require.Equal(t, 200, code) - j := JSON{} + j := R.JSON{} err = json.Unmarshal(body, &j) assert.Nil(t, err) assert.Equal(t, "user1", j["user_id"]) @@ -528,8 +529,8 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { claims.Flags.DeleteMe = true _ = os.MkdirAll("/tmp/42", 0700) - defer os.RemoveAll("/tmp/42") - ioutil.WriteFile("/tmp/42/pic.image", []byte("some image data"), 0600) + defer func(){_ = os.RemoveAll("/tmp/42")}() + require.NoError(t,ioutil.WriteFile("/tmp/42/pic.image", []byte("some image data"), 0600)) token, err := srv.Authenticator.JWTService.Token(&claims) assert.Nil(t, err) diff --git a/backend/app/rest/api/middleware.go b/backend/app/rest/api/middleware.go deleted file mode 100644 index f73f48ab..00000000 --- a/backend/app/rest/api/middleware.go +++ /dev/null @@ -1,190 +0,0 @@ -package api - -import ( - "bytes" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "regexp" - "runtime/debug" - "strings" - "time" - - "github.com/go-chi/chi/middleware" - - "github.com/umputun/remark/backend/app/rest" -) - -// JSON is a map alias, just for convenience -type JSON map[string]interface{} - -// AppInfo adds custom app-info to the response header -func AppInfo(app string, version string) func(http.Handler) http.Handler { - f := func(h http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Org", "Umputun") - w.Header().Set("App-Name", app) - w.Header().Set("App-Version", version) - if mhost := os.Getenv("MHOST"); mhost != "" { - w.Header().Set("Host", mhost) - } - h.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) - } - return f -} - -// Ping middleware response with pong to /ping. Stops chain if ping request detected -func Ping(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - - if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") { - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("pong")); err != nil { - log.Printf("[WARN] can't send pong, %s", err) - } - return - } - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) -} - -// Recoverer is a middleware that recovers from panics, logs the panic and returns a HTTP 500 status if possible. -func Recoverer(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rvr := recover(); rvr != nil { - log.Printf("[WARN] request panic, %v", rvr) - debug.PrintStack() - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }() - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) -} - -// LoggerFlag type -type LoggerFlag int - -// logger flags enum -const ( - LogAll LoggerFlag = iota - LogUser - LogBody - LogNone -) -const maxBody = 1024 - -var reMultWhtsp = regexp.MustCompile(`[\s\p{Zs}]{2,}`) - -// Logger middleware prints http log. Customized by set of LoggerFlag -func Logger(ipFn func(ip string) string, flags ...LoggerFlag) func(http.Handler) http.Handler { - - f := func(h http.Handler) http.Handler { - - fn := func(w http.ResponseWriter, r *http.Request) { - - if inLogFlags(LogNone, flags) { // skip logging - h.ServeHTTP(w, r) - return - } - - ww := middleware.NewWrapResponseWriter(w, 1) - body, user := getBodyAndUser(r, flags) - t1 := time.Now() - defer func() { - t2 := time.Now() - - q := r.URL.String() - if qun, err := url.QueryUnescape(q); err == nil { - q = qun - } - q = sanitizeQuery(q) - - remoteIP := strings.Split(r.RemoteAddr, ":")[0] - if strings.HasPrefix(r.RemoteAddr, "[") { - remoteIP = strings.Split(r.RemoteAddr, "]:")[0] + "]" - } - if ipFn != nil { - remoteIP = ipFn(remoteIP) - } - - log.Printf("[INFO] REST %s - %s - %s - %d (%d) - %v %s %s", - r.Method, q, remoteIP, ww.Status(), ww.BytesWritten(), t2.Sub(t1), user, body) - }() - - h.ServeHTTP(ww, r) - } - return http.HandlerFunc(fn) - } - - return f -} - -func getBodyAndUser(r *http.Request, flags []LoggerFlag) (body string, user string) { - ctx := r.Context() - if ctx == nil { - return "", "" - } - - if inLogFlags(LogBody, flags) { - if content, err := ioutil.ReadAll(r.Body); err == nil { - body = string(content) - r.Body = ioutil.NopCloser(bytes.NewReader(content)) - - if len(body) > 0 { - body = strings.Replace(body, "\n", " ", -1) - body = reMultWhtsp.ReplaceAllString(body, " ") - } - - if len(body) > maxBody { - body = body[:maxBody] + "..." - } - } - } - - if inLogFlags(LogUser, flags) { - u, err := rest.GetUserInfo(r) - if err == nil && u.Name != "" { - user = fmt.Sprintf(" - %s %q", u.ID, u.Name) - } - } - - return body, user -} - -func sanitizeQuery(u string) string { - out := []rune(u) - hide := []string{"password", "passwd", "secret", "credentials"} - for _, h := range hide { - if strings.Contains(strings.ToLower(u), h+"=") { - stPos := strings.Index(strings.ToLower(u), h+"=") + len(h) + 1 - fnPos := strings.Index(u[stPos:], "&") - if fnPos == -1 { - fnPos = len(u) - } else { - fnPos = stPos + fnPos - } - for i := stPos; i < fnPos; i++ { - out[i] = rune('*') - } - } - } - return string(out) -} - -func inLogFlags(f LoggerFlag, flags []LoggerFlag) bool { - for _, flg := range flags { - if (flg == LogAll && f != LogNone) || flg == f { - return true - } - } - return false -} diff --git a/backend/app/rest/api/middleware_test.go b/backend/app/rest/api/middleware_test.go deleted file mode 100644 index a91f00ff..00000000 --- a/backend/app/rest/api/middleware_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package api - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/go-chi/chi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/umputun/remark/backend/app/rest" - "github.com/umputun/remark/backend/app/store" -) - -func TestMiddleware_AppInfo(t *testing.T) { - router := chi.NewRouter() - router.With(AppInfo("remark42", "12345")).Get("/blah", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write([]byte("blah blah")) - }) - ts := httptest.NewServer(router) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/blah") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - defer resp.Body.Close() - - b, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - - assert.Equal(t, "blah blah", string(b)) - assert.Equal(t, "remark42", resp.Header.Get("App-Name")) - assert.Equal(t, "12345", resp.Header.Get("App-Version")) - assert.Equal(t, "Umputun", resp.Header.Get("Org")) -} - -func TestMiddleware_GetBodyAndUser(t *testing.T) { - req, err := http.NewRequest("GET", "http://example.com/request", strings.NewReader("body1\nbody2")) - require.Nil(t, err) - - body, user := getBodyAndUser(req, []LoggerFlag{LogAll}) - assert.Equal(t, "body1 body2", body) - assert.Equal(t, "", user, "no user") - - b, err := ioutil.ReadAll(req.Body) - assert.NoError(t, err) - assert.Equal(t, "body1\nbody2", string(b)) - - req = rest.SetUserInfo(req, store.User{ID: "id1", Name: "user1"}) - _, user = getBodyAndUser(req, []LoggerFlag{LogAll}) - assert.Equal(t, ` - id1 "user1"`, user, "no user") - - body, user = getBodyAndUser(req, nil) - assert.Equal(t, "", body) - assert.Equal(t, "", user, "no user") - - body, user = getBodyAndUser(req, []LoggerFlag{LogNone}) - assert.Equal(t, "", body) - assert.Equal(t, "", user, "no user") - - body, user = getBodyAndUser(req, []LoggerFlag{LogUser}) - assert.Equal(t, "", body) - assert.Equal(t, ` - id1 "user1"`, user, "no user") -} - -func TestMiddleware_sanitizeReqURL(t *testing.T) { - tbl := []struct { - in string - out string - }{ - {"", ""}, - {"/aa/bb?xyz=123", "/aa/bb?xyz=123"}, - {"/aa/bb?xyz=123&secret=asdfghjk", "/aa/bb?xyz=123&secret=********"}, - {"/aa/bb?xyz=123&secret=asdfghjk&key=val", "/aa/bb?xyz=123&secret=********&key=val"}, - {"/aa/bb?xyz=123&secret=asdfghjk&key=val&password=1234", "/aa/bb?xyz=123&secret=********&key=val&password=****"}, - } - for i, tt := range tbl { - assert.Equal(t, tt.out, sanitizeQuery(tt.in), "check #%d, %s", i, tt.in) - } -} diff --git a/backend/app/rest/api/migrator.go b/backend/app/rest/api/migrator.go index 080e5b72..7dad7c04 100644 --- a/backend/app/rest/api/migrator.go +++ b/backend/app/rest/api/migrator.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/pkg/errors" @@ -68,7 +69,7 @@ func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) { go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site render.Status(r, http.StatusAccepted) - render.JSON(w, r, JSON{"status": "import request accepted"}) + render.JSON(w, r, R.JSON{"status": "import request accepted"}) } // POST /import/form?secret=key&site=site-id&provider=disqus|remark|wordpress @@ -102,7 +103,7 @@ func (m *Migrator) importFormCtrl(w http.ResponseWriter, r *http.Request) { go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site render.Status(r, http.StatusAccepted) - render.JSON(w, r, JSON{"status": "import request accepted"}) + render.JSON(w, r, R.JSON{"status": "import request accepted"}) } func (m *Migrator) importWaitCtrl(w http.ResponseWriter, r *http.Request) { @@ -123,13 +124,13 @@ func (m *Migrator) importWaitCtrl(w http.ResponseWriter, r *http.Request) { select { case <-ctx.Done(): render.Status(r, http.StatusGatewayTimeout) - render.JSON(w, r, JSON{"status": "timeout expired", "site_id": siteID}) + render.JSON(w, r, R.JSON{"status": "timeout expired", "site_id": siteID}) return case <-time.After(100 * time.Millisecond): } } render.Status(r, http.StatusOK) - render.JSON(w, r, JSON{"status": "completed", "site_id": siteID}) + render.JSON(w, r, R.JSON{"status": "completed", "site_id": siteID}) } // GET /export?site=site-id&secret=12345&?mode=file|stream diff --git a/backend/app/rest/api/migrator_test.go b/backend/app/rest/api/migrator_test.go index dced318c..f0f68988 100644 --- a/backend/app/rest/api/migrator_test.go +++ b/backend/app/rest/api/migrator_test.go @@ -15,7 +15,7 @@ import ( "testing" "time" - bolt "github.com/coreos/bbolt" + "github.com/coreos/bbolt" "github.com/go-chi/chi" "github.com/go-pkgz/rest/cache" "github.com/stretchr/testify/assert" @@ -71,7 +71,7 @@ func TestMigrator_ImportForm(t *testing.T) { _, err = io.Copy(fileWriter, r) require.NoError(t, err) contentType := bodyWriter.FormDataContentType() - bodyWriter.Close() + require.NoError(t, bodyWriter.Close()) resp, err := http.Post(ts.URL+"/import/form?site=radio-t&provider=native&secret=123456", contentType, bodyBuf) assert.Nil(t, err) @@ -285,7 +285,7 @@ func prepImportSrv(t *testing.T) (svc *Migrator, ds *service.DataStore, ts *http func cleanupImportSrv(_ *Migrator, ts *httptest.Server) { ts.Close() - os.Remove(testDb) + _ = os.Remove(testDb) } var xmlTestWP = ` diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 050755db..5544e500 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -19,7 +19,9 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/cors" "github.com/go-chi/render" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" + "github.com/go-pkgz/rest/logger" "github.com/pkg/errors" "github.com/rakyll/statik/fs" @@ -154,9 +156,9 @@ func (s *Rest) makeHTTPServer(port int, router http.Handler) *http.Server { func (s *Rest) routes() chi.Router { router := chi.NewRouter() - router.Use(middleware.RealIP, Recoverer) + router.Use(middleware.RealIP, R.Recoverer) router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) - router.Use(AppInfo("remark42", s.Version), Ping) + router.Use(R.AppInfo("remark42", "umputun", s.Version), R.Ping) s.adminService = admin{ dataService: s.DataService, @@ -181,7 +183,8 @@ func (s *Rest) routes() chi.Router { // auth routes for all providers router.Route("/auth", func(r chi.Router) { - r.Use(Logger(ipFn, LogAll), tollbooth_chi.LimitHandler(tollbooth.NewLimiter(5, nil))) + l := logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)) + r.Use(l.Handler, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(5, nil))) for _, provider := range s.Authenticator.Providers { r.Mount("/"+provider.Name, provider.Routes()) // mount auth providers as /auth/{name} } @@ -192,7 +195,7 @@ func (s *Rest) routes() chi.Router { }) avatarMiddlewares := []func(http.Handler) http.Handler{ - Logger(ipFn, LogNone), + logger.New(logger.Flags(logger.None)).Handler, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(100, nil)), } router.Mount(s.AvatarProxy.Routes(avatarMiddlewares...)) // mount avatars to /api/v1/avatar/{file.img} @@ -204,7 +207,7 @@ func (s *Rest) routes() chi.Router { // open routes rapi.Group(func(ropen chi.Router) { ropen.Use(s.Authenticator.Auth(false)) - ropen.Use(Logger(ipFn, LogAll)) + ropen.Use(logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)).Handler) ropen.Get("/find", s.findCommentsCtrl) ropen.Get("/id/{id}", s.commentByIDCtrl) ropen.Get("/comments", s.findUserCommentsCtrl) @@ -223,7 +226,7 @@ func (s *Rest) routes() chi.Router { // protected routes, require auth rapi.Group(func(rauth chi.Router) { rauth.Use(s.Authenticator.Auth(true)) - rauth.Use(Logger(ipFn, LogAll)) + rauth.Use(logger.New(logger.Flags(logger.All), logger.IPfn(ipFn)).Handler) rauth.Post("/comment", s.createCommentCtrl) rauth.Put("/comment/{id}", s.updateCommentCtrl) rauth.Get("/user", s.userInfoCtrl) @@ -297,16 +300,6 @@ func addFileServer(r chi.Router, path string, root http.FileSystem) { })) } -// renderJSONWithHTML allows html tags and forces charset=utf-8 -func renderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) { - data, err := encodeJSONWithHTML(v) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't render json response") - return - } - renderJSONFromBytes(w, r, data) -} - func encodeJSONWithHTML(v interface{}) ([]byte, error) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) @@ -317,17 +310,6 @@ func encodeJSONWithHTML(v interface{}) ([]byte, error) { return buf.Bytes(), nil } -// renderJSONWithHTML allows html tags and forces charset=utf-8 -func renderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if status, ok := r.Context().Value(render.StatusCtxKey).(int); ok { - w.WriteHeader(status) - } - if _, err := w.Write(data); err != nil { - log.Printf("[WARN] failed to send response to %s, %s", r.RemoteAddr, err) - } -} - func filterComments(comments []store.Comment, fn func(c store.Comment) bool) (filtered []store.Comment) { for _, c := range comments { if fn(c) { diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index a607f100..ceb1c4ff 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -10,11 +10,12 @@ import ( "strings" "time" - jwt "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" "github.com/go-chi/chi" "github.com/go-chi/render" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-multierror" "github.com/umputun/remark/backend/app/rest" "github.com/umputun/remark/backend/app/rest/auth" @@ -164,7 +165,7 @@ func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) { return } s.Cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, comment.User.ID)) - render.JSON(w, r, JSON{"id": comment.ID, "score": comment.Score}) + render.JSON(w, r, R.JSON{"id": comment.ID, "score": comment.Score}) } // GET /userdata?site=siteID - exports all data about the user as a json with user info and list of all comments @@ -248,7 +249,7 @@ func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) { } link := fmt.Sprintf("%s/web/deleteme.html?token=%s", s.RemarkURL, tokenStr) - render.JSON(w, r, JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link}) + render.JSON(w, r, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link}) } func (s *Rest) isReadOnly(locator store.Locator) bool { diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 1dd820a9..9f439580 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + R "github.com/go-pkgz/rest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,7 +29,7 @@ func TestRest_Create(t *testing.T) { assert.Nil(t, err) require.Equal(t, http.StatusCreated, resp.StatusCode, string(b)) - c := JSON{} + c := R.JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) loc := c["locator"].(map[string]interface{}) @@ -83,7 +84,7 @@ func TestRest_CreateTooBig(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) - c := JSON{} + c := R.JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) assert.Equal(t, "comment text exceeded max allowed size 4000 (4001)", c["error"]) @@ -95,7 +96,7 @@ func TestRest_CreateTooBig(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.StatusCode) b, err = ioutil.ReadAll(resp.Body) assert.Nil(t, err) - c = JSON{} + c = R.JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) assert.Equal(t, "http: request body too large", c["error"]) @@ -127,7 +128,7 @@ func TestRest_CreateAndGet(t *testing.T) { require.Equal(t, http.StatusCreated, resp.StatusCode) b, err := ioutil.ReadAll(resp.Body) assert.Nil(t, err) - c := JSON{} + c := R.JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index 45cfdc45..77644c3b 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -10,6 +10,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/render" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/umputun/remark/backend/app/rest" @@ -55,7 +56,10 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comments") return } - renderJSONFromBytes(w, r, data) + + if err = R.RenderJSONFromBytes(w, r, data); err != nil { + log.Printf("[WARN] can't render comments for post %+v",locator) + } } // POST /preview, body is a comment, returns rendered html @@ -101,7 +105,9 @@ func (s *Rest) infoCtrl(w http.ResponseWriter, r *http.Request) { return } - renderJSONFromBytes(w, r, data) + if err = R.RenderJSONFromBytes(w, r, data); err != nil { + log.Printf("[WARN] can't render info for post %+v",locator) + } } // GET /last/{limit}?site=siteID - last comments for the siteID, across all posts, sorted by time @@ -130,7 +136,10 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't get last comments") return } - renderJSONFromBytes(w, r, data) + + if err = R.RenderJSONFromBytes(w, r, data); err != nil { + log.Printf("[WARN] can't render last comments for site %s",siteID) + } } // GET /id/{id}?site=siteID&url=post-url - gets a comment by id @@ -149,7 +158,10 @@ func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { } comment = s.adminService.alterComments([]store.Comment{comment}, r)[0] render.Status(r, http.StatusOK) - renderJSONWithHTML(w, r, comment) + + if err = R.RenderJSONWithHTML(w, r, comment); err != nil { + log.Printf("[WARN] can't render last comments for url=%s, id=%s",url, id) + } } // GET /comments?site=siteID&user=id - returns comments for given userID @@ -190,7 +202,10 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by user id") return } - renderJSONFromBytes(w, r, data) + + if err = R.RenderJSONFromBytes(w, r, data); err != nil { + log.Printf("[WARN] can't render found comments for user %s", userID) + } } // GET /config?site=siteID - returns configuration @@ -240,7 +255,7 @@ func (s *Rest) countCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count") return } - render.JSON(w, r, JSON{"count": count, "locator": locator}) + render.JSON(w, r, R.JSON{"count": count, "locator": locator}) } // POST /count?site=siteID - get number of comments for posts from post body @@ -273,7 +288,10 @@ func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get counts for "+siteID) return } - renderJSONFromBytes(w, r, data) + + if err = R.RenderJSONFromBytes(w, r, data); err != nil { + log.Printf("[WARN] can't render comments counters site %s",siteID) + } } // GET /list?site=siteID&limit=50&skip=10 - list posts with comments @@ -302,5 +320,8 @@ func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of comments for "+siteID) return } - renderJSONFromBytes(w, r, data) + + if err = R.RenderJSONFromBytes(w, r, data); err != nil { + log.Printf("[WARN] can't render posts lits for site %s",siteID) + } } diff --git a/backend/app/rest/api/rest_public_test.go b/backend/app/rest/api/rest_public_test.go index a133cf1c..042ba13f 100644 --- a/backend/app/rest/api/rest_public_test.go +++ b/backend/app/rest/api/rest_public_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + R "github.com/go-pkgz/rest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -310,7 +311,7 @@ func TestRest_Count(t *testing.T) { body, code := get(t, ts.URL+"/api/v1/count?site=radio-t&url=https://radio-t.com/blah1") assert.Equal(t, 200, code) - j := JSON{} + j := R.JSON{} err := json.Unmarshal([]byte(body), &j) assert.Nil(t, err) assert.Equal(t, 3.0, j["count"]) @@ -386,7 +387,7 @@ func TestRest_Config(t *testing.T) { body, code := get(t, ts.URL+"/api/v1/config?site=radio-t") assert.Equal(t, 200, code) - j := JSON{} + j := R.JSON{} err := json.Unmarshal([]byte(body), &j) assert.Nil(t, err) assert.Equal(t, 300., j["edit_duration"]) diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index d59710a5..4586299d 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -12,7 +12,8 @@ import ( "testing" "time" - bolt "github.com/coreos/bbolt" + "github.com/coreos/bbolt" + R "github.com/go-pkgz/rest" "github.com/go-pkgz/rest/cache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -53,7 +54,7 @@ func TestRest_GetStarted(t *testing.T) { assert.Equal(t, 200, code) assert.Equal(t, "some html blah", body) - os.Remove(getStartedHTML) + _ = os.Remove(getStartedHTML) _, code = get(t, ts.URL+"/index.html") assert.Equal(t, 404, code) @@ -268,7 +269,7 @@ func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string { b, err = ioutil.ReadAll(resp.Body) assert.Nil(t, err) - crResp := JSON{} + crResp := R.JSON{} err = json.Unmarshal(b, &crResp) assert.Nil(t, err) time.Sleep(time.Nanosecond * 10) diff --git a/backend/app/rest/api/ssl.go b/backend/app/rest/api/ssl.go index 14a4b728..a52771b2 100644 --- a/backend/app/rest/api/ssl.go +++ b/backend/app/rest/api/ssl.go @@ -9,6 +9,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" + R "github.com/go-pkgz/rest" "golang.org/x/crypto/acme/autocert" ) @@ -41,7 +42,7 @@ type SSLConfig struct { func (s *Rest) httpToHTTPSRouter() chi.Router { log.Printf("[DEBUG] create https-to-http redirect routes") router := chi.NewRouter() - router.Use(middleware.RealIP, Recoverer) + router.Use(middleware.RealIP, R.Recoverer) router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) router.Handle("/*", s.redirectHandler()) @@ -55,7 +56,7 @@ func (s *Rest) httpToHTTPSRouter() chi.Router { func (s *Rest) httpChallengeRouter(m *autocert.Manager) chi.Router { log.Printf("[DEBUG] create http-challenge routes") router := chi.NewRouter() - router.Use(middleware.RealIP, Recoverer) + router.Use(middleware.RealIP, R.Recoverer) router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) router.Handle("/*", m.HTTPHandler(s.redirectHandler())) diff --git a/backend/vendor/github.com/go-pkgz/rest/.gitignore b/backend/vendor/github.com/go-pkgz/rest/.gitignore new file mode 100644 index 00000000..cdc02295 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/.gitignore @@ -0,0 +1,13 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +vendor \ No newline at end of file diff --git a/backend/vendor/github.com/go-pkgz/rest/.travis.yml b/backend/vendor/github.com/go-pkgz/rest/.travis.yml new file mode 100644 index 00000000..aba3d55f --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/.travis.yml @@ -0,0 +1,18 @@ +language: go + +go: + - "1.11.x" + +install: true + +before_install: + - go get github.com/mattn/goveralls + - go get gopkg.in/alecthomas/gometalinter.v2 + - $GOPATH/bin/gometalinter.v2 --install + +script: + - GO111MODULE=on go get ./... + - GO111MODULE=on go mod vendor + - GO111MODULE=on go test -v -mod=vendor ./... + - $GOPATH/bin/gometalinter.v2 --exclude=test --exclude=mock --exclude=vendor ./... + - $GOPATH/bin/goveralls -service=travis-ci diff --git a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/davecgh/go-spew/LICENSE b/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/davecgh/go-spew/LICENSE deleted file mode 100644 index c8364161..00000000 --- a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/davecgh/go-spew/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -ISC License - -Copyright (c) 2012-2016 Dave Collins - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/chi/LICENSE b/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/chi/LICENSE deleted file mode 100644 index d99f02ff..00000000 --- a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/chi/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/render/LICENSE b/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/render/LICENSE deleted file mode 100644 index 4344db78..00000000 --- a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/go-chi/render/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2016-Present https://github.com/go-chi authors - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pkg/errors/LICENSE b/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pkg/errors/LICENSE deleted file mode 100644 index 835ba3e7..00000000 --- a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pkg/errors/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2015, Dave Cheney -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pmezard/go-difflib/LICENSE b/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pmezard/go-difflib/LICENSE deleted file mode 100644 index c67dad61..00000000 --- a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/pmezard/go-difflib/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2013, Patrick Mezard -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - The names of its contributors may not be used to endorse or promote -products derived from this software without specific prior written -permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/stretchr/testify/LICENSE b/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/stretchr/testify/LICENSE deleted file mode 100644 index 473b670a..00000000 --- a/backend/vendor/github.com/go-pkgz/rest/.vendor/github.com/stretchr/testify/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell - -Please consider promoting this project if you find it useful. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT -OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/rest/README.md b/backend/vendor/github.com/go-pkgz/rest/README.md new file mode 100644 index 00000000..6e80e899 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/README.md @@ -0,0 +1,71 @@ +## REST helpers and middleware [![Build Status](https://travis-ci.org/go-pkgz/rest.svg?branch=master)](https://travis-ci.org/go-pkgz/rest) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/rest)](https://goreportcard.com/report/github.com/go-pkgz/rest) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/rest/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/rest?branch=master) + +## Install and update + +`go get -u github.com/go-pkgz/rest` + +## Middlewares + +### AppInfo middleware + +Adds info to every response header: +- App-Name - application name +- App-Version - application version +- Org - organization +- M-Host - host name from instance-level `$MHOST` env + +### Ping-Pong middleware + +Responds with `pong` on `GET /ping`. Also responds to anything with `/ping` suffix, like `/v2/ping` + +example for both: + +``` +> http GET https://remark42.radio-t.com/ping + +HTTP/1.1 200 OK +Date: Sun, 15 Jul 2018 19:40:31 GMT +Content-Type: text/plain +Content-Length: 4 +Connection: keep-alive +App-Name: remark42 +App-Version: master-ed92a0b-20180630-15:59:56 +Org: Umputun + +pong +``` + +### Logger middleware + +Logs all info about request, including user, method, status code, response size, url, elapsed time, request body (optional). +Can be customized by passing flags - LogNone, LogAll, LogUser and LogBody. Flags can be combined (provided multiple times) + +### Recoverer middleware + +Recoverer is a middleware that recovers from panics, logs the panic (and a backtrace), +and returns a HTTP 500 (Internal Server Error) status if possible. + +### OnlyFrom middleware + +OnlyFrom middleware allows access for limited list of source IPs. +Such IPs can be defined as complete ip (like 192.168.1.12), prefix (129.168.) or CIDR (192.168.0.0/16) + +### Metrics middleware + +Metrics middleware responds to GET /metrics with list of [expvar](https://golang.org/pkg/expvar/). Optionally allows to restrict list of source ips. + +### BlackWords middleware + +BlackWords middleware doesn't allow user-defined words in the request body. + +## Helpers + +- `rest.JSON` - map alias, just for convenience `type JSON map[string]interface{}` +- `rest.RenderJSON` - renders json response from `interface{}` +- `rest.RenderJSONFromBytes` - renders json response from `[]byte` +- `rest.RenderJSONWithHTML` - renders json response with html tags and forced `charset=utf-8` +- `rest.SendErrorJSON` - makes `{error: blah, details: blah}` json body and responds with given error code. Also adds context to logged message + +## Caching + +Cache wrapper provides loading cache for rest/http responses. See [cache readme](https://github.com/go-pkgz/rest/tree/master/cache) for more details and examples. diff --git a/backend/vendor/github.com/go-pkgz/rest/blackwords.go b/backend/vendor/github.com/go-pkgz/rest/blackwords.go new file mode 100644 index 00000000..65e764a7 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/blackwords.go @@ -0,0 +1,34 @@ +package rest + +import ( + "bytes" + "io/ioutil" + "net/http" + "strings" +) + +// BlackWords middleware doesn't allow some words in the request body +func BlackWords(words ...string) func(http.Handler) http.Handler { + + return func(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + if content, err := ioutil.ReadAll(r.Body); err == nil { + body := strings.ToLower(string(content)) + r.Body = ioutil.NopCloser(bytes.NewReader(content)) + + if len(body) > 0 { + for _, word := range words { + if strings.Contains(body, strings.ToLower(word)) { + w.WriteHeader(http.StatusForbidden) + RenderJSON(w, r, JSON{"error": "one of blacklisted words detected"}) + return + } + } + } + } + h.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } +} diff --git a/backend/vendor/github.com/go-pkgz/rest/go.mod b/backend/vendor/github.com/go-pkgz/rest/go.mod new file mode 100644 index 00000000..19894e8d --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/go.mod @@ -0,0 +1,9 @@ +module github.com/go-pkgz/rest + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/golang-lru v0.5.0 + github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 +) diff --git a/backend/vendor/github.com/go-pkgz/rest/go.sum b/backend/vendor/github.com/go-pkgz/rest/go.sum new file mode 100644 index 00000000..aa73c9e6 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/backend/vendor/github.com/go-pkgz/rest/httperrors.go b/backend/vendor/github.com/go-pkgz/rest/httperrors.go new file mode 100644 index 00000000..c9730ffc --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/httperrors.go @@ -0,0 +1,39 @@ +package rest + +import ( + "fmt" + "log" + "net/http" + "net/url" + "runtime" + "strings" +) + +// SendErrorJSON makes {error: blah, details: blah} json body and responds with error code +func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, details string) { + log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, details)) + w.WriteHeader(code) + RenderJSON(w, r, map[string]interface{}{"error": err.Error(), "details": details}) +} + +func errDetailsMsg(r *http.Request, code int, err error, details string) string { + + q := r.URL.String() + if qun, e := url.QueryUnescape(q); e == nil { + q = qun + } + + srcFileInfo := "" + if pc, file, line, ok := runtime.Caller(2); ok { + fnameElems := strings.Split(file, "/") + funcNameElems := strings.Split(runtime.FuncForPC(pc).Name(), "/") + srcFileInfo = fmt.Sprintf(" [caused by %s:%d %s]", strings.Join(fnameElems[len(fnameElems)-3:], "/"), + line, funcNameElems[len(funcNameElems)-1]) + } + + remoteIP := r.RemoteAddr + if pos := strings.Index(remoteIP, ":"); pos >= 0 { + remoteIP = remoteIP[:pos] + } + return fmt.Sprintf("%s - %v - %d - %s - %s%s", details, err, code, remoteIP, q, srcFileInfo) +} diff --git a/backend/vendor/github.com/go-pkgz/rest/logger/logger.go b/backend/vendor/github.com/go-pkgz/rest/logger/logger.go new file mode 100644 index 00000000..d3746101 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/logger/logger.go @@ -0,0 +1,192 @@ +package logger + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "regexp" + "strings" + "time" +) + +var reMultWhtsp = regexp.MustCompile(`[\s\p{Zs}]{2,}`) + +// Middleware for logging rest requests +type Middleware struct { + prefix string + maxBodySize int + flags []Flag + ipFn func(ip string) string + userFn func(r *http.Request) (string, error) +} + +// Flag type +type Flag int + +// logger flags enum +const ( + All Flag = iota + User + Body + None +) + +// New makes rest Logger with given options +func New(options ...Option) *Middleware { + res := Middleware{ + prefix: "", + maxBodySize: 1024, + flags: []Flag{All}, + } + for _, opt := range options { + opt(&res) + } + return &res +} + +// Handler middleware prints http log +func (l *Middleware) Handler(next http.Handler) http.Handler { + + fn := func(w http.ResponseWriter, r *http.Request) { + + if l.inLogFlags(None) { // skip logging + next.ServeHTTP(w, r) + return + } + + ww := newCustomResponseWriter(w) + body, user := l.getBodyAndUser(r) + t1 := time.Now() + defer func() { + t2 := time.Now() + + q := l.sanitizeQuery(r.URL.String()) + if qun, err := url.QueryUnescape(q); err == nil { + q = qun + } + + remoteIP := strings.Split(r.RemoteAddr, ":")[0] + if strings.HasPrefix(r.RemoteAddr, "[") { + remoteIP = strings.Split(r.RemoteAddr, "]:")[0] + "]" + } + + if l.ipFn != nil { // mask ip with ipFn + remoteIP = l.ipFn(remoteIP) + } + + log.Printf("%s %s - %s - %s - %d (%d) - %v %s %s", + l.prefix, r.Method, q, remoteIP, ww.status, ww.size, t2.Sub(t1), user, body) + }() + + next.ServeHTTP(ww, r) + } + return http.HandlerFunc(fn) +} + +func (l *Middleware) getBodyAndUser(r *http.Request) (body string, user string) { + ctx := r.Context() + if ctx == nil { + return "", "" + } + + if l.inLogFlags(Body) { + if content, err := ioutil.ReadAll(r.Body); err == nil { + body = string(content) + r.Body = ioutil.NopCloser(bytes.NewReader(content)) + + if len(body) > 0 { + body = strings.Replace(body, "\n", " ", -1) + body = reMultWhtsp.ReplaceAllString(body, " ") + } + + if len(body) > l.maxBodySize { + body = body[:l.maxBodySize] + "..." + } + } + } + + if l.inLogFlags(User) && l.userFn != nil { + u, err := l.userFn(r) + if err == nil && u != "" { + user = fmt.Sprintf(" - %s", u) + } + } + + return body, user +} + +func (l *Middleware) inLogFlags(f Flag) bool { + for _, flg := range l.flags { + if (flg == All && f != None) || flg == f { + return true + } + } + return false +} + +func (l *Middleware) sanitizeQuery(inp string) string { + out := []rune(inp) + hide := []string{"password", "passwd", "secret", "credentials"} + for _, h := range hide { + if strings.Contains(strings.ToLower(inp), h+"=") { + stPos := strings.Index(strings.ToLower(inp), h+"=") + len(h) + 1 + fnPos := strings.Index(inp[stPos:], "&") + if fnPos == -1 { + fnPos = len(inp) + } else { + fnPos = stPos + fnPos + } + for i := stPos; i < fnPos; i++ { + out[i] = rune('*') + } + } + } + return string(out) +} + +// customResponseWriter implements ResponseWriter and keeping status and size +type customResponseWriter struct { + http.ResponseWriter + status int + size int +} + +func newCustomResponseWriter(w http.ResponseWriter) *customResponseWriter { + return &customResponseWriter{ + ResponseWriter: w, + status: 200, + } +} + +// WriteHeader implements ResponseWriter and saves status +func (c *customResponseWriter) WriteHeader(status int) { + c.status = status + c.ResponseWriter.WriteHeader(status) +} + +// WriteHeader implements ResponseWriter and tracking size +func (c *customResponseWriter) Write(b []byte) (int, error) { + size, err := c.ResponseWriter.Write(b) + c.size += size + return size, err +} + +// Flush implements ResponseWriter +func (c *customResponseWriter) Flush() { + if f, ok := c.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +// Hijack implements ResponseWriter +func (c *customResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hj, ok := c.ResponseWriter.(http.Hijacker); ok { + return hj.Hijack() + } + return nil, nil, fmt.Errorf("ResponseWriter does not implement the Hijacker interface") +} diff --git a/backend/vendor/github.com/go-pkgz/rest/logger/options.go b/backend/vendor/github.com/go-pkgz/rest/logger/options.go new file mode 100644 index 00000000..99360763 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/logger/options.go @@ -0,0 +1,45 @@ +package logger + +import ( + "net/http" +) + +// Option func type +type Option func(l *Middleware) + +// Flags functional option defines output modes +func Flags(flags ...Flag) Option { + return func(l *Middleware) { + l.flags = flags + } +} + +// MaxBodySize functional option defines the largest body size to log. +func MaxBodySize(max int) Option { + return func(l *Middleware) { + if max >= 0 { + l.maxBodySize = max + } + } +} + +// Prefix functional option defines log line prefix. +func Prefix(prefix string) Option { + return func(l *Middleware) { + l.prefix = prefix + } +} + +// IPfn functional option defines ip masking function. +func IPfn(ipFn func(ip string) string) Option { + return func(l *Middleware) { + l.ipFn = ipFn + } +} + +// UserFn functional option defines user name function. +func UserFn(userFn func(r *http.Request) (string, error)) Option { + return func(l *Middleware) { + l.userFn = userFn + } +} diff --git a/backend/vendor/github.com/go-pkgz/rest/metrics.go b/backend/vendor/github.com/go-pkgz/rest/metrics.go new file mode 100644 index 00000000..2a57a939 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/metrics.go @@ -0,0 +1,30 @@ +package rest + +import ( + "expvar" + "fmt" + "net/http" + "strings" +) + +// Metrics responds to GET /metrics with list of expvar +func Metrics(onlyIps ...string) func(http.Handler) http.Handler { + + return func(h http.Handler) http.Handler { + + fn := func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/metrics") { + if matched, ip := matchSourceIP(r, onlyIps); !matched { + w.WriteHeader(http.StatusForbidden) + RenderJSON(w, r, JSON{"error": fmt.Sprintf("ip %s rejected", ip)}) + return + } + expvar.Handler().ServeHTTP(w, r) + return + } + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} diff --git a/backend/vendor/github.com/go-pkgz/rest/middleware.go b/backend/vendor/github.com/go-pkgz/rest/middleware.go new file mode 100644 index 00000000..d0dbbe9e --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/middleware.go @@ -0,0 +1,58 @@ +package rest + +import ( + "log" + "net/http" + "os" + "runtime/debug" + "strings" +) + +// AppInfo adds custom app-info to the response header +func AppInfo(app string, author string, version string) func(http.Handler) http.Handler { + f := func(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Author", author) + w.Header().Set("App-Name", app) + w.Header().Set("App-Version", version) + if mhost := os.Getenv("MHOST"); mhost != "" { + w.Header().Set("Host", mhost) + } + h.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } + return f +} + +// Ping middleware response with pong to /ping. Stops chain if ping request detected +func Ping(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte("pong")); err != nil { + log.Printf("[WARN] can't send pong, %s", err) + } + return + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +// Recoverer is a middleware that recovers from panics, logs the panic and returns a HTTP 500 status if possible. +func Recoverer(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rvr := recover(); rvr != nil { + log.Printf("[WARN] request panic, %v", rvr) + log.Print(string(debug.Stack())) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} diff --git a/backend/vendor/github.com/go-pkgz/rest/onlyfrom.go b/backend/vendor/github.com/go-pkgz/rest/onlyfrom.go new file mode 100644 index 00000000..1ca52f0b --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/onlyfrom.go @@ -0,0 +1,58 @@ +package rest + +import ( + "fmt" + "net" + "net/http" + "strings" +) + +// OnlyFrom middleware allows access for limited list of source IPs. +// Such IPs can be defined as complete ip (like 192.168.1.12), prefix (129.168.) or CIDR (192.168.0.0/16) +func OnlyFrom(onlyIps ...string) func(http.Handler) http.Handler { + + return func(h http.Handler) http.Handler { + + fn := func(w http.ResponseWriter, r *http.Request) { + + matched, ip := matchSourceIP(r, onlyIps) + if matched { + h.ServeHTTP(w, r) + return + } + + w.WriteHeader(http.StatusForbidden) + RenderJSON(w, r, JSON{"error": fmt.Sprintf("ip %s rejected", ip)}) + } + return http.HandlerFunc(fn) + } +} + +// matchSourceIP returns true if request's ip matches any of ips +func matchSourceIP(r *http.Request, ips []string) (bool, string) { + + // try X-Real-IP first then fail back to X-Forwarded-For and finally to RemoteAddr + ip := r.Header.Get("X-Real-IP") + if ip == "" { + ip = strings.Split(r.Header.Get("X-Forwarded-For"), ", ")[0] + } + if ip == "" { + ip = r.Header.Get("RemoteAddr") + } + if ip == "" { + ip = strings.Split(r.RemoteAddr, ":")[0] + } + + // check for ip prefix or CIDR + for _, exclIP := range ips { + if _, cidrnet, err := net.ParseCIDR(exclIP); err == nil { + if cidrnet.Contains(net.ParseIP(ip)) { + return true, ip + } + } + if strings.HasPrefix(ip, exclIP) { + return true, ip + } + } + return false, ip +} diff --git a/backend/vendor/github.com/go-pkgz/rest/rest.go b/backend/vendor/github.com/go-pkgz/rest/rest.go new file mode 100644 index 00000000..7cc8d216 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/rest/rest.go @@ -0,0 +1,54 @@ +package rest + +import ( + "bytes" + "encoding/json" + "net/http" + + "github.com/pkg/errors" +) + +// JSON is a map alias, just for convenience +type JSON map[string]interface{} + +// RenderJSON sends data as json +func RenderJSON(w http.ResponseWriter, r *http.Request, data interface{}) { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(true) + if err := enc.Encode(data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write(buf.Bytes()) // nolint: errcheck, gosec +} + +// RenderJSONFromBytes sends binary data as json +func RenderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err := w.Write(data); err != nil { + return errors.Wrapf(err, "failed to send response to %s", r.RemoteAddr) + } + return nil +} + +// RenderJSONWithHTML allows html tags and forces charset=utf-8 +func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) error { + + encodeJSONWithHTML := func(v interface{}) ([]byte, error) { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return nil, errors.Wrap(err, "json encoding failed") + } + return buf.Bytes(), nil + } + + data, err := encodeJSONWithHTML(v) + if err != nil { + return errors.Wrap(err, "json encoding failed") + } + return RenderJSONFromBytes(w, r, data) +}