integrate anonym auth and logger middleware

This commit is contained in:
Umputun
2017-12-25 14:49:13 -06:00
parent 5a664e95c1
commit f1fd6dba78
6 changed files with 198 additions and 33 deletions
+1
View File
@@ -11,6 +11,7 @@ import (
"github.com/umputun/remark/app/store"
)
// admin provides router for all requests available for admin only
type admin struct {
dataStore store.Interface
exporter migrator.Exporter
+10 -5
View File
@@ -64,7 +64,7 @@ func (p Provider) LoginHandler(w http.ResponseWriter, r *http.Request) {
state := randToken()
session, err := p.Get(r, "remark")
if err != nil {
log.Printf("[WARN] %s", err)
log.Printf("[DEBUG] can't get session, %s", err)
}
session.Values["state"] = state
@@ -80,12 +80,14 @@ func (p Provider) LoginHandler(w http.ResponseWriter, r *http.Request) {
}
// return login url
log.Printf("[DEBUG] login url %s", p.conf.AuthCodeURL(state))
http.Redirect(w, r, p.conf.AuthCodeURL(state), http.StatusTemporaryRedirect)
loginURL := p.conf.AuthCodeURL(state)
log.Printf("[DEBUG] login url %s", loginURL)
http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
}
// AuthHandler fills user info and redirects to "from" url
// AuthHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser
func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) {
session, err := p.Get(r, "remark")
if err != nil {
http.Error(w, fmt.Sprintf("failed to get session, %s", err), http.StatusInternalServerError)
@@ -140,7 +142,7 @@ func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] %+v", jData)
// redirect to back url if presented
// redirect to back url if presented in login query params
if fromURL, ok := session.Values["from"]; ok {
http.Redirect(w, r, fromURL.(string), http.StatusTemporaryRedirect)
return
@@ -157,7 +159,10 @@ func (p Provider) LogoutHandler(w http.ResponseWriter, r *http.Request) {
return
}
session.Values["uinfo"] = ""
session.Values["from"] = ""
session.Values["state"] = ""
delete(session.Values, "uinfo")
delete(session.Values, "from")
delete(session.Values, "state")
+122 -13
View File
@@ -1,15 +1,21 @@
package rest
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"regexp"
"runtime/debug"
"strings"
"time"
"github.com/didip/tollbooth"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
"github.com/go-errors/errors"
"github.com/gorilla/sessions"
@@ -110,13 +116,29 @@ func Recoverer(next http.Handler) http.Handler {
type contextKey string
const (
anonymous = iota
developer
full
)
// Auth adds auth from session and populate user info
func Auth(sessionStore *sessions.FilesystemStore, devMode bool, admins []string) func(http.Handler) http.Handler {
func Auth(sessionStore *sessions.FilesystemStore, admins []string, modes ...int) func(http.Handler) http.Handler {
inModes := func(mode int) bool {
for _, m := range modes {
if m == mode {
return true
}
}
return false
}
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// for dev mode skip all real auth, make dev admin user
if devMode {
if inModes(developer) {
user := store.User{
ID: "dev",
Name: "developer one",
@@ -138,22 +160,24 @@ func Auth(sessionStore *sessions.FilesystemStore, devMode bool, admins []string)
}
uinfoData, ok := session.Values["uinfo"]
if !ok {
if !ok && inModes(full) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
user := uinfoData.(store.User)
for _, admin := range admins {
if admin == user.ID {
user.Admin = true
break
if ok {
user := uinfoData.(store.User)
for _, admin := range admins {
if admin == user.ID {
user.Admin = true
break
}
}
ctx := r.Context()
ctx = context.WithValue(ctx, contextKey("user"), user)
r = r.WithContext(ctx)
}
ctx := r.Context()
ctx = context.WithValue(ctx, contextKey("user"), user)
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
@@ -195,3 +219,88 @@ func GetUserInfo(r *http.Request) (user store.User, err error) {
return store.User{}, errors.New("user can't be parsed")
}
// LoggerFlag type
type LoggerFlag int
// logger flags enum
const (
LogAll LoggerFlag = iota
LogUser
LogBody
)
const maxBody = 1024
var reMultWhtsp = regexp.MustCompile(`[\s\p{Zs}]{2,}`)
// Logger middleware prints http log. Customized by set of LoggerFlag
func Logger(flags ...LoggerFlag) func(http.Handler) http.Handler {
inFlags := func(f LoggerFlag) bool {
for _, flg := range flags {
if flg == LogAll || flg == f {
return true
}
}
return false
}
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, 1)
body, user := func() (body string, user string) {
ctx := r.Context()
if ctx == nil {
return "", ""
}
if inFlags(LogBody) {
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 inFlags(LogUser) {
u, err := GetUserInfo(r)
if err == nil && u.Name != "" {
user = fmt.Sprintf(" - %s %q", u.ID, u.Name)
}
}
return body, user
}()
t1 := time.Now()
defer func() {
t2 := time.Now()
q := r.URL.String()
if qun, err := url.QueryUnescape(q); err == nil {
q = qun
}
log.Printf("[INFO] REST %s%s - %s - %s - %d (%d) - %v %s",
r.Method, user, q, strings.Split(r.RemoteAddr, ":")[0],
ww.Status(), ww.BytesWritten(), t2.Sub(t1), body)
}()
h.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}
return f
}
+16 -7
View File
@@ -39,10 +39,19 @@ type Server struct {
func (s *Server) Run() {
log.Print("[INFO] activate rest server")
applyDevMode := func(mode int) (modes []int) {
modes = append(modes, mode)
if s.DevMode {
modes = append(modes, developer)
}
return modes
}
router := chi.NewRouter()
router.Use(middleware.RealIP, Recoverer)
router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second))
router.Use(Limiter(10), AppInfo("remark", s.Version), Ping)
router.Use(Auth(s.SessionStore, s.Admins, applyDevMode(anonymous)...))
router.Use(Limiter(10), AppInfo("remark", s.Version), Ping, Logger(LogAll))
router.Get("/login/google", s.AuthGoogle.LoginHandler)
router.Get("/auth/google", s.AuthGoogle.AuthHandler)
@@ -56,7 +65,7 @@ func (s *Server) Run() {
rapi.Get("/last/{max}", s.lastCommentsCtrl)
rapi.Get("/count", s.countCtrl)
rapi.With(Auth(s.SessionStore, s.DevMode, s.Admins)).Group(func(rauth chi.Router) {
rapi.With(Auth(s.SessionStore, s.Admins, applyDevMode(full)...)).Group(func(rauth chi.Router) {
rauth.Post("/comment", s.createCommentCtrl)
rauth.Get("/user", s.userInfoCtrl)
rauth.Put("/vote/{id}", s.voteCtrl)
@@ -112,7 +121,7 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
comment.User = user
comment.User.IP = strings.Split(r.RemoteAddr, ":")[0]
log.Printf("[INFO] create comment %+v", comment)
log.Printf("[DEBUG] create comment %+v", comment)
// check if user blocked
if s.mod.checkBlocked(store.Locator{}, comment.User) {
@@ -136,7 +145,7 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
log.Printf("[INFO] delete comment %s", id)
log.Printf("[DEBUG] delete comment %s", id)
url := r.URL.Query().Get("url")
err := s.Store.Delete(store.Locator{URL: url}, id)
@@ -153,7 +162,7 @@ func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
// GET /find?url=post-url
func (s *Server) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
log.Printf("[INFO] get comments for %s", url)
log.Printf("[DEBUG] get comments for %s", url)
comments, err := s.Store.Find(store.Request{Locator: store.Locator{URL: url}})
if err != nil {
@@ -190,7 +199,7 @@ func (s *Server) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
url := r.URL.Query().Get("url")
log.Printf("[INFO] get comments by id %s, %s", id, url)
log.Printf("[DEBUG] get comments by id %s, %s", id, url)
comment, err := s.Store.Get(store.Locator{URL: url}, id)
if err != nil {
@@ -233,7 +242,7 @@ func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) {
}
id := chi.URLParam(r, "id")
log.Printf("[INFO] vote for comment %s", id)
log.Printf("[DEBUG] vote for comment %s", id)
url := r.URL.Query().Get("url")
vote := r.URL.Query().Get("vote") == "1"
+10 -8
View File
@@ -13,9 +13,8 @@ import (
)
// BoltDB implements store.Interface. Each instance represents one site.
// Keys are commendID. Each url (post) makes it's own bucket.
// In addition there is a bucket "last" with reference to other buckets+keys to all cross-posts last comment extraction.
// Thread safe.
// Keys are commentID. Each url (post) makes it's own bucket. In addition there is a bucket "last" with
// reference to other buckets+keys to all cross-posts last comment extraction. Thread safe.
type BoltDB struct {
*bolt.DB
}
@@ -54,7 +53,7 @@ func (b *BoltDB) Create(comment Comment) (string, error) {
err := b.Update(func(tx *bolt.Tx) error {
bucket, e := tx.CreateBucketIfNotExists([]byte(comment.Locator.URL))
if e != nil {
return errors.Wrapf(e, "can't make bucket", comment.Locator.URL)
return errors.Wrapf(e, "can't make or open bucket", comment.Locator.URL)
}
// check if key already in store, reject doubles
@@ -250,6 +249,7 @@ func (b *BoltDB) Vote(locator Locator, commentID string, userID string, val bool
// update votes and score
comment.Votes[userID] = val
if val {
comment.Score++
} else {
@@ -321,10 +321,6 @@ func (b *BoltDB) IsBlocked(locator Locator, userID string) (result bool) {
return result
}
func (b *BoltDB) bucketForBlock(locator Locator, userID string) []byte {
return []byte(fmt.Sprintf("%s%s", blocksBucketPrefix, locator.SiteID))
}
// List returns list of buckets, which is list of all commented posts
func (b BoltDB) List(locator Locator) (result []string, err error) {
@@ -339,11 +335,17 @@ func (b BoltDB) List(locator Locator) (result []string, err error) {
return result, err
}
func (b *BoltDB) bucketForBlock(locator Locator, userID string) []byte {
return []byte(fmt.Sprintf("%s%s", blocksBucketPrefix, locator.SiteID))
}
// ref represents key:value pair for extra, index-only buckets
type ref struct {
key string
value string
}
// refFromComment makes reference record used for related buckets referencing prim data set
func refFromComment(comment Comment) *ref {
result := ref{
key: fmt.Sprintf("%s!!%s", comment.Timestamp.Format(time.RFC3339Nano), comment.ID),
+39
View File
@@ -0,0 +1,39 @@
package store
import "testing"
import "github.com/stretchr/testify/assert"
func TestStore_MakeCommentID(t *testing.T) {
cid1 := makeCommentID()
assert.True(t, len(cid1) > 8, "cid1 is long enough")
cid2 := makeCommentID()
assert.True(t, len(cid2) > 8, "cid2 is long enough")
assert.NotEqual(t, cid1, cid2, "cids different")
}
func TestStore_SanitizeComment(t *testing.T) {
tbl := []struct {
inp Comment
out Comment
}{
{inp: Comment{}, out: Comment{}},
{
inp: Comment{
Text: `blah <a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>` + "\n\t",
User: User{ID: `<a href="http://blah.com">username</a>`},
},
out: Comment{
Text: `blah XSS`,
User: User{ID: `&lt;a href=&#34;http://blah.com&#34;&gt;username&lt;/a&gt;`},
},
},
}
for n, tt := range tbl {
out := sanitizeComment(tt.inp)
assert.Equal(t, tt.out, out, "check #%d", n)
}
}