switch local rest middlewares to go-pkgz/rest
This commit is contained in:
Generated
+10
-5
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()))
|
||||
|
||||
+13
@@ -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
|
||||
+18
@@ -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
|
||||
Generated
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
||||
|
||||
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.
|
||||
-20
@@ -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.
|
||||
-20
@@ -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.
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
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.
|
||||
Generated
Vendored
-27
@@ -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.
|
||||
Generated
Vendored
-22
@@ -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.
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
## REST helpers and middleware [](https://travis-ci.org/go-pkgz/rest) [](https://goreportcard.com/report/github.com/go-pkgz/rest) [](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.
|
||||
+34
@@ -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)
|
||||
}
|
||||
}
|
||||
+9
@@ -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
|
||||
)
|
||||
+10
@@ -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=
|
||||
+39
@@ -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)
|
||||
}
|
||||
+192
@@ -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")
|
||||
}
|
||||
+45
@@ -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
|
||||
}
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
+58
@@ -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
|
||||
}
|
||||
+54
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user