switch core to go-pkgz/jrpc
This commit is contained in:
@@ -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)
|
||||
}))
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+3
-2
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
+4
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,99 @@
|
||||
# jrpc - rpc with json [](https://travis-ci.org/go-pkgz/jrpc) [](https://goreportcard.com/report/github.com/go-pkgz/jrpc) [](https://coveralls.io/github/go-pkgz/jrpc?branch=master) [](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.
|
||||
|
||||
+12
-9
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||
}
|
||||
+40
-42
@@ -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{}) {})
|
||||
Vendored
+4
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user