Merge pull request #401 from umputun/rpc-example

RPC example
This commit is contained in:
Umputun
2019-08-01 02:08:26 -05:00
committed by GitHub
38 changed files with 2579 additions and 481 deletions
+1
View File
@@ -20,3 +20,4 @@ remark42
/bin/
/backend/var/
compose-private-backend.yml
/backend/_example/*/vendor
+22
View File
@@ -0,0 +1,22 @@
FROM umputun/baseimage:buildgo-latest as build-backend
#ADD . /build/memory_store
#WORKDIR /build/memory_store
ADD backend /build/backend
WORKDIR /build/backend/_example/memory_store
RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
FROM umputun/baseimage:app-latest
WORKDIR /srv
COPY --from=build-backend /build/bin/memory_store /srv/memory_store
RUN chown -R app:app /srv
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
USER app
CMD ["/srv/memory_store"]
+13
View File
@@ -0,0 +1,13 @@
# sample store implementation
`memory_store` illustrates how to make a custom storage plugin for remark42.
In order to run remark42 with memory_store copy provided `compose-dev-memstore.yml` to the root directory and run:
1. docker-compose -f compose-dev-memstore.yml build
1. docker-compose -f compose-dev-memstore.yml up
As usual, demo site will run on http://127.0.0.1:8080/web/
note: in order to work with the latest (current) version of master `go.mod` uses replacement directive for the backend package
. In real-life usage `replace github.com/umputun/remark/backend => ../../` should not be used.
@@ -0,0 +1,61 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
log "github.com/go-pkgz/lgr"
"github.com/pkg/errors"
)
// MemAdmin implements admin.Store with memory backend
type MemAdmin struct {
data map[string]AdminRec // admin info per site
key string
}
// AdminRec is a records per site with all admin info in
type AdminRec struct {
SiteID string `bson:"site"`
IDs []string `bson:"ids"` // admin ids
Email string `bson:"email"` // admin email
}
// NewMemAdminStore makes admin Store in memory
func NewMemAdminStore(key string) *MemAdmin {
log.Print("[DEBUG] make memory admin store")
return &MemAdmin{data: map[string]AdminRec{}, key: key}
}
// Key executes find by siteID and returns substructure with secret key
func (m *MemAdmin) Key() (key string, err error) {
return m.key, nil
}
// Admins executes find by siteID and returns admins ids
func (m *MemAdmin) Admins(siteID string) (ids []string, err error) {
resp, ok := m.data[siteID]
if !ok {
return nil, errors.Errorf("site %s not found", siteID)
}
log.Printf("[DEBUG] admins for %s, %+v", siteID, resp.IDs)
return resp.IDs, nil
}
// Email executes find by siteID and returns admin's email
func (m *MemAdmin) Email(siteID string) (email string, err error) {
resp, ok := m.data[siteID]
if !ok {
return "", errors.Errorf("site %s not found", siteID)
}
return resp.Email, nil
}
// Set admin data for siteID
func (m *MemAdmin) Set(siteID string, arec AdminRec) {
m.data[siteID] = arec
}
@@ -0,0 +1,52 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/umputun/remark/backend/app/store/admin"
)
func TestMemAdmin_Get(t *testing.T) {
adm := NewMemAdminStore("secret")
var ms admin.Store = adm
adm.data = map[string]AdminRec{
"site1": {"site1", []string{"i11", "i12"}, "e1"},
"site2": {"site2", []string{"i21", "i22"}, "e2"},
}
admins, err := ms.Admins("site1")
assert.NoError(t, err)
assert.Equal(t, []string{"i11", "i12"}, admins)
email, err := ms.Email("site1")
assert.NoError(t, err)
assert.Equal(t, "e1", email)
key, err := ms.Key()
assert.NoError(t, err)
assert.Equal(t, "secret", key)
admins, err = ms.Admins("site2")
assert.NoError(t, err)
assert.Equal(t, []string{"i21", "i22"}, admins)
email, err = ms.Email("site2")
assert.NoError(t, err)
assert.Equal(t, "e2", email)
key, err = ms.Key()
assert.NoError(t, err)
assert.Equal(t, "secret", key)
admins, err = ms.Admins("no-site-in-db")
assert.EqualError(t, err, "site no-site-in-db not found")
email, err = ms.Email("no-site-in-db")
assert.EqualError(t, err, "site no-site-in-db not found")
}
@@ -0,0 +1,441 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"log"
"sort"
"sync"
"time"
"github.com/pkg/errors"
"github.com/umputun/remark/backend/app/store/engine"
"github.com/umputun/remark/backend/app/store"
)
const lastLimit = 1000
// MemData implements in-memory data store
type MemData struct {
posts map[string][]store.Comment // key is siteID
metaUsers map[string]metaUser // key is userID
metaPosts map[store.Locator]metaPost // key is post's locator
sync.RWMutex
}
type metaPost struct {
PostURL string
SiteID string
ReadOnly bool
}
type metaUser struct {
UserID string
SiteID string
Verified bool
Blocked bool
BlockedUntil time.Time
}
// NewMemData makes in-memory engine.
func NewMemData() *MemData {
result := &MemData{
posts: map[string][]store.Comment{},
metaUsers: map[string]metaUser{},
metaPosts: map[store.Locator]metaPost{},
}
return result
}
// Create new comment
func (m *MemData) Create(comment store.Comment) (commentID string, err error) {
if ro, e := m.Flag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: comment.Locator}); e == nil && ro {
return "", errors.Errorf("post %s is read-only", comment.Locator.URL)
}
m.Lock()
defer m.Unlock()
comments := m.posts[comment.Locator.SiteID]
for _, c := range comments { // don't allow duplicated IDs
if c.ID == comment.ID {
return "", errors.New("dup key")
}
}
comments = append(comments, comment)
m.posts[comment.Locator.SiteID] = comments
return comment.ID, nil
}
// Find returns all comments for post and sorts results
func (m *MemData) Find(req engine.FindRequest) (comments []store.Comment, err error) {
m.RLock()
defer m.RUnlock()
comments = []store.Comment{}
if req.Sort == "" {
req.Sort = "time"
}
switch {
case req.Locator.SiteID != "" && req.Locator.URL != "": // find comments for site and url
comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.Locator == req.Locator && (req.Since.IsZero() || c.Timestamp.After(req.Since))
})
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.UserID == "": // find last comments for site
if req.Limit > lastLimit || req.Limit == 0 {
req.Limit = lastLimit
}
if req.Since.IsZero() {
req.Since = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
}
comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return !c.Deleted && c.Timestamp.After(req.Since)
})
comments = engine.SortComments(comments, "-time")
if len(comments) > req.Limit {
comments = comments[:req.Limit]
}
return comments, nil
case req.Locator.SiteID != "" && req.UserID != "": // find comments for user
comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.User.ID == req.UserID
})
}
comments = engine.SortComments(comments, req.Sort)
if req.Skip > 0 && req.Skip > len(comments) {
return []store.Comment{}, nil
}
if req.Skip > 0 && req.Skip < len(comments) {
comments = comments[req.Skip:]
}
if req.Limit > 0 && req.Limit < len(comments) {
comments = comments[:req.Limit]
}
return comments, err
}
// Get returns comment for locator.URL and commentID string
func (m *MemData) Get(req engine.GetRequest) (comment store.Comment, err error) {
m.RLock()
defer m.RUnlock()
return m.get(req.Locator, req.CommentID)
}
// Update updates comment for locator.URL with mutable part of comment
func (m *MemData) Update(comment store.Comment) error {
m.Lock()
defer m.Unlock()
return m.updateComment(comment)
}
// Count returns number of comments for post or user
func (m *MemData) Count(req engine.FindRequest) (count int, err error) {
m.RLock()
defer m.RUnlock()
switch {
case req.Locator.URL != "": // comment's count for post
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.Locator == req.Locator && !c.Deleted
})
return len(comments), nil
case req.UserID != "":
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.User.ID == req.UserID && !c.Deleted
})
return len(comments), nil
default:
return 0, errors.Errorf("invalid count request %+v", req)
}
}
// Info get post(s) meta info
func (m *MemData) Info(req engine.InfoRequest) (res []store.PostInfo, err error) {
m.RLock()
defer m.RUnlock()
res = []store.PostInfo{}
if req.Locator.URL != "" { // post info
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.Locator == req.Locator
})
if len(comments) == 0 {
return nil, errors.New("not found")
}
info := store.PostInfo{
URL: req.Locator.URL,
Count: len(comments),
ReadOnly: false,
FirstTS: comments[0].Timestamp.UTC(),
LastTS: comments[len(comments)-1].Timestamp.UTC(),
}
// set read-only from age and manual bucket
info.ReadOnly = req.ReadOnlyAge > 0 && !info.FirstTS.IsZero() &&
info.FirstTS.AddDate(0, 0, req.ReadOnlyAge).Before(time.Now())
if !info.ReadOnly {
v := m.checkFlag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: req.Locator})
info.ReadOnly = v
}
return []store.PostInfo{info}, nil
}
if req.Locator.URL == "" && req.Locator.SiteID != "" { // site info (list)
if req.Limit <= 0 {
req.Limit = 1000
}
if req.Skip < 0 {
req.Skip = 0
}
infoAll := map[store.Locator]store.PostInfo{}
for _, c := range m.posts[req.Locator.SiteID] {
var info store.PostInfo
var ok bool
if info, ok = infoAll[c.Locator]; !ok {
info = store.PostInfo{URL: c.Locator.URL, FirstTS: c.Timestamp.UTC()}
}
info.Count++
info.LastTS = c.Timestamp.UTC()
infoAll[c.Locator] = info
}
for _, v := range infoAll {
res = append(res, v)
}
sort.Slice(res, func(i, j int) bool {
return res[i].URL > res[j].URL
})
if req.Skip > 0 {
if req.Skip >= len(res) {
return []store.PostInfo{}, nil
}
res = res[req.Skip:]
}
if req.Limit > 0 && req.Limit < len(res) {
res = res[:req.Limit]
}
return res, nil
}
return nil, errors.Errorf("invalid info request %+v", req)
}
// Flag sets and gets flag values
func (m *MemData) Flag(req engine.FlagRequest) (val bool, err error) {
m.Lock()
defer m.Unlock()
if req.Update == engine.FlagNonSet { // read flag value, no update requested
return m.checkFlag(req), nil
}
// write flag value
return m.setFlag(req)
}
// ListFlags get list of flagged keys, like blocked & verified user
// works for full locator (post flags) or with userID
func (m *MemData) ListFlags(req engine.FlagRequest) (res []interface{}, err error) {
m.RLock()
defer m.RUnlock()
res = []interface{}{}
switch req.Flag {
case engine.Verified:
for _, u := range m.metaUsers {
if u.SiteID == req.Locator.SiteID {
res = append(res, u.UserID)
}
}
return res, nil
case engine.Blocked:
log.Printf("%+v", m.metaUsers)
for _, u := range m.metaUsers {
if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
}
}
return res, nil
}
return nil, errors.Errorf("flag %s not listable", req.Flag)
}
// Delete post(s) by id or by userID
func (m *MemData) Delete(req engine.DeleteRequest) error {
m.Lock()
defer m.Unlock()
switch {
case req.Locator.URL != "" && req.CommentID != "": // delete comment
return m.deleteComment(req.Locator, req.CommentID, req.DeleteMode)
case req.Locator.SiteID != "" && req.UserID != "" && req.CommentID == "": // delete user
comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
return c.User.ID == req.UserID && !c.Deleted
})
for _, c := range comments {
if e := m.deleteComment(c.Locator, c.ID, req.DeleteMode); e != nil {
return e
}
}
return nil
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.CommentID == "" && req.UserID == "": // delete site
if _, ok := m.posts[req.Locator.SiteID]; !ok {
return errors.New("not found")
}
m.posts[req.Locator.SiteID] = []store.Comment{}
return nil
}
return errors.Errorf("invalid delete request %+v", req)
}
func (m *MemData) deleteComment(loc store.Locator, id string, mode store.DeleteMode) error {
comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool {
return c.Locator == loc && c.ID == id
})
if len(comments) == 0 {
return errors.New("not found")
}
comments[0].SetDeleted(mode)
return m.updateComment(comments[0])
}
// Close store
func (m *MemData) Close() error {
return nil
}
func (m *MemData) checkFlag(req engine.FlagRequest) (val bool) {
switch req.Flag {
case engine.Blocked:
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return false
}
return meta.Blocked && meta.BlockedUntil.After(time.Now())
}
case engine.Verified:
if meta, ok := m.metaUsers[req.UserID]; ok {
if meta.SiteID != req.Locator.SiteID {
return false
}
return meta.Verified
}
case engine.ReadOnly:
if meta, ok := m.metaPosts[req.Locator]; ok {
return meta.ReadOnly
}
}
return false
}
func (m *MemData) setFlag(req engine.FlagRequest) (res bool, err error) {
status := false
if req.Update == engine.FlagTrue {
status = true
}
switch req.Flag {
case engine.Blocked:
until := time.Time{}
if status {
until = time.Now().AddDate(100, 0, 0) // permanent is 100years
if req.TTL > 0 {
until = time.Now().Add(req.TTL)
}
}
meta := m.metaUsers[req.UserID]
meta = metaUser{
UserID: req.UserID,
SiteID: req.Locator.SiteID,
Blocked: status,
BlockedUntil: until,
}
m.metaUsers[req.UserID] = meta
case engine.Verified:
meta := m.metaUsers[req.UserID]
meta = metaUser{
UserID: req.UserID,
SiteID: req.Locator.SiteID,
Verified: status,
}
m.metaUsers[req.UserID] = meta
case engine.ReadOnly:
info, ok := m.metaPosts[req.Locator]
if !ok {
info.SiteID = req.Locator.SiteID
info.PostURL = req.Locator.URL
}
info.ReadOnly = status
m.metaPosts[req.Locator] = info
}
return status, errors.Wrapf(err, "failed to set flag %+v", req)
}
func (m *MemData) get(loc store.Locator, commentID string) (store.Comment, error) {
comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool {
return c.Locator == loc && c.ID == commentID
})
if len(comments) == 0 {
return store.Comment{}, errors.New("not found")
}
return comments[0], nil
}
func (m *MemData) updateComment(comment store.Comment) error {
comments := m.posts[comment.Locator.SiteID]
for i, c := range comments {
if c.ID == comment.ID && c.Locator == comment.Locator {
c.Text = comment.Text
c.Orig = comment.Orig
c.Score = comment.Score
c.Votes = comment.Votes
c.Pin = comment.Pin
c.Deleted = comment.Deleted
c.User = comment.User
comments[i] = c
m.posts[comment.Locator.SiteID] = comments
return nil
}
}
return errors.New("not found")
}
func (m *MemData) match(comments []store.Comment, fn func(c store.Comment) bool) (res []store.Comment) {
res = []store.Comment{}
for _, c := range comments {
if fn(c) {
res = append(res, c)
}
}
return res
}
@@ -0,0 +1,727 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package accessor
import (
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/engine"
)
func TestMem_CreateAndFind(t *testing.T) {
m := prepMem(t) // adds two comments
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := m.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text)
assert.Equal(t, "user1", res[0].User.ID)
t.Log(res[0].ID)
_, err = m.Create(store.Comment{ID: res[0].ID, Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}})
require.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "dup key"), err.Error())
id, err := m.Create(store.Comment{ID: "id-3", Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}})
require.NoError(t, err)
assert.Equal(t, "id-3", id)
req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}, Sort: "time"}
res, err = m.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res))
}
func TestMem_CreateFailedReadOnly(t *testing.T) {
b := prepMem(t)
comment := store.Comment{
ID: "id-ro",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/ro", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
flagReq := engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagTrue}
v, err := b.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, v)
_, err = b.Create(comment)
assert.NotNil(t, err)
assert.Equal(t, "post https://radio-t.com/ro is read-only", err.Error())
flagReq = engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagFalse}
v, err = b.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, false, v)
_, err = b.Create(comment)
assert.NoError(t, err)
}
func TestMem_Get(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res), "2 records initially")
comment, err := b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID))
assert.NoError(t, err)
assert.Equal(t, "some text2", comment.Text)
_, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567"))
assert.EqualError(t, err, `not found`)
_, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, res[1].ID))
assert.EqualError(t, err, `not found`)
}
func TestMem_Update(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res), "2 records initially")
comment := res[0]
comment.Text = "abc 123"
comment.Score = 100
err = b.Update(comment)
assert.NoError(t, err)
comment, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID))
assert.NoError(t, err)
assert.Equal(t, "abc 123", comment.Text)
assert.Equal(t, res[0].ID, comment.ID)
assert.Equal(t, 100, comment.Score)
comment.Locator.SiteID = "bad"
err = b.Update(comment)
assert.EqualError(t, err, `not found`)
comment.Locator.SiteID = "https://radio-t.com"
comment.Locator.URL = "https://radio-t.com-bad"
err = b.Update(comment)
assert.EqualError(t, err, `not found`)
}
func TestMem_FindLast(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Limit = 1
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Locator.SiteID = "bad"
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res))
}
func TestMem_FindLastSince(t *testing.T) {
b := prepMem(t)
ts := time.Date(2017, 12, 20, 15, 18, 21, 0, time.Local)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", Since: ts}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Since = time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local)
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res))
assert.Equal(t, "some text2", res[0].Text)
req.Since = time.Date(2017, 12, 20, 16, 18, 22, 0, time.Local)
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res))
}
func TestMem_FindForUser(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 5}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1}
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res), "allow 1 comment")
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 1, len(res), "allow 1 comment")
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text, "second comment")
req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res), "no comments")
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "userZ", Limit: 1, Skip: 1}
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res), "no comments")
}
func TestMem_FindForUserPagination(t *testing.T) {
b := NewMemData()
c := store.Comment{
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
// write 200 comments
for i := 0; i < 200; i++ {
c.ID = fmt.Sprintf("idd-%d", i)
c.Text = fmt.Sprintf("text #%d", i)
c.Timestamp = time.Date(2017, 12, 20, 15, 18, i, 0, time.Local)
_, err := b.Create(c)
require.Nil(t, err)
}
// get all comments
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1"}
res, err := b.Find(req)
assert.NoError(t, err)
require.Equal(t, 200, len(res))
assert.Equal(t, "idd-199", res[0].ID)
// seek 0, 5 comments
req.Limit = 5
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 5, len(res))
assert.Equal(t, "idd-199", res[0].ID)
assert.Equal(t, "idd-195", res[4].ID)
// seek 10, 3 comments
req.Skip, req.Limit = 10, 3
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 3, len(res))
assert.Equal(t, "idd-189", res[0].ID)
assert.Equal(t, "idd-187", res[2].ID)
// seek 195, ask 10 comments
req.Skip, req.Limit = 195, 10
res, err = b.Find(req)
assert.NoError(t, err)
require.Equal(t, 5, len(res))
assert.Equal(t, "idd-4", res[0].ID)
assert.Equal(t, "idd-0", res[4].ID)
// seek 255, ask 10 comments
req.Skip, req.Limit = 255, 10
res, err = b.Find(req)
assert.NoError(t, err)
assert.Equal(t, 0, len(res))
}
func TestMem_CountPost(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}}
c, err := b.Count(req)
assert.NoError(t, err)
require.Equal(t, 2, c)
req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com-xxx", SiteID: "radio-t"}}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "bad"}}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
}
func TestMem_CountUser(t *testing.T) {
b := prepMem(t)
req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
c, err := b.Count(req)
assert.NoError(t, err)
require.Equal(t, 2, c)
req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1"}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "userZ"}
c, err = b.Count(req)
assert.NoError(t, err)
assert.Equal(t, 0, c)
}
func TestMem_InfoPost(t *testing.T) {
b := prepMem(t)
ts := func(min int) time.Time { return time.Date(2017, 12, 20, 15, 18, min, 0, time.Local).In(time.UTC) }
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 24, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
req := engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
r, err := b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24)}}, r)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 10}
r, err = b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1,
FirstTS: ts(24), LastTS: ts(24), ReadOnly: true}}, r)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, ReadOnlyAge: 0}
r, err = b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, r)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/error", SiteID: "radio-t"}, ReadOnlyAge: 0}
_, err = b.Info(req)
require.NotNil(t, err)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-error"}, ReadOnlyAge: 0}
_, err = b.Info(req)
require.NotNil(t, err)
fr := engine.FlagRequest{Flag: engine.ReadOnly,
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, Update: engine.FlagTrue}
_, err = b.Flag(fr)
require.NoError(t, err)
req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
r, err = b.Info(req)
require.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24),
ReadOnly: true}}, r)
}
func TestMem_InfoList(t *testing.T) {
b := prepMem(t)
// add one more for https://radio-t.com/2
comment := store.Comment{
ID: "12345",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := b.Create(comment)
assert.NoError(t, err)
ts := func(sec int) time.Time { return time.Date(2017, 12, 20, 15, 18, sec, 0, time.Local).In(time.UTC) }
req := engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}}
res, err := b.Info(req)
assert.NoError(t, err)
assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: -1, Skip: -1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1, Skip: 1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
req = engine.InfoRequest{Locator: store.Locator{SiteID: "bad"}, Limit: 1, Skip: 1}
res, err = b.Info(req)
assert.NoError(t, err)
assert.Equal(t, []store.PostInfo{}, res)
}
func TestMem_FlagBlockedUser(t *testing.T) {
b := prepMem(t)
req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
val, err := b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "nothing blocked yet")
req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
Update: engine.FlagTrue}
_, err = b.Flag(req)
assert.NoError(t, err)
val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
assert.NoError(t, err)
assert.True(t, val, "user1 blocked")
req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
Update: engine.FlagTrue}
_, err = b.Flag(req)
assert.NoError(t, err)
val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
assert.NoError(t, err)
assert.True(t, val, "user1 still blocked")
req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
Update: engine.FlagFalse}
_, err = b.Flag(req)
assert.NoError(t, err)
val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
assert.NoError(t, err)
assert.False(t, val, "user1 unblocked")
}
func TestMem_FlagReadOnlyPost(t *testing.T) {
b := prepMem(t)
req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
val, err := b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "nothing ro")
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly,
Update: engine.FlagTrue}
val, err = b.Flag(req)
assert.NoError(t, err)
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
val, err = b.Flag(req)
assert.NoError(t, err)
assert.True(t, val, "url-1 ro")
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-2"}, Flag: engine.ReadOnly}
val, err = b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "url-2 still writable")
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly,
Update: engine.FlagFalse}
_, err = b.Flag(req)
assert.NoError(t, err)
req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
val, err = b.Flag(req)
assert.NoError(t, err)
assert.False(t, val, "url-1 writable")
}
func TestMem_FlagVerified(t *testing.T) {
b := prepMem(t)
isVerified := func(site, user string) bool {
req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user}
v, err := b.Flag(req)
require.NoError(t, err)
return v
}
setVerified := func(site, user string, status engine.FlagStatus) error {
req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
_, err := b.Flag(req)
return err
}
assert.False(t, isVerified("radio-t", "u1"), "nothing verified")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
assert.True(t, isVerified("radio-t", "u1"), "u1 verified")
assert.False(t, isVerified("radio-t", "u2"), "u2 still not verified")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagFalse))
assert.False(t, isVerified("radio-t", "u1"), "u1 not verified anymore")
assert.NoError(t, setVerified("bad", "u1", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u1xyz", engine.FlagFalse))
assert.False(t, isVerified("radio-t-bad", "u1"), "nothing verified on wrong site")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u3", engine.FlagFalse))
}
func TestMem_FlagListVerified(t *testing.T) {
b := prepMem(t)
toIDs := func(inp []interface{}) (res []string) {
res = make([]string, len(inp))
for i, v := range inp {
vv, ok := v.(string)
require.True(t, ok)
res[i] = vv
}
sort.Strings(res)
return res
}
setVerified := func(site, user string, status engine.FlagStatus) error {
req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
_, err := b.Flag(req)
return err
}
ids, err := b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
assert.Equal(t, []string{}, toIDs(ids), "verified list empty")
assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue))
ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
assert.EqualValues(t, []string{"u1", "u2"}, toIDs(ids), "verified 2 ids")
ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t-bad"}})
assert.NoError(t, err)
assert.Equal(t, 0, len(ids))
}
func TestMem_FlagListBlocked(t *testing.T) {
b := prepMem(t)
setBlocked := func(site, user string, status engine.FlagStatus, ttl time.Duration) error {
req := engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status,
TTL: ttl}
_, err := b.Flag(req)
return err
}
toBlocked := func(inp []interface{}) (res []store.BlockedUser) {
res = make([]store.BlockedUser, len(inp))
for i, v := range inp {
vv, ok := v.(store.BlockedUser)
require.True(t, ok)
res[i] = vv
}
return res
}
assert.NoError(t, setBlocked("radio-t", "user1", engine.FlagTrue, 0))
assert.NoError(t, setBlocked("radio-t", "user2", engine.FlagTrue, 50*time.Millisecond))
assert.NoError(t, setBlocked("radio-t", "user3", engine.FlagFalse, 0))
vv, err := b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
blockedList := toBlocked(vv)
require.Equal(t, 2, len(blockedList), b.metaUsers)
assert.Equal(t, "user1", blockedList[0].ID)
assert.Equal(t, "user2", blockedList[1].ID)
t.Logf("%+v", blockedList)
// check block expiration
time.Sleep(50 * time.Millisecond)
vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}})
assert.NoError(t, err)
blockedList = toBlocked(vv)
require.Equal(t, 1, len(blockedList))
assert.Equal(t, "user1", blockedList[0].ID)
vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "bad"}})
assert.NoError(t, err)
assert.Equal(t, 0, len(vv))
}
func TestMem_DeleteComment(t *testing.T) {
b := prepMem(t)
reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(reqReq)
assert.NoError(t, err)
require.Equal(t, 2, len(res), "initially 2 comments")
count, err := b.Count(reqReq)
require.NoError(t, err)
require.Equal(t, 2, count, "count=2 initially")
delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
CommentID: res[0].ID, DeleteMode: store.SoftDelete}
err = b.Delete(delReq)
assert.NoError(t, err)
res, err = b.Find(reqReq)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "", res[0].Text)
assert.True(t, res[0].Deleted, "marked deleted")
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
assert.Equal(t, "some text2", res[1].Text)
assert.False(t, res[1].Deleted)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
assert.NoError(t, err)
assert.Equal(t, 1, len(comments), "1 in last, 1 removed")
count, err = b.Count(reqReq)
require.NoError(t, err)
assert.Equal(t, 1, count)
delReq.CommentID = "123456"
err = b.Delete(delReq)
assert.NotNil(t, err)
delReq.Locator.SiteID = "bad"
delReq.CommentID = res[0].ID
err = b.Delete(delReq)
assert.EqualError(t, err, `not found`)
delReq.Locator = store.Locator{URL: "https://radio-t.com/bad", SiteID: "radio-t"}
err = b.Delete(delReq)
assert.EqualError(t, err, `not found`)
}
func TestMem_DeleteHard(t *testing.T) {
b := prepMem(t)
reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
res, err := b.Find(reqReq)
assert.NoError(t, err)
assert.Equal(t, 2, len(res), "initially 2 comments")
delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
CommentID: res[0].ID, DeleteMode: store.HardDelete}
err = b.Delete(delReq)
assert.NoError(t, err)
res, err = b.Find(reqReq)
assert.NoError(t, err)
require.Equal(t, 2, len(res))
assert.Equal(t, "", res[0].Text)
assert.True(t, res[0].Deleted, "marked deleted")
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
}
func TestMem_DeleteAll(t *testing.T) {
b := prepMem(t)
delReq := engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}}
err := b.Delete(delReq)
assert.NoError(t, err)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
assert.NoError(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
}
func TestBoltAdmin_DeleteUserHard(t *testing.T) {
b := prepMem(t)
err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
DeleteMode: store.HardDelete})
require.NoError(t, err)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"},
Sort: "time"})
assert.NoError(t, err)
require.Equal(t, 2, len(comments), "2 comments with deleted info")
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User)
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User)
c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}})
assert.NoError(t, err)
assert.Equal(t, 0, c, "0 count")
_, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5})
assert.NoError(t, err, "no comments for user user1 in store")
comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"})
assert.NoError(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
}
func TestBoltAdmin_DeleteUserSoft(t *testing.T) {
b := prepMem(t)
err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
DeleteMode: store.SoftDelete})
require.NoError(t, err)
comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"},
Sort: "time"})
assert.NoError(t, err)
require.Equal(t, 2, len(comments), "2 comments with deleted info")
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User)
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User)
c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}})
assert.NoError(t, err)
assert.Equal(t, 0, c, "0 count")
comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5})
assert.NoError(t, err, "no comments for user user1 in store")
require.Equal(t, 2, len(comments), "2 comments with deleted info")
assert.True(t, comments[0].Deleted)
assert.True(t, comments[1].Deleted)
assert.Equal(t, "", comments[0].Text)
assert.Equal(t, "", comments[1].Text)
comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"})
assert.NoError(t, err)
assert.Equal(t, 0, len(comments), "nothing left")
}
func prepMem(t *testing.T) *MemData {
m := NewMemData()
comment := store.Comment{
ID: "id-1",
Text: `some text, <a href="http://radio-t.com">link</a>`,
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err := m.Create(comment)
require.NoError(t, err)
comment = store.Comment{
ID: "id-2",
Text: "some text2",
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
User: store.User{ID: "user1", Name: "user name"},
}
_, err = m.Create(comment)
require.NoError(t, err)
return m
}
func getReq(locator store.Locator, commentID string) engine.GetRequest {
return engine.GetRequest{
Locator: locator,
CommentID: commentID,
}
}
@@ -0,0 +1,47 @@
# compose file demonstrating custom storage use. The memory_store (see backend/_example/memory_store) starts
# in a separate container and remark42 communicates to mem_store.r42 via STORE_RPC_API url
version: '2'
services:
remark42:
build:
context: .
dockerfile: Dockerfile
args:
- SKIP_BACKEND_TEST=true
- SKIP_FRONTEND_TEST=true
image: umputun/remark42:dev
container_name: "remark42"
hostname: "remark42"
restart: always
ports:
- "8080:8080" # primary rest server
- "8084:8084" # local oauth2 server
environment:
- REMARK_URL=http://127.0.0.1:8080
- SECRET=123456
- BACKUP_PATH=/srv/var/backup
- DEBUG=true
- EMOJI=true
- AUTH_ANON=true
- AUTH_DEV=true
- ADMIN_TYPE=rpc
- ADMIN_RPC_API=http://mem_store.r42:8080/cmd
- STORE_TYPE=rpc
- STORE_RPC_API=http://mem_store.r42:8080/cmd
mem_store.r42:
image: umputun/mem_store.r42
build:
context: .
dockerfile: backend/_example/memory_store/Dockerfile
container_name: "mem_store.r42"
hostname: "mem_store.r42"
environment:
- API=/cmd
- SECRET=123456
- DEBUG=true
+17
View File
@@ -0,0 +1,17 @@
module github.com/umputun/remark/memory_store
go 1.12
require (
github.com/go-pkgz/jrpc v0.1.0
github.com/go-pkgz/lgr v0.6.3
github.com/jessevdk/go-flags v1.4.0
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.3.0
github.com/umputun/remark/backend v1.4.0
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 // indirect
)
replace github.com/umputun/remark/backend => ../../
replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1
+191
View File
@@ -0,0 +1,191 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro=
cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d h1:vs5Nf6IE0N/PwGJ8//zRed4gpCdcr99K2HzX7RuLOQ8=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-pkgz/auth v0.7.2/go.mod h1:ibOpZYISiaOvAHe2bsKj2s3v4AkMam2WxxIFn+zhulo=
github.com/go-pkgz/jrpc v0.1.0 h1:hNg/IyfEqJcSWOKkuHw0ZwcuGc9TDp7QZREsD2ycmiM=
github.com/go-pkgz/jrpc v0.1.0/go.mod h1:JxZsvoBklA50DNhELVJnJ567Rt+KrMH9rR3u515wvE8=
github.com/go-pkgz/lcw v0.3.1/go.mod h1:k+PY1CkCMTLXILtFoJOyK65Qqi9rkoTYunFH1vE/C0I=
github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
github.com/go-pkgz/lgr v0.6.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
github.com/go-pkgz/lgr v0.6.3 h1:n9pGk2paBV8w/Y/FVEq5MkwDmP33dnUPKbY4CyyygwM=
github.com/go-pkgz/lgr v0.6.3/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
github.com/go-pkgz/mongo v1.1.2 h1:2Vqn3CWQJkkx4gxxDiQUitAW2FN/CH26lKHkipmpKcc=
github.com/go-pkgz/mongo v1.1.2/go.mod h1:0NkWnzpiUxoL5fYZuttCtJrpC67oNDidfYxcdPqHTf0=
github.com/go-pkgz/repeater v1.1.2/go.mod h1:QfNR/a+xqjs+f9wSxWqOQlw9aQhmKlUaSwXCiZ+Ko2w=
github.com/go-pkgz/rest v1.4.1 h1:DmaVLPH2O7yLehrWOW0uz01d2mVHz9fBR/iuTiPRzaw=
github.com/go-pkgz/rest v1.4.1/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk=
github.com/go-pkgz/syncs v1.1.1/go.mod h1:bt9lxWRRJ9vOCMGc8Big8ttjYHLKP88ofj1y38UlaHE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/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/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/umputun/remark v1.4.0 h1:rJf4ndpvRDS7tOrtIpVkacItTxF5pVpXvDqtb1dX40M=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package main
import (
"fmt"
"os"
"github.com/go-pkgz/jrpc"
log "github.com/go-pkgz/lgr"
"github.com/jessevdk/go-flags"
"github.com/umputun/remark/memory_store/accessor"
"github.com/umputun/remark/memory_store/server"
)
// opts with all cli commands and flags
var opts struct {
API string `long:"api" env:"API" default:"/" description:"api root url"`
Port int `long:"port" env:"PORT" default:"8080" description:"rpc server port"`
AuthUser string `long:"auth-user" env:"AUTH_USER" default:"" description:"rpc auth user name"`
AuthPasswd string `long:"auth-passwd" env:"AUTH_PASSWD" default:"" description:"rpc auth password"`
Secret string `long:"secret" env:"SECRET" required:"true" description:"secret key"`
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
}
var revision = "unknown"
func main() {
fmt.Printf("remark42-memory module %s\n", revision)
if _, err := flags.Parse(&opts); err != nil {
os.Exit(2)
}
setupLog(opts.Dbg)
dataStore := accessor.NewMemData()
adminStore := accessor.NewMemAdminStore(opts.Secret)
rpcServer := jrpc.Server{
API: opts.API,
AuthUser: opts.AuthUser,
AuthPasswd: opts.AuthPasswd,
Version: revision,
AppName: "remark42-memory",
Logger: log.Default(),
}
srv := server.NewRPC(dataStore, adminStore, &rpcServer)
admRec := accessor.AdminRec{
SiteID: "remark",
IDs: []string{"dev_user"},
Email: "admin@example.com",
}
adminStore.Set("remark", admRec)
err := srv.Run(opts.Port)
log.Printf("[ERROR] server failed or terminated, %+v", err)
}
func setupLog(dbg bool) {
if dbg {
log.Setup(log.Debug, log.CallerFile, log.CallerFunc, log.Msec, log.LevelBraces)
return
}
log.Setup(log.Msec, log.LevelBraces)
}
+183
View File
@@ -0,0 +1,183 @@
package server
import (
"encoding/json"
"github.com/go-pkgz/jrpc"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/admin"
"github.com/umputun/remark/backend/app/store/engine"
)
// RPC handler wraps both engine and remote server and implements all handlers for data store and admin store
// Note: this file can be used as-is in any custom jrpc plugin
type RPC struct {
*jrpc.Server
eng engine.Interface
adm admin.Store
}
// NewRPC makes RPC instance and register handlers
func NewRPC(e engine.Interface, a admin.Store, r *jrpc.Server) *RPC {
res := &RPC{eng: e, adm: a, Server: r}
res.addHandlers()
return res
}
func (s *RPC) addHandlers() {
// data store handlers
s.Group("store", jrpc.HandlersGroup{
"create": s.createHndl,
"find": s.findHndl,
"get": s.getHndl,
"update": s.updateHndl,
"count": s.countHndl,
"info": s.infoHndl,
"flag": s.flagHndl,
"list_flags": s.listFlagsHndl,
"delete": s.deleteHndl,
"close": s.closeHndl,
})
// admin store handlers
s.Group("admin", jrpc.HandlersGroup{
"key": s.admKeyHndl,
"admins": s.admAdminsHndl,
"email": s.admEmailHndl,
})
}
func (s *RPC) createHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
comment := store.Comment{}
if err := json.Unmarshal(params, &comment); err != nil {
return jrpc.Response{Error: err.Error()}
}
commentID, err := s.eng.Create(comment)
return jrpc.EncodeResponse(id, commentID, err)
}
// Find comments
func (s *RPC) findHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FindRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
resp, err := s.eng.Find(req)
return jrpc.EncodeResponse(id, resp, err)
}
// Get comment
func (s *RPC) getHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.GetRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
comment, err := s.eng.Get(req)
return jrpc.EncodeResponse(id, comment, err)
}
// Update comment
func (s *RPC) updateHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
comment := store.Comment{}
if err := json.Unmarshal(params, &comment); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.eng.Update(comment)
return jrpc.EncodeResponse(id, nil, err)
}
// counts for site and users
func (s *RPC) countHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FindRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
count, err := s.eng.Count(req)
return jrpc.EncodeResponse(id, count, err)
}
// info get post meta info
func (s *RPC) infoHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.InfoRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
info, err := s.eng.Info(req)
return jrpc.EncodeResponse(id, info, err)
}
// flagHndl get and sets flag value
func (s *RPC) flagHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FlagRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
status, err := s.eng.Flag(req)
return jrpc.EncodeResponse(id, status, err)
}
// listFlagsHndl list flags for given request
func (s *RPC) listFlagsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.FlagRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
flags, err := s.eng.ListFlags(req)
return jrpc.EncodeResponse(id, flags, err)
}
// deleteHndl remove comment(s)
func (s *RPC) deleteHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
req := engine.DeleteRequest{}
if err := json.Unmarshal(params, &req); err != nil {
return jrpc.Response{Error: err.Error()}
}
err := s.eng.Delete(req)
return jrpc.EncodeResponse(id, nil, err)
}
// close store
func (s *RPC) closeHndl(id uint64, _ json.RawMessage) (rr jrpc.Response) {
if err := s.eng.Close(); err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.Response{}
}
// get admin key
func (s *RPC) admKeyHndl(id uint64, _ json.RawMessage) (rr jrpc.Response) {
key, err := s.adm.Key()
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, key, err)
}
// get admins list
func (s *RPC) admAdminsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var siteID string
if err := json.Unmarshal(params, &siteID); err != nil {
return jrpc.Response{Error: err.Error()}
}
admins, err := s.adm.Admins(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, admins, err)
}
// get admin email
func (s *RPC) admEmailHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
var siteID string
if err := json.Unmarshal(params, &siteID); err != nil {
return jrpc.Response{Error: err.Error()}
}
email, err := s.adm.Email(siteID)
if err != nil {
return jrpc.Response{Error: err.Error()}
}
return jrpc.EncodeResponse(id, email, err)
}
@@ -0,0 +1,318 @@
/*
* Copyright 2019 Umputun. All rights reserved.
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file.
*/
package server
import (
"fmt"
"math/rand"
"net/http"
"testing"
"time"
"github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/admin"
"github.com/umputun/remark/backend/app/store/engine"
"github.com/umputun/remark/memory_store/accessor"
)
func TestRPC_createHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
id, err := re.Create(store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}})
assert.NoError(t, err)
assert.Equal(t, "123456", id)
}
func TestRPC_findHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
comments, err := re.Find(findReq)
require.NoError(t, err)
assert.Equal(t, 0, len(comments))
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
comments, err = re.Find(findReq)
require.NoError(t, err)
assert.Equal(t, 1, len(comments))
assert.Equal(t, c, comments[0])
}
func TestRPC_getHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
req := engine.GetRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
_, err := re.Get(req)
assert.EqualError(t, err, "not found")
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
_, err = re.Create(c)
assert.NoError(t, err)
comment, err := re.Get(req)
assert.NoError(t, err)
assert.Equal(t, c, comment)
}
func TestRPC_updateHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
err := re.Update(c)
assert.EqualError(t, err, "not found")
_, err = re.Create(c)
assert.NoError(t, err)
c.Text = "updates"
err = re.Update(c)
assert.NoError(t, err)
req := engine.GetRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
comment, err := re.Get(req)
assert.NoError(t, err)
assert.Equal(t, c, comment)
}
func TestRPC_countHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
count, err := re.Count(findReq)
require.NoError(t, err)
assert.Equal(t, 0, count)
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
count, err = re.Count(findReq)
require.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestRPC_infoHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
infoReq := engine.InfoRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
info, err := re.Info(infoReq)
require.NoError(t, err)
assert.Equal(t, 1, len(info))
i := info[0]
assert.Equal(t, store.PostInfo{URL: "http://example.com/post1", Count: 1}, i)
}
func TestRPC_flagHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
flagReq := engine.FlagRequest{
Flag: engine.Verified,
Locator: store.Locator{
SiteID: "test-site",
},
UserID: "u1",
}
status, err := re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, false, status)
flagReq.Update = engine.FlagTrue
status, err = re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
flagReq.Update = engine.FlagNonSet
status, err = re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
}
func TestRPC_listFlagsHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
id, err := re.Create(c)
assert.NoError(t, err)
assert.Equal(t, "123456", id)
flagReq := engine.FlagRequest{
Flag: engine.Verified,
UserID: "u1",
Locator: store.Locator{
SiteID: "test-site",
},
}
flags, err := re.ListFlags(flagReq)
require.NoError(t, err)
assert.Equal(t, []interface{}{}, flags)
flagReq.Update = engine.FlagTrue
status, err := re.Flag(flagReq)
require.NoError(t, err)
assert.Equal(t, true, status)
flags, err = re.ListFlags(flagReq)
require.NoError(t, err)
assert.Equal(t, []interface{}{"u1"}, flags)
}
func TestRPC_deleteHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
req := engine.DeleteRequest{
Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
CommentID: "123456",
}
err := re.Delete(req)
assert.EqualError(t, err, "not found")
c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
_, err = re.Create(c)
assert.NoError(t, err)
err = re.Delete(req)
assert.NoError(t, err)
}
func TestRPC_closeHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
err := re.Close()
assert.NoError(t, err)
}
func TestRPC_admKeyHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
key, err := ra.Key()
assert.NoError(t, err)
assert.Equal(t, "secret", key)
}
func TestRPC_admAdminsHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
_, err := ra.Admins("bad site")
assert.EqualError(t, err, "site bad site not found")
admins, err := ra.Admins("test-site")
assert.NoError(t, err)
assert.Equal(t, []string{"id1", "id2"}, admins)
}
func TestRPC_admEmailHndl(t *testing.T) {
_, port, teardown := prepTestStore(t)
defer teardown()
api := fmt.Sprintf("http://localhost:%d/test", port)
ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
_, err := ra.Admins("bad site")
assert.EqualError(t, err, "site bad site not found")
email, err := ra.Email("test-site")
assert.NoError(t, err)
assert.Equal(t, "admin@example.com", email)
}
func prepTestStore(t *testing.T) (s *RPC, port int, teardown func()) {
port = 40000 + int(rand.Int31n(10000))
mg := accessor.NewMemData()
adm := accessor.NewMemAdminStore("secret")
s = NewRPC(mg, adm, &jrpc.Server{API: "/test", Logger: jrpc.NoOpLogger})
admRec := accessor.AdminRec{
SiteID: "test-site",
IDs: []string{"id1", "id2"},
Email: "admin@example.com",
}
adm.Set("test-site", admRec)
go func() {
t.Log(s.Run(port))
}()
time.Sleep(time.Millisecond * 10)
return s, port, func() {
require.NoError(t, s.Shutdown())
}
}
+4 -4
View File
@@ -14,6 +14,7 @@ import (
"time"
bolt "github.com/coreos/bbolt"
"github.com/go-pkgz/jrpc"
log "github.com/go-pkgz/lgr"
"github.com/kyokomi/emoji"
authcache "github.com/patrickmn/go-cache"
@@ -30,7 +31,6 @@ import (
"github.com/umputun/remark/backend/app/notify"
"github.com/umputun/remark/backend/app/rest/api"
"github.com/umputun/remark/backend/app/rest/proxy"
"github.com/umputun/remark/backend/app/rpc"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/admin"
"github.com/umputun/remark/backend/app/store/engine"
@@ -108,7 +108,7 @@ type StoreGroup struct {
Path string `long:"path" env:"PATH" default:"./var" description:"parent dir for bolt files"`
Timeout time.Duration `long:"timeout" env:"TIMEOUT" default:"30s" description:"bolt timeout"`
} `group:"bolt" namespace:"bolt" env-namespace:"BOLT"`
RPC RPCGroup `group:"rpc" namespace:"rpc" env-namespace:"TPC"`
RPC RPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"`
}
// ImageGroup defines options group for store pictures
@@ -438,7 +438,7 @@ func (s *ServerCommand) makeDataStore() (result engine.Interface, err error) {
}
result, err = engine.NewBoltDB(bolt.Options{Timeout: s.Store.Bolt.Timeout}, sites...)
case "rpc":
r := &engine.RPC{Client: rpc.Client{
r := &engine.RPC{Client: jrpc.Client{
API: s.Store.RPC.API,
Client: http.Client{Timeout: s.Store.RPC.TimeOut},
AuthUser: s.Store.RPC.AuthUser,
@@ -503,7 +503,7 @@ func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
}
return admin.NewStaticStore(s.SharedSecret, s.Admin.Shared.Admins, s.Admin.Shared.Email), nil
case "rpc":
r := &admin.RPC{Client: rpc.Client{
r := &admin.RPC{Client: jrpc.Client{
API: s.Admin.RPC.API,
Client: http.Client{Timeout: s.Admin.RPC.TimeOut},
AuthUser: s.Admin.RPC.AuthUser,
-97
View File
@@ -1,97 +0,0 @@
package rpc
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClient_Call(t *testing.T) {
ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"result":"12345"}`)
defer ts.Close()
c := Client{API: ts.URL, Client: http.Client{}}
resp, err := c.Call("test", 123, "abc")
assert.NoError(t, err)
res := ""
err = json.Unmarshal(*resp.Result, &res)
assert.NoError(t, err)
assert.Equal(t, "12345", res)
t.Logf("%v %T", res, res)
}
func TestClient_CallWithObject(t *testing.T) {
ts := testServer(t, `{"method":"test","params":{"F1":123,"F2":"abc","F3":"2019-06-09T23:03:55Z"},"id":1}`, `{"result":"12345"}`)
defer ts.Close()
c := Client{API: ts.URL, Client: http.Client{}}
obj := struct {
F1 int
F2 string
F3 time.Time
}{
F1: 123,
F2: "abc",
F3: time.Date(2019, 6, 9, 23, 3, 55, 0, time.UTC),
}
resp, err := c.Call("test", obj)
assert.NoError(t, err)
res := ""
err = json.Unmarshal(*resp.Result, &res)
assert.NoError(t, err)
assert.Equal(t, "12345", res)
t.Logf("%v %T", res, res)
}
func TestClient_CallWithNoParams(t *testing.T) {
ts := testServer(t, `{"method":"test","id":1}`, `{"result":"12345"}`)
defer ts.Close()
c := Client{API: ts.URL, Client: http.Client{}}
resp, err := c.Call("test")
assert.NoError(t, err)
res := ""
err = json.Unmarshal(*resp.Result, &res)
assert.NoError(t, err)
assert.Equal(t, "12345", res)
t.Logf("%v %T", res, res)
}
func TestClient_CallError(t *testing.T) {
ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"error":"some error"}`)
defer ts.Close()
c := Client{API: ts.URL, Client: http.Client{}}
_, err := c.Call("test", 123, "abc")
assert.EqualError(t, err, "some error")
}
func TestClient_CallBadResponse(t *testing.T) {
ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"result":"12345 invalid}`)
defer ts.Close()
c := Client{API: ts.URL, Client: http.Client{}}
_, err := c.Call("test", 123, "abc")
assert.NotNil(t, err)
}
func TestClient_CallBadRemote(t *testing.T) {
ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"result":"12345"}`)
defer ts.Close()
c := Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}
_, err := c.Call("test", 123)
assert.NotNil(t, err)
}
func testServer(t *testing.T, req, resp string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, req, string(body))
t.Logf("req: %s", string(body))
fmt.Fprintf(w, resp)
}))
}
-23
View File
@@ -1,23 +0,0 @@
// Package rpc implements client ans server for RPC-like communication with remote storage.
// The protocol is somewhat simplified version of json-rpc with a single POST call sending
// Request json (method name and the list of parameters) and receiving back json Response with "result" json
// and error string
package rpc
import (
"encoding/json"
)
// Request encloses method name and all params
type Request struct {
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
ID uint64 `json:"id"`
}
// Response encloses result and error received from remote server
type Response struct {
Result *json.RawMessage `json:"result,omitempty"`
Error string `json:"error,omitempty"`
ID uint64 `json:"id"`
}
-245
View File
@@ -1,245 +0,0 @@
package rpc
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServerPrimitiveTypes(t *testing.T) {
s := Server{API: "/v1/cmd"}
type respData struct {
Res1 string
Res2 bool
}
s.Add("test", func(id uint64, params json.RawMessage) Response {
args := []interface{}{}
if err := json.Unmarshal(params, &args); err != nil {
return Response{Error: err.Error()}
}
t.Logf("%+v", args)
assert.Equal(t, 3, len(args))
assert.Equal(t, "blah", args[0].(string))
assert.Equal(t, 42., args[1].(float64))
assert.Equal(t, true, args[2].(bool))
r, err := s.EncodeResponse(id, respData{"res blah", true}, nil)
assert.NoError(t, err)
return r
})
go func() { _ = s.Run(9091) }()
defer func() { assert.NoError(t, s.Shutdown()) }()
time.Sleep(10 * time.Millisecond)
// check with direct http call
clientReq := Request{Method: "test", Params: []interface{}{"blah", 42, true}, ID: 123}
b := bytes.Buffer{}
require.NoError(t, json.NewEncoder(&b).Encode(clientReq))
resp, err := http.Post("http://127.0.0.1:9091/v1/cmd", "application/json", &b)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
data, err := ioutil.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, `{"result":{"Res1":"res blah","Res2":true},"id":123}`+"\n", string(data))
// check with client call
c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
r, err := c.Call("test", "blah", 42, true)
assert.NoError(t, err)
assert.Equal(t, "", r.Error)
res := respData{}
err = json.Unmarshal(*r.Result, &res)
assert.NoError(t, err)
assert.Equal(t, respData{Res1: "res blah", Res2: true}, res)
assert.Equal(t, uint64(1), r.ID)
}
func TestServerWithObject(t *testing.T) {
s := Server{API: "/v1/cmd"}
type respData struct {
Res1 string
Res2 bool
}
type reqData struct {
Time time.Time
F1 string
F2 time.Duration
}
s.Add("test", func(id uint64, params json.RawMessage) Response {
arg := reqData{}
if err := json.Unmarshal(params, &arg); err != nil {
return Response{Error: err.Error()}
}
t.Logf("%+v", arg)
r, err := s.EncodeResponse(id, respData{"res blah", true}, nil)
assert.NoError(t, err)
return r
})
go func() { _ = s.Run(9091) }()
defer func() { assert.NoError(t, s.Shutdown()) }()
time.Sleep(10 * time.Millisecond)
c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
r, err := c.Call("test", reqData{Time: time.Now(), F1: "sawert", F2: time.Minute})
assert.NoError(t, err)
assert.Equal(t, "", r.Error)
res := respData{}
err = json.Unmarshal(*r.Result, &res)
assert.NoError(t, err)
assert.Equal(t, respData{Res1: "res blah", Res2: true}, res)
}
func TestServerMethodNotImplemented(t *testing.T) {
s := Server{}
ts := httptest.NewServer(http.HandlerFunc(s.handler))
defer ts.Close()
s.Add("test", func(id uint64, params json.RawMessage) Response {
return Response{}
})
r := Request{Method: "blah"}
buf := bytes.Buffer{}
assert.NoError(t, json.NewEncoder(&buf).Encode(r))
resp, err := http.Post(ts.URL, "application/json", &buf)
require.NoError(t, err)
assert.Equal(t, http.StatusNotImplemented, resp.StatusCode)
assert.EqualError(t, s.Shutdown(), "http server is not running")
}
func TestServerWithAuth(t *testing.T) {
s := Server{API: "/v1/cmd", AuthUser: "user", AuthPasswd: "passwd"}
s.Add("test", func(id uint64, params json.RawMessage) Response {
args := []interface{}{}
if err := json.Unmarshal(params, &args); err != nil {
return Response{Error: err.Error()}
}
t.Logf("%+v", args)
assert.Equal(t, 3, len(args))
assert.Equal(t, "blah", args[0].(string))
assert.Equal(t, 42., args[1].(float64))
assert.Equal(t, true, args[2].(bool))
r, err := s.EncodeResponse(id, "res blah", nil)
assert.NoError(t, err)
return r
})
go func() { _ = s.Run(9091) }()
time.Sleep(10 * time.Millisecond)
defer func() { assert.NoError(t, s.Shutdown()) }()
c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}, AuthUser: "user", AuthPasswd: "passwd"}
r, err := c.Call("test", "blah", 42, true)
assert.NoError(t, err)
assert.Equal(t, "", r.Error)
val := ""
err = json.Unmarshal(*r.Result, &val)
assert.NoError(t, err)
assert.Equal(t, "res blah", val)
c = Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
_, err = c.Call("test", "blah", 42, true)
assert.EqualError(t, err, "bad status 401 Unauthorized for test")
}
func TestServerErrReturn(t *testing.T) {
s := Server{API: "/v1/cmd", AuthUser: "user", AuthPasswd: "passwd"}
s.Add("test", func(id uint64, params json.RawMessage) Response {
args := []interface{}{}
if err := json.Unmarshal(params, &args); err != nil {
return Response{Error: err.Error()}
}
t.Logf("%+v", args)
assert.Equal(t, 3, len(args))
assert.Equal(t, "blah", args[0].(string))
assert.Equal(t, 42., args[1].(float64))
assert.Equal(t, true, args[2].(bool))
r, err := s.EncodeResponse(id, "res blah", errors.New("some error"))
assert.NoError(t, err)
return r
})
go func() { _ = s.Run(9091) }()
defer func() { assert.NoError(t, s.Shutdown()) }()
time.Sleep(10 * time.Millisecond)
c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}, AuthUser: "user", AuthPasswd: "passwd"}
_, err := c.Call("test", "blah", 42, true)
assert.EqualError(t, err, "some error")
}
func TestServerGroup(t *testing.T) {
s := Server{API: "/v1/cmd"}
s.Group("pre", HandlersGroup{
"fn1": func(id uint64, params json.RawMessage) Response {
return Response{}
},
"fn2": func(id uint64, params json.RawMessage) Response {
return Response{}
},
})
go func() { _ = s.Run(9091) }()
defer func() { assert.NoError(t, s.Shutdown()) }()
time.Sleep(10 * time.Millisecond)
c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
_, err := c.Call("fn1")
assert.EqualError(t, err, "bad status 501 Not Implemented for fn1")
_, err = c.Call("pre.fn1")
assert.NoError(t, err)
_, err = c.Call("pre.fn2")
assert.NoError(t, err)
}
func TestServerAddLate(t *testing.T) {
s := Server{API: "/v1/cmd"}
s.Add("fn1", func(id uint64, params json.RawMessage) Response {
return Response{}
})
go func() { _ = s.Run(9091) }()
defer func() { assert.NoError(t, s.Shutdown()) }()
time.Sleep(10 * time.Millisecond)
// too late, ignored after run
s.Add("fn2", func(id uint64, params json.RawMessage) Response {
return Response{}
})
c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
_, err := c.Call("fn1")
assert.NoError(t, err)
_, err = c.Call("fn2")
assert.EqualError(t, err, "bad status 501 Not Implemented for fn2")
}
func TestServerNoHandlers(t *testing.T) {
s := Server{API: "/v1/cmd", AuthUser: "user", AuthPasswd: "passwd"}
assert.EqualError(t, s.Run(9091), "nothing mapped for dispatch, Add has to be called prior to Run")
}
+2 -2
View File
@@ -9,12 +9,12 @@ package admin
import (
"encoding/json"
"github.com/umputun/remark/backend/app/rpc"
"github.com/go-pkgz/jrpc"
)
// RPC implements remote engine and delegates all Calls to remote http server
type RPC struct {
rpc.Client
jrpc.Client
}
// Key returns the key, same for all sites
+6 -7
View File
@@ -13,17 +13,16 @@ import (
"net/http/httptest"
"testing"
"github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/backend/app/rpc"
)
func TestRemote_Key(t *testing.T) {
ts := testServer(t, `{"method":"admin.key","id":1}`,
`{"result":"12345","id":1}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
@@ -35,10 +34,10 @@ func TestRemote_Key(t *testing.T) {
}
func TestRemote_Admins(t *testing.T) {
ts := testServer(t, `{"method":"admin.admins","params":["site-1"],"id":1}`,
ts := testServer(t, `{"method":"admin.admins","params":"site-1","id":1}`,
`{"result":["id1","id2"],"id":1}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
@@ -50,10 +49,10 @@ func TestRemote_Admins(t *testing.T) {
}
func TestRemote_Email(t *testing.T) {
ts := testServer(t, `{"method":"admin.email","params":["site-1"],"id":1}`,
ts := testServer(t, `{"method":"admin.email","params":"site-1","id":1}`,
`{"result":"bbb@example.com","id":1}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
+3 -2
View File
@@ -3,13 +3,14 @@ package engine
import (
"encoding/json"
"github.com/umputun/remark/backend/app/rpc"
"github.com/go-pkgz/jrpc"
"github.com/umputun/remark/backend/app/store"
)
// RPC implements remote engine and delegates all Calls to remote http server
type RPC struct {
rpc.Client
jrpc.Client
}
// Create comment and return ID
+15 -15
View File
@@ -9,10 +9,10 @@ import (
"testing"
"time"
"github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/backend/app/rpc"
"github.com/umputun/remark/backend/app/store"
)
@@ -20,7 +20,7 @@ func TestRemote_Create(t *testing.T) {
ts := testServer(t, `{"method":"store.create","params":{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"},"id":1}`,
`{"result":"12345","id":1}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var eng Interface = &c
_ = eng
@@ -35,7 +35,7 @@ func TestRemote_Create(t *testing.T) {
func TestRemote_Get(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, `{"result":{"id":"123","pid":"","text":"msg","delete":true}}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
res, err := c.Get(req)
@@ -47,7 +47,7 @@ func TestRemote_Get(t *testing.T) {
func TestRemote_GetWithErrorResult(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, `{"error":"failed"}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -57,7 +57,7 @@ func TestRemote_GetWithErrorResult(t *testing.T) {
func TestRemote_GetWithErrorDecode(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, ``)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -65,7 +65,7 @@ func TestRemote_GetWithErrorDecode(t *testing.T) {
}
func TestRemote_GetWithErrorRemote(t *testing.T) {
c := RPC{Client: rpc.Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}}
c := RPC{Client: jrpc.Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -81,7 +81,7 @@ func TestRemote_FailedStatus(t *testing.T) {
w.WriteHeader(400)
}))
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -91,7 +91,7 @@ func TestRemote_FailedStatus(t *testing.T) {
func TestRemote_Update(t *testing.T) {
ts := testServer(t, `{"method":"store.update","params":{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site123","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"},"id":1}`, `{}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
err := c.Update(store.Comment{ID: "123", Locator: store.Locator{URL: "http://example.com/url", SiteID: "site123"},
Text: "msg"})
@@ -102,7 +102,7 @@ func TestRemote_Update(t *testing.T) {
func TestRemote_Find(t *testing.T) {
ts := testServer(t, `{"method":"store.find","params":{"locator":{"url":"http://example.com/url"},"sort":"-time","since":"0001-01-01T00:00:00Z","limit":10},"id":1}`, `{"result":[{"text":"1"},{"text":"2"}]}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Find(FindRequest{Locator: store.Locator{URL: "http://example.com/url"}, Sort: "-time", Limit: 10})
assert.NoError(t, err)
@@ -112,7 +112,7 @@ func TestRemote_Find(t *testing.T) {
func TestRemote_Info(t *testing.T) {
ts := testServer(t, `{"method":"store.info","params":{"locator":{"url":"http://example.com/url"},"limit":10,"skip":5,"ro_age":10},"id":1}`, `{"result":[{"url":"u1","count":22},{"url":"u2","count":33}]}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Info(InfoRequest{Locator: store.Locator{URL: "http://example.com/url"},
Limit: 10, Skip: 5, ReadOnlyAge: 10})
@@ -123,7 +123,7 @@ func TestRemote_Info(t *testing.T) {
func TestRemote_Flag(t *testing.T) {
ts := testServer(t, `{"method":"store.flag","params":{"flag":"verified","locator":{"url":"http://example.com/url"}},"id":1}`, `{"result":false}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Flag(FlagRequest{Locator: store.Locator{URL: "http://example.com/url"}, Flag: Verified})
assert.NoError(t, err)
@@ -133,7 +133,7 @@ func TestRemote_Flag(t *testing.T) {
func TestRemote_ListFlag(t *testing.T) {
ts := testServer(t, `{"method":"store.list_flags","params":{"flag":"blocked","locator":{"site":"site_id","url":""}},"id":1}`, `{"result":[{"ID":"id1"},{"ID":"id2"}]}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.ListFlags(FlagRequest{Locator: store.Locator{SiteID: "site_id"}, Flag: Blocked})
assert.NoError(t, err)
assert.Equal(t, []interface{}{map[string]interface{}{"ID": "id1"}, map[string]interface{}{"ID": "id2"}}, res)
@@ -142,7 +142,7 @@ func TestRemote_ListFlag(t *testing.T) {
func TestRemote_Count(t *testing.T) {
ts := testServer(t, `{"method":"store.count","params":{"locator":{"url":"http://example.com/url"},"since":"0001-01-01T00:00:00Z"},"id":1}`, `{"result":11}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Count(FindRequest{Locator: store.Locator{URL: "http://example.com/url"}})
assert.NoError(t, err)
@@ -153,7 +153,7 @@ func TestRemote_Delete(t *testing.T) {
ts := testServer(t, `{"method":"store.delete","params":{"locator":{"url":"http://example.com/url"},"del_mode":0},"id":1}`,
`{}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
err := c.Delete(DeleteRequest{Locator: store.Locator{URL: "http://example.com/url"}})
assert.NoError(t, err)
@@ -162,7 +162,7 @@ func TestRemote_Delete(t *testing.T) {
func TestRemote_Close(t *testing.T) {
ts := testServer(t, `{"method":"store.close","id":1}`, `{}`)
defer ts.Close()
c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
err := c.Close()
assert.NoError(t, err)
}
+1
View File
@@ -469,6 +469,7 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
func (s *DataStore) IsAdmin(siteID string, userID string) bool {
admins, err := s.AdminStore.Admins(siteID)
if err != nil {
log.Printf("[WARN] can't get admins for %s, %v", siteID, err)
return false
}
for _, a := range admins {
+3 -2
View File
@@ -9,12 +9,13 @@ require (
github.com/PuerkitoBio/goquery v1.5.0
github.com/coreos/bbolt v1.3.3
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/didip/tollbooth v4.0.0+incompatible
github.com/didip/tollbooth v4.0.2+incompatible
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.1
github.com/go-pkgz/auth v0.7.2
github.com/go-pkgz/jrpc v0.1.0
github.com/go-pkgz/lcw v0.3.1
github.com/go-pkgz/lgr v0.6.3
github.com/go-pkgz/repeater v1.1.2
@@ -35,7 +36,7 @@ require (
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9
golang.org/x/net v0.0.0-20190628185345-da137c7871d7
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
gopkg.in/russross/blackfriday.v2 v2.0.1
)
+7
View File
@@ -21,6 +21,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/didip/tollbooth v4.0.0+incompatible h1:ayQZYuF5QOxx3NdYRNuRVFLv9/2b64JtSUlewb+0TMo=
github.com/didip/tollbooth v4.0.0+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d h1:vs5Nf6IE0N/PwGJ8//zRed4gpCdcr99K2HzX7RuLOQ8=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -34,6 +36,8 @@ github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-pkgz/auth v0.7.2 h1:+LvAgqwQtYuWphpZE8qLtspVd65+VgreJkFLsKrtmmk=
github.com/go-pkgz/auth v0.7.2/go.mod h1:ibOpZYISiaOvAHe2bsKj2s3v4AkMam2WxxIFn+zhulo=
github.com/go-pkgz/jrpc v0.1.0 h1:hNg/IyfEqJcSWOKkuHw0ZwcuGc9TDp7QZREsD2ycmiM=
github.com/go-pkgz/jrpc v0.1.0/go.mod h1:JxZsvoBklA50DNhELVJnJ567Rt+KrMH9rR3u515wvE8=
github.com/go-pkgz/lcw v0.3.1 h1:PhfB0xNUawLMlx5rXvOTIc7d5LMrr1GM9vIzmG96aUI=
github.com/go-pkgz/lcw v0.3.1/go.mod h1:k+PY1CkCMTLXILtFoJOyK65Qqi9rkoTYunFH1vE/C0I=
github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
@@ -115,6 +119,7 @@ github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/umputun/remark v1.4.0 h1:rJf4ndpvRDS7tOrtIpVkacItTxF5pVpXvDqtb1dX40M=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -153,6 +158,8 @@ golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
-16
View File
@@ -14,22 +14,6 @@ server {
limit_conn perip 10;
location / {
#CORS
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, PUT, POST, DELETE, OPTIONS';
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With";
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, PUT, POST, DELETE, OPTIONS';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain; charset=UTF-8';
return 204;
}
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+4 -4
View File
@@ -7,9 +7,7 @@ This is a generic middleware to rate-limit HTTP requests.
**NOTE 1:** This library is considered finished.
**NOTE 2:** In the coming weeks, I will be removing thirdparty modules and moving them to their own dedicated repos.
**NOTE 3:** Major version changes are backward-incompatible. `v2.0.0` streamlines the ugliness of the old API.
**NOTE 2:** Major version changes are backward-incompatible. `v2.0.0` streamlines the ugliness of the old API.
## Versions
@@ -21,7 +19,7 @@ This is a generic middleware to rate-limit HTTP requests.
**v3.x.x:** Apparently we have been using golang.org/x/time/rate incorrectly. See issue #48. It always limit X number per 1 second. The time duration is not changeable, so it does not make sense to pass TTL to tollbooth.
## Five Minutes Tutorial
## Five Minute Tutorial
```go
package main
@@ -161,4 +159,6 @@ Sometimes, other frameworks require a little bit of shim to use Tollbooth. These
* [Stopwatch](https://github.com/didip/stopwatch): A small library to measure latency of things. Useful if you want to report latency data to Graphite.
* [LaborUnion](https://github.com/didip/laborunion): A dynamic worker pool library.
* [Gomet](https://github.com/didip/gomet): Simple HTTP client & server long poll library for Go. Useful for receiving live updates without needing Websocket.
+21 -11
View File
@@ -6,10 +6,11 @@ import (
"strings"
"fmt"
"math"
"github.com/didip/tollbooth/errors"
"github.com/didip/tollbooth/libstring"
"github.com/didip/tollbooth/limiter"
"math"
)
// setResponseHeaders configures X-Rate-Limit-Limit and X-Rate-Limit-Duration
@@ -58,18 +59,21 @@ func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
if libstring.StringInSlice(lmtMethods, r.Method) {
for headerKey, headerValues := range lmtHeaders {
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
// If header values are empty, rate-limit all request with headerKey.
// If header values are empty, rate-limit all request containing headerKey.
username, _, ok := r.BasicAuth()
if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, username})
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, r.Header.Get(headerKey), username})
}
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
// If header values are not empty, rate-limit all request with headerKey and headerValues.
for _, headerValue := range headerValues {
username, _, ok := r.BasicAuth()
if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue, username})
if r.Header.Get(headerKey) == headerValue {
username, _, ok := r.BasicAuth()
if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue, username})
}
break
}
}
}
@@ -82,12 +86,15 @@ func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
for headerKey, headerValues := range lmtHeaders {
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
// If header values are empty, rate-limit all request with headerKey.
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey})
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, r.Header.Get(headerKey)})
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
// If header values are not empty, rate-limit all request with headerKey and headerValues.
// We are only limiting if request's header value is defined inside `headerValues`.
for _, headerValue := range headerValues {
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue})
if r.Header.Get(headerKey) == headerValue {
sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue})
break
}
}
}
}
@@ -113,12 +120,15 @@ func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
for headerKey, headerValues := range lmtHeaders {
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
// If header values are empty, rate-limit all request with headerKey.
sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey})
sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey, r.Header.Get(headerKey)})
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
// If header values are not empty, rate-limit all request with headerKey and headerValues.
for _, headerValue := range headerValues {
sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey, headerValue})
if r.Header.Get(headerKey) == headerValue {
sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey, headerValue})
break
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
# 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
+70
View File
@@ -0,0 +1,70 @@
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0
gocyclo:
min-complexity: 15
maligned:
suggest-new: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
misspell:
locale: US
lll:
line-length: 140
gocritic:
enabled-tags:
- performance
- style
- experimental
disabled-checks:
- wrapperFunc
linters:
disable-all: true
enable:
- megacheck
- golint
- govet
- unconvert
- megacheck
- structcheck
- gas
- gocyclo
- dupl
- misspell
- unparam
- varcheck
- deadcode
- typecheck
- ineffassign
- varcheck
- stylecheck
- gochecknoinits
- scopelint
- gocritic
- golint
- nakedret
- gosimple
- prealloc
fast: false
run:
# modules-download-mode: vendor
skip-dirs:
- vendor
tests: true
issues:
exclude-rules:
- text: "weak cryptographic primitive"
linters:
- gosec
service:
golangci-lint-version: 1.17.x
+21
View File
@@ -0,0 +1,21 @@
language: go
go:
- "1.12.x"
install: true
before_install:
- export TZ=America/Chicago
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
- go get github.com/mattn/goveralls
- export PATH=$(pwd)/bin:$PATH
script:
- GO111MODULE=on go get ./...
- GO111MODULE=on go mod vendor
- GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
- GO111MODULE=on go test -v -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
- golangci-lint run -v || travis_terminate 1;
- $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Umputun
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.
+99
View File
@@ -0,0 +1,99 @@
# jrpc - rpc with json [![Build Status](https://travis-ci.org/go-pkgz/jrpc.svg?branch=master)](https://travis-ci.org/go-pkgz/jrpc) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/jrpc)](https://goreportcard.com/report/github.com/go-pkgz/jrpc) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/jrpc/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/jrpc?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/jrpc?status.svg)](https://godoc.org/github.com/go-pkgz/jrpc)
jrpc library provides client and server for RPC-like communication over HTTP with json encoded messages.
The protocol is a somewhat simplified version of json-rpc with a single POST call sending Request json
(method name and the list of parameters) moreover, receiving json Response with result data and an error string.
## Usage
### Plugin (server)
```go
// Server wraps jrpc.Server and adds synced map to store data
type Puglin struct {
*jrpc.Server
}
// create plugin (jrpc server)
plugin := jrpcServer{
Server: &jrpc.Server{
API: "/command", // base url for rpc calls
AuthUser: "user", // basic auth user name
AuthPasswd: "password", // basic auth password
AppName: "jrpc-example", // plugin name for headers
Logger: logger,
},
}
plugin.Add("mycommand", func(id uint64, params json.RawMessage) Response {
return jrpc.EncodeResponse(id, "hello, it works", nil)
})
```
### Application (client)
```go
// Client makes jrpc.Client and invoke remote call
rpcClient := jrpc.Client{
API: "http://127.0.0.1:8080/command",
Client: http.Client{},
AuthUser: "user",
AuthPasswd: "password",
}
resp, err := rpcClient.Call("mycommand")
var message string
if err = json.Unmarshal(*resp.Result, &message); err != nil {
panic(err)
}
```
*for functional examples for both plugin and application see [_example](https://github.com/go-pkgz/jrpc/tree/master/_example)*
## Technical details
* `jrpc.Server` runs on user-defined port as a regular http server
* Server accepts a single POST request on user-defined url with [Request](https://github.com/go-pkgz/jrpc/blob/master/jrpc.go#L12) sent as json payload
<details><summary>request details and an example:</summary>
```go
type Request struct {
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
ID uint64 `json:"id"`
}
```
example:
```json
{
"method":"test",
"params":[123,"abc"],
"id":1
}
```
</details>
* Params can be a struct, primitive type or slice of values, even with different types.
* Server defines `ServerFn` handler function to react on a POST request. The handler provided by the user.
* Communication between the server and the caller can be protected with basic auth.
* [Client](https://github.com/go-pkgz/jrpc/blob/master/client.go) provides a single method `Call` and return `Response`
<details><summary>response details:</summary>
```go
// Response encloses result and error received from remote server
type Response struct {
Result *json.RawMessage `json:"result,omitempty"`
Error string `json:"error,omitempty"`
ID uint64 `json:"id"`
}
```
</details>
* User should encode and decode json payloads on the application level, see provided [examples](https://github.com/go-pkgz/jrpc/tree/master/_example)
* `jrpc.Server` doesn't support https internally (yet). If used on exposed or non-private networks, should be proxied with something providing https termination (nginx and others).
## Status
The code was extracted from [remark42](https://github.com/umputun/remark) and still under development. Until v1.x released the
API & protocol may change.
@@ -1,26 +1,29 @@
package rpc
package jrpc
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
"sync/atomic"
"github.com/pkg/errors"
)
// Client implements remote engine and delegates all calls to remote http server
// if AuthUser and AuthPasswd defined will be used for basic auth in each call to server
type Client struct {
API string
Client http.Client
AuthUser string
AuthPasswd string
API string // URL to jrpc server with entrypoint, i.e. http://127.0.0.1:8080/command
Client http.Client // http client injected by user
AuthUser string // basic auth user name, should match Server.AuthUser, optional
AuthPasswd string // basic auth password, should match Server.AuthPasswd, optional
id uint64
id uint64 // used with atomic to populate unique id to Request.ID
}
// Call remote server with given method and arguments
// Call remote server with given method and arguments.
// Empty args will be ignored, single arg will be marshaled as-us and multiple args marshaled as []interface{}.
// Returns Response and error. Note: Response has it's own Error field, but that onw controlled by server.
// Returned error represent client-level errors, like failed http call, failed marshaling and so on.
func (r *Client) Call(method string, args ...interface{}) (*Response, error) {
var b []byte
@@ -32,7 +35,7 @@ func (r *Client) Call(method string, args ...interface{}) (*Response, error) {
if err != nil {
return nil, errors.Wrapf(err, "marshaling failed for %s", method)
}
case len(args) == 1 && reflect.TypeOf(args[0]).Kind() == reflect.Struct:
case len(args) == 1:
b, err = json.Marshal(Request{Method: method, Params: args[0], ID: atomic.AddUint64(&r.id, 1)})
if err != nil {
return nil, errors.Wrapf(err, "marshaling failed for %s", method)
+16
View File
@@ -0,0 +1,16 @@
module github.com/go-pkgz/jrpc
go 1.12
require (
github.com/didip/tollbooth v4.0.2+incompatible
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/render v1.0.1
github.com/go-pkgz/rest v1.4.1
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.3.0
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
)
+32
View File
@@ -0,0 +1,32 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d h1:vs5Nf6IE0N/PwGJ8//zRed4gpCdcr99K2HzX7RuLOQ8=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-pkgz/rest v1.4.1 h1:DmaVLPH2O7yLehrWOW0uz01d2mVHz9fBR/iuTiPRzaw=
github.com/go-pkgz/rest v1.4.1/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+40
View File
@@ -0,0 +1,40 @@
// Package jrpc implements client and server for RPC-like communication over HTTP with json encoded messages.
// The protocol is somewhat simplified version of json-rpc with a single POST call sending Request json
// (method name and the list of parameters) and receiving back json Response with "result" json
// and error string
package jrpc
import (
"encoding/json"
)
// Request encloses method name and all params
type Request struct {
Method string `json:"method"` // method (function) name
Params interface{} `json:"params,omitempty"` // function arguments
ID uint64 `json:"id"` // unique call id
}
// Response encloses result and error received from remote server
type Response struct {
Result *json.RawMessage `json:"result,omitempty"` // response json
Error string `json:"error,omitempty"` // optional remote (server side / plugin side) error
ID uint64 `json:"id"` // unique call id, echoed Request.ID to allow calls tracing
}
// EncodeResponse convert anything (type interface{}) and incoming error (if any) to Response
func EncodeResponse(id uint64, resp interface{}, e error) Response {
v, err := json.Marshal(&resp)
if err != nil {
return Response{Error: err.Error()}
}
if e != nil {
return Response{ID: id, Result: nil, Error: e.Error()} // pass input error
}
raw := json.RawMessage{}
if err := raw.UnmarshalJSON(v); err != nil {
return Response{Error: err.Error()}
}
return Response{ID: id, Result: &raw}
}
@@ -1,4 +1,4 @@
package rpc
package jrpc
import (
"context"
@@ -13,21 +13,19 @@ import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
log "github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
"github.com/go-pkgz/rest"
"github.com/go-pkgz/rest/logger"
"github.com/pkg/errors"
"github.com/umputun/remark/backend/app/rest"
)
// Server is json-rpc server with an optional basic auth
type Server struct {
API string
AuthUser string
AuthPasswd string
Version string
AppName string
API string // url path, i.e. "/command" or "/rpc" etc.
AuthUser string // basic auth user name, should match Client.AuthUser, optional
AuthPasswd string // basic auth password, should match Client.AuthPasswd, optional
Version string // server version, injected from main and used for informational headers only
AppName string // plugin name, injected from main and used for informational headers only
Logger L // logger, if nil will default to NoOpLogger
funcs struct {
m map[string]ServerFn
@@ -40,26 +38,26 @@ type Server struct {
}
}
// Encoder is a function to encode call's result to Response
type Encoder func(id uint64, resp interface{}, e error) (Response, error)
// ServerFn handler registered for each method with Add
// Implementations provided by consumer and define response logic.
// ServerFn handler registered for each method with Add or Group.
// Implementations provided by consumer and defines response logic.
type ServerFn func(id uint64, params json.RawMessage) Response
// Run http server on given port
func (s *Server) Run(port int) error {
if s.Logger == nil {
s.Logger = NoOpLogger
}
if s.AuthUser == "" || s.AuthPasswd == "" {
log.Print("[WARN] extension server runs without auth")
s.Logger.Logf("[WARN] extension server runs without auth")
}
if s.funcs.m == nil && len(s.funcs.m) == 0 {
return errors.Errorf("nothing mapped for dispatch, Add has to be called prior to Run")
}
router := chi.NewRouter()
router.Use(middleware.Throttle(1000), middleware.RealIP, R.Recoverer(log.Default()))
router.Use(R.AppInfo(s.AppName, "umputun", s.Version), R.Ping)
logInfoWithBody := logger.New(logger.Log(log.Default()), logger.WithBody, logger.Prefix("[INFO]")).Handler
router.Use(middleware.Throttle(1000), middleware.RealIP, rest.Recoverer(s.Logger))
router.Use(rest.AppInfo(s.AppName, "umputun", s.Version), rest.Ping)
logInfoWithBody := logger.New(logger.Log(s.Logger), logger.WithBody, logger.Prefix("[INFO]")).Handler
router.Use(middleware.Timeout(5 * time.Second))
router.Use(logInfoWithBody, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(1000, nil)), middleware.NoCache)
router.Use(s.basicAuth)
@@ -76,26 +74,10 @@ func (s *Server) Run(port int) error {
}
s.httpServer.Unlock()
log.Printf("[INFO] listen on %d", port)
s.Logger.Logf("[INFO] listen on %d", port)
return s.httpServer.ListenAndServe()
}
// EncodeResponse convert anything to Response
func (s *Server) EncodeResponse(id uint64, resp interface{}, e error) (Response, error) {
v, err := json.Marshal(&resp)
if err != nil {
return Response{}, err
}
if e != nil {
return Response{ID: id, Result: nil, Error: e.Error()}, nil
}
raw := json.RawMessage{}
if err = raw.UnmarshalJSON(v); err != nil {
return Response{}, err
}
return Response{ID: id, Result: &raw}, nil
}
// Shutdown http server
func (s *Server) Shutdown() error {
s.httpServer.Lock()
@@ -108,12 +90,12 @@ func (s *Server) Shutdown() error {
return s.httpServer.Shutdown(ctx)
}
// Add method handler
// Add method handler. Handler will be called on matching method (Request.Method)
func (s *Server) Add(method string, fn ServerFn) {
s.httpServer.Lock()
defer s.httpServer.Unlock()
if s.httpServer.Server != nil {
log.Printf("[WARN] ignored method %s, can't be added to activated server", method)
s.Logger.Logf("[WARN] ignored method %s, can't be added to activated server", method)
return
}
@@ -122,19 +104,20 @@ func (s *Server) Add(method string, fn ServerFn) {
})
s.funcs.m[method] = fn
log.Printf("[INFO] add handler for %s", method)
s.Logger.Logf("[INFO] add handler for %s", method)
}
// HandlersGroup alias for map of handlers
type HandlersGroup map[string]ServerFn
// Group of handlers with common prefix
// Group of handlers with common prefix, match on group.method
func (s *Server) Group(prefix string, m HandlersGroup) {
for k, v := range m {
s.Add(prefix+"."+k, v)
}
}
// handler is http handler multiplexing calls by req.Method
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
req := struct {
ID uint64 `json:"id"`
@@ -143,12 +126,12 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
}{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, req.Method, 0)
rest.SendErrorJSON(w, r, s.Logger, http.StatusBadRequest, err, req.Method)
return
}
fn, ok := s.funcs.m[req.Method]
if !ok {
rest.SendErrorJSON(w, r, http.StatusNotImplemented, errors.New("unsupported method"), req.Method, 0)
rest.SendErrorJSON(w, r, s.Logger, http.StatusNotImplemented, errors.New("unsupported method"), req.Method)
return
}
@@ -160,6 +143,7 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, fn(req.ID, params))
}
// basicAuth middleware. enabled only if both AuthUser and AuthPasswd defined.
func (s *Server) basicAuth(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -177,3 +161,17 @@ func (s *Server) basicAuth(h http.Handler) http.Handler {
h.ServeHTTP(w, r)
})
}
// L defined logger interface used for an optional rest logging
type L interface {
Logf(format string, args ...interface{})
}
// LoggerFunc type is an adapter to allow the use of ordinary functions as Logger.
type LoggerFunc func(format string, args ...interface{})
// Logf calls f(id)
func (f LoggerFunc) Logf(format string, args ...interface{}) { f(format, args...) }
// NoOpLogger logger does nothing
var NoOpLogger = LoggerFunc(func(format string, args ...interface{}) {})
+4 -2
View File
@@ -10,7 +10,7 @@ github.com/coreos/bbolt
github.com/davecgh/go-spew/spew
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dgrijalva/jwt-go
# github.com/didip/tollbooth v4.0.0+incompatible
# github.com/didip/tollbooth v4.0.2+incompatible
github.com/didip/tollbooth
github.com/didip/tollbooth/errors
github.com/didip/tollbooth/libstring
@@ -38,6 +38,8 @@ github.com/go-pkgz/auth/provider/sender
github.com/go-pkgz/auth/token
github.com/go-pkgz/auth/logger
github.com/go-pkgz/auth/middleware
# github.com/go-pkgz/jrpc v0.1.0
github.com/go-pkgz/jrpc
# github.com/go-pkgz/lcw v0.3.1
github.com/go-pkgz/lcw
# github.com/go-pkgz/lgr v0.6.3
@@ -98,7 +100,7 @@ golang.org/x/crypto/acme
# golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9
golang.org/x/image/draw
golang.org/x/image/math/f64
# golang.org/x/net v0.0.0-20190628185345-da137c7871d7
# golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
golang.org/x/net/html
golang.org/x/net/idna
golang.org/x/net/html/atom