switch core to go-pkgz/jrpc

This commit is contained in:
Umputun
2019-07-31 18:38:53 -05:00
parent e84a155ef5
commit 3c21cba9c1
20 changed files with 420 additions and 452 deletions
-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")
}
+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)
}
+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=
+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