diff --git a/app/main.go b/app/main.go index 9827469e..661b4586 100644 --- a/app/main.go +++ b/app/main.go @@ -33,9 +33,10 @@ var opts struct { BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"` MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"` - SessionStore string `long:"session" env:"SESSION_STORE" default:"./var/session" description:"session store location"` - AvatarStore string `long:"avatars" env:"AVATAR_STORE" default:"./var/avatars" description:"avatars location"` - StoreKey string `long:"store-key" env:"STORE_KEY" default:"secure-store-key" description:"store key"` + SessionStore string `long:"session" env:"SESSION_STORE" default:"./var/session" description:"session store location"` + AvatarStore string `long:"avatars" env:"AVATAR_STORE" default:"./var/avatars" description:"avatars location"` + StoreKey string `long:"store-key" env:"STORE_KEY" default:"secure-store-key" description:"store key"` + MaxCommentSize int `long:"max-comment" env:"MAX_COMMENT_SIZE" default:"2048" description:"max comment size"` GoogleCID string `long:"google-cid" env:"REMARK_GOOGLE_CID" description:"Google OAuth client ID"` GoogleCSEC string `long:"google-csec" env:"REMARK_GOOGLE_CSEC" description:"Google OAuth client secret"` @@ -72,7 +73,13 @@ func main() { log.Printf("[WARN] running in dev mode") } - dataService := store.Service{Interface: dataStore, EditDuration: 5 * time.Minute, Secret: opts.StoreKey} + dataService := store.Service{ + Interface: dataStore, + EditDuration: 5 * time.Minute, + Secret: opts.StoreKey, + MaxCommentSize: opts.MaxCommentSize, + } + sessionStore := func() sessions.Store { sess := sessions.NewFilesystemStore(opts.SessionStore, []byte(opts.StoreKey)) sess.Options.HttpOnly = true diff --git a/app/rest/api/admin_test.go b/app/rest/api/admin_test.go index 7f39cbc0..c5597ac7 100644 --- a/app/rest/api/admin_test.go +++ b/app/rest/api/admin_test.go @@ -19,9 +19,9 @@ func TestAdmin_Delete(t *testing.T) { assert.NotNil(t, srv) defer cleanup(srv) - c1 := store.Comment{Text: "test test #1", + c1 := store.Comment{Text: "test test #1", User: store.User{ID: "id", Name: "name"}, Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", + c2 := store.Comment{Text: "test test #2", User: store.User{ID: "id", Name: "name"}, ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} id1 := addComment(t, c1, port) diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index 40abed6d..62984e62 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -139,7 +139,7 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) { comment.User = user comment.User.IP = strings.Split(r.RemoteAddr, ":")[0] - if comment.Validate() != nil { + if err := s.DataService.ValidateComment(&comment); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") return } @@ -177,7 +177,7 @@ func (s *Rest) previewCommentCtrl(w http.ResponseWriter, r *http.Request) { return } comment.User = user - if err := comment.Validate(); err != nil { + if err := s.DataService.ValidateComment(&comment); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment") return } diff --git a/app/rest/api/rest_test.go b/app/rest/api/rest_test.go index 831f0c88..551f5496 100644 --- a/app/rest/api/rest_test.go +++ b/app/rest/api/rest_test.go @@ -439,7 +439,7 @@ func prep(t *testing.T) (srv *Rest, port int) { dataStore, err := store.NewBoltDB(bolt.Options{}, store.BoltSite{FileName: testDb, SiteID: "radio-t"}) require.Nil(t, err) srv = &Rest{ - DataService: store.Service{Interface: dataStore, EditDuration: 5 * time.Minute}, + DataService: store.Service{Interface: dataStore, EditDuration: 5 * time.Minute, MaxCommentSize: 4000}, Authenticator: auth.Authenticator{ SessionStore: sessions.NewFilesystemStore("/tmp", []byte("blah")), DevPasswd: "password", diff --git a/app/rest/cache_test.go b/app/rest/cache_test.go index 47fa3bef..21b3321c 100644 --- a/app/rest/cache_test.go +++ b/app/rest/cache_test.go @@ -2,6 +2,7 @@ package rest import ( "net/http" + "sync/atomic" "testing" "time" @@ -10,32 +11,32 @@ import ( ) func TestLoadingCache_Get(t *testing.T) { - var postFnCall, coldCalls int + var postFnCall, coldCalls int32 lc := NewLoadingCache(1*time.Minute, 200*time.Millisecond, func() { - postFnCall++ + atomic.AddInt32(&postFnCall, 1) }) res, err := lc.Get("key", time.Minute, func() ([]byte, error) { - coldCalls++ + atomic.AddInt32(&coldCalls, 1) return []byte("result"), nil }) assert.Nil(t, err) assert.Equal(t, "result", string(res)) - assert.Equal(t, 1, coldCalls) - assert.Equal(t, 0, postFnCall) + assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls)) + assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) res, err = lc.Get("key", time.Minute, func() ([]byte, error) { - coldCalls++ + atomic.AddInt32(&coldCalls, 1) return []byte("result"), nil }) assert.Nil(t, err) assert.Equal(t, "result", string(res)) - assert.Equal(t, 1, coldCalls) - assert.Equal(t, 0, postFnCall) + assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls)) + assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) lc.Flush() time.Sleep(100 * time.Millisecond) // let postFn to do its thing - assert.Equal(t, 1, postFnCall) + assert.Equal(t, int32(1), atomic.LoadInt32(&postFnCall)) } func TestLoadingCache_URLKey(t *testing.T) { diff --git a/app/store/comment.go b/app/store/comment.go index 28c461aa..de1b551d 100644 --- a/app/store/comment.go +++ b/app/store/comment.go @@ -3,7 +3,6 @@ package store import ( "crypto/hmac" "crypto/sha1" - "errors" "fmt" "html/template" "log" @@ -63,9 +62,6 @@ type BlockedUser struct { Timestamp time.Time `json:"time"` } -// MaxCommentSize defines max size of comment's text -const MaxCommentSize = 2048 - // PrepareUntrusted preprocess comment received from untrusted source by clearing all // autogen fields and reset everything users not supposed to provide func (c *Comment) PrepareUntrusted() { @@ -99,20 +95,6 @@ func (c *Comment) Sanitize() { // c.Text = strings.Replace(c.Text, "\t", "", -1) } -// Validate comment -func (c *Comment) Validate() error { - if c.Text == "" { - return errors.New("empty comment text") - } - if len(c.Text) > MaxCommentSize { - return errors.New("comment text exceeded max allowed size") - } - if c.User.ID == "" || c.User.Name == "" { - return errors.New("empty user info") - } - return nil -} - // hashIP replace sensitive fields with hashes func (u *User) hashIP(secret string) { diff --git a/app/store/comment_test.go b/app/store/comment_test.go index 0180c80f..33713083 100644 --- a/app/store/comment_test.go +++ b/app/store/comment_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -33,31 +32,6 @@ func TestComment_Sanitize(t *testing.T) { } } -func TestComment_Validate(t *testing.T) { - longText := "" - for i := 0; i < 4000; i++ { - longText += "X" - } - tbl := []struct { - inp Comment - err error - }{ - {inp: Comment{}, err: errors.New("empty comment text")}, - {inp: Comment{Text: "something blah", User: User{ID: "myid", Name: "name"}}, err: nil}, - {inp: Comment{Text: "something blah", User: User{ID: "myid"}}, err: errors.New("empty user info")}, - {inp: Comment{Text: longText, User: User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size")}, - } - - for n, tt := range tbl { - e := tt.inp.Validate() - if tt.err == nil { - assert.Nil(t, e, "check #%d", n) - continue - } - assert.EqualError(t, tt.err, e.Error(), "check #%d", n) - } -} - func TestComment_PrepareUntrusted(t *testing.T) { comment := Comment{ Text: `blah`, diff --git a/app/store/service.go b/app/store/service.go index c8fe2fd6..0b8c62e1 100644 --- a/app/store/service.go +++ b/app/store/service.go @@ -10,8 +10,9 @@ import ( // Service wraps store.Interface with additional methods type Service struct { Interface - EditDuration time.Duration - Secret string + EditDuration time.Duration + Secret string + MaxCommentSize int } // Create prepares comment and forward to Interface.Create @@ -115,3 +116,21 @@ func (s *Service) Counts(siteID string, postIDs []string) ([]PostInfo, error) { } return res, nil } + +// ValidateComment checks if comment size below max and user fields set +func (s *Service) ValidateComment(c *Comment) error { + maxSize := s.MaxCommentSize + if s.MaxCommentSize <= 0 { + maxSize = 2000 + } + if c.Text == "" { + return errors.New("empty comment text") + } + if len(c.Text) > maxSize { + return errors.New("comment text exceeded max allowed size") + } + if c.User.ID == "" || c.User.Name == "" { + return errors.New("empty user info") + } + return nil +} diff --git a/app/store/service_test.go b/app/store/service_test.go index f3455c14..c86af5d3 100644 --- a/app/store/service_test.go +++ b/app/store/service_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/coreos/bbolt" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -166,6 +167,33 @@ func TestService_EditCommentDurationFailed(t *testing.T) { assert.NotNil(t, err) } +func TestService_ValidateComment(t *testing.T) { + + b := Service{MaxCommentSize: 2000} + longText := "" + for i := 0; i < 4000; i++ { + longText += "X" + } + tbl := []struct { + inp Comment + err error + }{ + {inp: Comment{}, err: errors.New("empty comment text")}, + {inp: Comment{Text: "something blah", User: User{ID: "myid", Name: "name"}}, err: nil}, + {inp: Comment{Text: "something blah", User: User{ID: "myid"}}, err: errors.New("empty user info")}, + {inp: Comment{Text: longText, User: User{ID: "myid", Name: "name"}}, err: errors.New("comment text exceeded max allowed size")}, + } + + for n, tt := range tbl { + e := b.ValidateComment(&tt.inp) + if tt.err == nil { + assert.Nil(t, e, "check #%d", n) + continue + } + assert.EqualError(t, tt.err, e.Error(), "check #%d", n) + } +} + func TestService_Counts(t *testing.T) { defer os.Remove(testDb) b := prep(t) // two comments for https://radio-t.com