├── CHANGE_LOG.md
├── README.md
├── client.go
├── client_example_test.go
├── client_test.go
├── constants_unix.go
├── constants_unix_nocgo.go
├── doc.go
├── endpoints.go
├── errors.go
├── event.go
├── go.mod
├── go.sum
├── handle_batch_middleware.go
├── handle_msg_middleware.go
├── handler.go
├── http.go
├── http_test.go
├── inproc.go
├── interfaces
└── provider.go
├── ipc.go
├── ipc_js.go
├── ipc_unix.go
├── ipc_windows.go
├── json.go
├── metrics.go
├── provider_wrapper
├── circuit_breaker.go
├── circuit_breaker_test.go
├── generic.go
├── provider.go
├── provider_base.go
├── provider_breaker.go
├── provider_logger.go
├── provider_middleware.go
├── provider_middleware_test.go
├── provider_retry.go
├── provider_test.go
└── provider_timeout.go
├── server.go
├── server_test.go
├── service.go
├── stdio.go
├── subscription.go
├── subscription_test.go
├── testdata
├── invalid-badid.js
├── invalid-batch.js
├── invalid-idonly.js
├── invalid-nonobj.js
├── invalid-syntax.json
├── reqresp-batch.js
├── reqresp-echo.js
├── reqresp-namedparam.js
├── reqresp-noargsrets.js
├── reqresp-nomethod.js
├── reqresp-noparam.js
├── reqresp-paramsnull.js
├── reqresp-paramsvariadic.js
├── revcall.js
├── revcall2.js
└── subscription.js
├── testservice_test.go
├── types.go
├── types_test.go
├── utils
└── utils.go
├── websocket.go
└── websocket_test.go
/CHANGE_LOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## v0.3.1
4 | - Support block tag: finalize and safe
5 |
6 | ## v0.2.1
7 | - Support logger middleware
8 | - Set default retry count to 0
9 | - Set default request timeout to 30s
10 |
11 | ## v0.2.0
12 | - Remove Call/BatchCall and HookCall/HookBatchall for avoiding user confuse
13 | - Unify all created provider to MiddlewareProvider in providers package
14 | - Support NewBaseProvider/NewTimeoutableProvider/NewRetriableProvider/NewProviderWithOption and all of them return a MiddlewarableProvider
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | go-rpc-provider
2 | ===========
3 | go-rpc-provider is enhanced on the basis of the package github.com/ethereum/go-ethereum/rpc
4 |
5 | Features
6 | -----------
7 | - Replace HTTP by fasthttp to improve performance of concurrency HTTP request
8 | - Refactor function `func (err *jsonError) Error() ` to return 'data' of JSON RPC response
9 | - Set HTTP request header 'Connection' to 'Keep-Alive' to reuse TCP connection for avoiding the amount of TIME_WAIT on the RPC server, and the RPC server also need to set 'Connection' to 'Keep-Alive'.
10 | - Add remote client address to WebSocket context for tracing.
11 | - Add client pointer to context when new RPC connection for tracing.
12 | - Add HTTP request in RPC context in function `ServeHTTP` for tracing.
13 | - Support variadic arguments for RPC service
14 | - Add provider wrapper for convenient extension
15 | - Support MiddlewarableProvider to extend provider features, currently support
16 | - `NewRetriableProvider` to create MiddlewarableProvider instance with auto-retry when failing
17 | - `NewTimeoutableProvider` to create MiddlewarableProvider instance with timeout when CallContext/BatchCallContext
18 | - `NewLoggerProvider` to create MiddlewarableProvider instance for logging request/response when CallContext/BatchCallContext
19 | - `NewProviderWithOption` to create MiddlewarableProvider instance includes all features of `NewRetriableProvider`, `NewTimeoutableProvider` and `NewLoggerProvider`
20 |
21 |
22 | Usage
23 | -----------
24 | rpc.Client implements a provider interface, so you could use it as a provider
25 |
26 | create a simple RPC client
27 | ```golang
28 | rpc.Dial("http://localhost:8545")
29 | ```
30 | create a simple RPC client with context for canceling or timeout the initial connection establishment
31 | ```golang
32 | rpc.DialContext("http://localhost:8545")
33 | ```
34 |
35 | For feature extension, we apply MiddlewarableProvider for hook CallContext/BatchCallContext/Subscribe, such as log rpc request and response or cache environment variable in the context.
36 |
37 | you can create MiddlewarableProvider by NewMiddlewarableProvider and pass the provider created below as the parameter
38 |
39 | ```golang
40 | p, e := rpc.DialContext(context.Background(), "http://localhost:8545")
41 | if e != nil {
42 | t.Fatal(e)
43 | }
44 | mp := NewMiddlewarableProvider(p)
45 | mp.HookCallContext(callContextLogMiddleware)
46 | mp.HookCallContext(otherMiddleware)
47 | ```
48 |
49 | the callContextLogMiddleware is like
50 | ```golang
51 | func callContextLogMiddleware(f providers.CallContextFunc) providers.CallContextFunc {
52 | return func(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
53 | fmt.Printf("request %v %v\n", method, args)
54 | err := f(ctx, resultPtr, method, args...)
55 | j, _ := json.Marshal(resultPtr)
56 | fmt.Printf("response %s\n", j)
57 | return err
58 | }
59 | }
60 | ```
61 |
62 | The simplest way to create middlewarable provider is by `providers.NewBaseProvider`. It will create a MiddlewareProvider and it could set the max connection number for client with the server.
63 |
64 | For example, created by `providers`.NewBaseProvider` and use the logging middleware created below to hook by HookCallContext
65 | ```golang
66 | p, e := warpper.NewBaseProvider(context.Background(), "http://localhost:8545")
67 | if e != nil {
68 | return e
69 | }
70 | p.HookCallContext(callLogMiddleware)
71 | NewClientWithProvider(p)
72 | ```
73 |
74 | However, we also apply several functions to create kinds of instances of MiddlewarableProvider.
75 | The functions are `providers.NewTimeoutableProvider`, `providers.NewRetriableProvider`.
76 |
77 | And the simplest way to create NewMiddlewarableProvider with retry and timeout features is to use `providers.NewProviderWithOption`
78 |
79 |
80 | Server
81 | ----------
82 |
83 | The `rpc` package is extended based on go-ethereum. And we extend RPC server features for hook on handle requests on both "batch call msg" and "call msg".
84 |
85 | ### Usage
86 |
87 | Both `rpc.HookHandleMsg` and `rpc.HookHandleBatch` are globally effective, it will effective to both HTTP and Websocket when hook once.
88 |
89 | #### Note:
90 | - HookHandleCallMsg works when "call msg" and "batch call msg", for example, batch requests `[ jsonrpc_a, jsonrpc_b ]`, it will trigger function nested by `HookHandleBatch` once and function netsed by `HookHandleCallMsg` twice
91 | - HookHandleBatch works only when "call msg"
92 |
93 | ```golang
94 | rpc.HookHandleCallMsg(func(next rpc.HandleCallMsgFunc) rpc.HandleCallMsgFunc {
95 | return func(ctx context.Context, msg *rpc.JsonRpcMessage) *rpc.JsonRpcMessage {
96 | fmt.Printf("request call msg %v\n", utils.PrettyJSON(msg))
97 | fmt.Printf("callmsg -- request context key of %v value %v\n", "test-k", ctx.Value("test-k"))
98 | result := next(ctx, msg)
99 | fmt.Printf("response call msg %v\n", utils.PrettyJSON(result))
100 | return result
101 | }
102 | })
103 | rpc.HookHandleBatch(func(next rpc.HandleBatchFunc) rpc.HandleBatchFunc {
104 | return func(ctx context.Context, msgs []*rpc.JsonRpcMessage) []*rpc.JsonRpcMessage {
105 | fmt.Printf("request batch %v\n", utils.PrettyJSON(msgs))
106 | fmt.Printf("batch -- request context key of %v value %v\n", "test-k", ctx.Value("test-k"))
107 | results := next(ctx, msgs)
108 | fmt.Printf("response batch %v\n", utils.PrettyJSON(results))
109 | return results
110 | }
111 | })
112 | ```
--------------------------------------------------------------------------------
/client_example_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc_test
18 |
19 | import (
20 | "context"
21 | "fmt"
22 | "time"
23 |
24 | "github.com/ethereum/go-ethereum/common/hexutil"
25 | "github.com/ethereum/go-ethereum/rpc"
26 | )
27 |
28 | // In this example, our client wishes to track the latest 'block number'
29 | // known to the server. The server supports two methods:
30 | //
31 | // eth_getBlockByNumber("latest", {})
32 | // returns the latest block object.
33 | //
34 | // eth_subscribe("newHeads")
35 | // creates a subscription which fires block objects when new blocks arrive.
36 |
37 | type Block struct {
38 | Number *hexutil.Big
39 | }
40 |
41 | func ExampleClientSubscription() {
42 | // Connect the client.
43 | client, _ := rpc.Dial("ws://127.0.0.1:8545")
44 | subch := make(chan Block)
45 |
46 | // Ensure that subch receives the latest block.
47 | go func() {
48 | for i := 0; ; i++ {
49 | if i > 0 {
50 | time.Sleep(2 * time.Second)
51 | }
52 | subscribeBlocks(client, subch)
53 | }
54 | }()
55 |
56 | // Print events from the subscription as they arrive.
57 | for block := range subch {
58 | fmt.Println("latest block:", block.Number)
59 | }
60 | }
61 |
62 | // subscribeBlocks runs in its own goroutine and maintains
63 | // a subscription for new blocks.
64 | func subscribeBlocks(client *rpc.Client, subch chan Block) {
65 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
66 | defer cancel()
67 |
68 | // Subscribe to new blocks.
69 | sub, err := client.EthSubscribe(ctx, subch, "newHeads")
70 | if err != nil {
71 | fmt.Println("subscribe error:", err)
72 | return
73 | }
74 |
75 | // The connection is established now.
76 | // Update the channel with the current block.
77 | var lastBlock Block
78 | err = client.CallContext(ctx, &lastBlock, "eth_getBlockByNumber", "latest", false)
79 | if err != nil {
80 | fmt.Println("can't get latest block:", err)
81 | return
82 | }
83 | subch <- lastBlock
84 |
85 | // The subscription will deliver events to the channel. Wait for the
86 | // subscription to end for any reason, then loop around to re-establish
87 | // the connection.
88 | fmt.Println("connection lost: ", <-sub.Err())
89 | }
90 |
--------------------------------------------------------------------------------
/client_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "fmt"
22 | "math/rand"
23 | "net"
24 | "net/http"
25 | "net/http/httptest"
26 | "os"
27 | "reflect"
28 | "runtime"
29 | "sync"
30 | "testing"
31 | "time"
32 |
33 | "github.com/davecgh/go-spew/spew"
34 | "github.com/ethereum/go-ethereum/log"
35 | )
36 |
37 | func TestClientRequest(t *testing.T) {
38 | server := newTestServer()
39 | defer server.Stop()
40 | client := DialInProc(server)
41 | defer client.Close()
42 |
43 | var resp echoResult
44 | if err := client.Call(&resp, "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
45 | t.Fatal(err)
46 | }
47 | if !reflect.DeepEqual(resp, echoResult{"hello", 10, &echoArgs{"world"}}) {
48 | t.Errorf("incorrect result %#v", resp)
49 | }
50 | }
51 |
52 | func TestClientResponseType(t *testing.T) {
53 | server := newTestServer()
54 | defer server.Stop()
55 | client := DialInProc(server)
56 | defer client.Close()
57 |
58 | if err := client.Call(nil, "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
59 | t.Errorf("Passing nil as result should be fine, but got an error: %v", err)
60 | }
61 | var resultVar echoResult
62 | // Note: passing the var, not a ref
63 | err := client.Call(resultVar, "test_echo", "hello", 10, &echoArgs{"world"})
64 | if err == nil {
65 | t.Error("Passing a var as result should be an error")
66 | }
67 | }
68 |
69 | func TestClientBatchRequest(t *testing.T) {
70 | server := newTestServer()
71 | defer server.Stop()
72 | client := DialInProc(server)
73 | defer client.Close()
74 |
75 | batch := []BatchElem{
76 | {
77 | Method: "test_echo",
78 | Args: []interface{}{"hello", 10, &echoArgs{"world"}},
79 | Result: new(echoResult),
80 | },
81 | {
82 | Method: "test_echo",
83 | Args: []interface{}{"hello2", 11, &echoArgs{"world"}},
84 | Result: new(echoResult),
85 | },
86 | {
87 | Method: "no_such_method",
88 | Args: []interface{}{1, 2, 3},
89 | Result: new(int),
90 | },
91 | }
92 | if err := client.BatchCall(batch); err != nil {
93 | t.Fatal(err)
94 | }
95 | wantResult := []BatchElem{
96 | {
97 | Method: "test_echo",
98 | Args: []interface{}{"hello", 10, &echoArgs{"world"}},
99 | Result: &echoResult{"hello", 10, &echoArgs{"world"}},
100 | },
101 | {
102 | Method: "test_echo",
103 | Args: []interface{}{"hello2", 11, &echoArgs{"world"}},
104 | Result: &echoResult{"hello2", 11, &echoArgs{"world"}},
105 | },
106 | {
107 | Method: "no_such_method",
108 | Args: []interface{}{1, 2, 3},
109 | Result: new(int),
110 | Error: &jsonError{Code: -32601, Message: "the method no_such_method does not exist/is not available"},
111 | },
112 | }
113 | if !reflect.DeepEqual(batch, wantResult) {
114 | t.Errorf("batch results mismatch:\ngot %swant %s", spew.Sdump(batch), spew.Sdump(wantResult))
115 | }
116 | }
117 |
118 | func TestClientNotify(t *testing.T) {
119 | server := newTestServer()
120 | defer server.Stop()
121 | client := DialInProc(server)
122 | defer client.Close()
123 |
124 | if err := client.Notify(context.Background(), "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
125 | t.Fatal(err)
126 | }
127 | }
128 |
129 | // func TestClientCancelInproc(t *testing.T) { testClientCancel("inproc", t) }
130 | func TestClientCancelWebsocket(t *testing.T) { testClientCancel("ws", t) }
131 | func TestClientCancelHTTP(t *testing.T) { testClientCancel("http", t) }
132 | func TestClientCancelIPC(t *testing.T) { testClientCancel("ipc", t) }
133 |
134 | // This test checks that requests made through CallContext can be canceled by canceling
135 | // the context.
136 | func testClientCancel(transport string, t *testing.T) {
137 | // These tests take a lot of time, run them all at once.
138 | // You probably want to run with -parallel 1 or comment out
139 | // the call to t.Parallel if you enable the logging.
140 | t.Parallel()
141 |
142 | server := newTestServer()
143 | defer server.Stop()
144 |
145 | // What we want to achieve is that the context gets canceled
146 | // at various stages of request processing. The interesting cases
147 | // are:
148 | // - cancel during dial
149 | // - cancel while performing a HTTP request
150 | // - cancel while waiting for a response
151 | //
152 | // To trigger those, the times are chosen such that connections
153 | // are killed within the deadline for every other call (maxKillTimeout
154 | // is 2x maxCancelTimeout).
155 | //
156 | // Once a connection is dead, there is a fair chance it won't connect
157 | // successfully because the accept is delayed by 1s.
158 | maxContextCancelTimeout := 300 * time.Millisecond
159 | fl := &flakeyListener{
160 | maxAcceptDelay: 1 * time.Second,
161 | maxKillTimeout: 600 * time.Millisecond,
162 | }
163 |
164 | var client *Client
165 | switch transport {
166 | case "ws", "http":
167 | c, hs := httpTestClient(server, transport, fl)
168 | defer hs.Close()
169 | client = c
170 | case "ipc":
171 | c, l := ipcTestClient(server, fl)
172 | defer l.Close()
173 | client = c
174 | default:
175 | panic("unknown transport: " + transport)
176 | }
177 |
178 | // The actual test starts here.
179 | var (
180 | wg sync.WaitGroup
181 | nreqs = 10
182 | ncallers = 10
183 | )
184 | caller := func(index int) {
185 | defer wg.Done()
186 | for i := 0; i < nreqs; i++ {
187 | var (
188 | ctx context.Context
189 | cancel func()
190 | timeout = time.Duration(rand.Int63n(int64(maxContextCancelTimeout)))
191 | )
192 | if index < ncallers/2 {
193 | // For half of the callers, create a context without deadline
194 | // and cancel it later.
195 | ctx, cancel = context.WithCancel(context.Background())
196 | time.AfterFunc(timeout, cancel)
197 | } else {
198 | // For the other half, create a context with a deadline instead. This is
199 | // different because the context deadline is used to set the socket write
200 | // deadline.
201 | ctx, cancel = context.WithTimeout(context.Background(), timeout)
202 | }
203 |
204 | // Now perform a call with the context.
205 | // The key thing here is that no call will ever complete successfully.
206 | err := client.CallContext(ctx, nil, "test_block")
207 | switch {
208 | case err == nil:
209 | _, hasDeadline := ctx.Deadline()
210 | t.Errorf("no error for call with %v wait time (deadline: %v)", timeout, hasDeadline)
211 | // default:
212 | // t.Logf("got expected error with %v wait time: %v", timeout, err)
213 | }
214 | cancel()
215 | }
216 | }
217 | wg.Add(ncallers)
218 | for i := 0; i < ncallers; i++ {
219 | go caller(i)
220 | }
221 | wg.Wait()
222 | }
223 |
224 | func TestClientSubscribeInvalidArg(t *testing.T) {
225 | server := newTestServer()
226 | defer server.Stop()
227 | client := DialInProc(server)
228 | defer client.Close()
229 |
230 | check := func(shouldPanic bool, arg interface{}) {
231 | defer func() {
232 | err := recover()
233 | if shouldPanic && err == nil {
234 | t.Errorf("EthSubscribe should've panicked for %#v", arg)
235 | }
236 | if !shouldPanic && err != nil {
237 | t.Errorf("EthSubscribe shouldn't have panicked for %#v", arg)
238 | buf := make([]byte, 1024*1024)
239 | buf = buf[:runtime.Stack(buf, false)]
240 | t.Error(err)
241 | t.Error(string(buf))
242 | }
243 | }()
244 | client.EthSubscribe(context.Background(), arg, "foo_bar")
245 | }
246 | check(true, nil)
247 | check(true, 1)
248 | check(true, (chan int)(nil))
249 | check(true, make(<-chan int))
250 | check(false, make(chan int))
251 | check(false, make(chan<- int))
252 | }
253 |
254 | func TestClientSubscribe(t *testing.T) {
255 | server := newTestServer()
256 | defer server.Stop()
257 | client := DialInProc(server)
258 | defer client.Close()
259 |
260 | nc := make(chan int)
261 | count := 10
262 | sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", count, 0)
263 | if err != nil {
264 | t.Fatal("can't subscribe:", err)
265 | }
266 | for i := 0; i < count; i++ {
267 | if val := <-nc; val != i {
268 | t.Fatalf("value mismatch: got %d, want %d", val, i)
269 | }
270 | }
271 |
272 | sub.Unsubscribe()
273 | select {
274 | case v := <-nc:
275 | t.Fatal("received value after unsubscribe:", v)
276 | case err := <-sub.Err():
277 | if err != nil {
278 | t.Fatalf("Err returned a non-nil error after explicit unsubscribe: %q", err)
279 | }
280 | case <-time.After(1 * time.Second):
281 | t.Fatalf("subscription not closed within 1s after unsubscribe")
282 | }
283 | }
284 |
285 | // In this test, the connection drops while Subscribe is waiting for a response.
286 | func TestClientSubscribeClose(t *testing.T) {
287 | server := newTestServer()
288 | service := ¬ificationTestService{
289 | gotHangSubscriptionReq: make(chan struct{}),
290 | unblockHangSubscription: make(chan struct{}),
291 | }
292 | if err := server.RegisterName("nftest2", service); err != nil {
293 | t.Fatal(err)
294 | }
295 |
296 | defer server.Stop()
297 | client := DialInProc(server)
298 | defer client.Close()
299 |
300 | var (
301 | nc = make(chan int)
302 | errc = make(chan error, 1)
303 | sub *ClientSubscription
304 | err error
305 | )
306 | go func() {
307 | sub, err = client.Subscribe(context.Background(), "nftest2", nc, "hangSubscription", 999)
308 | errc <- err
309 | }()
310 |
311 | <-service.gotHangSubscriptionReq
312 | client.Close()
313 | service.unblockHangSubscription <- struct{}{}
314 |
315 | select {
316 | case err := <-errc:
317 | if err == nil {
318 | t.Errorf("Subscribe returned nil error after Close")
319 | }
320 | if sub != nil {
321 | t.Error("Subscribe returned non-nil subscription after Close")
322 | }
323 | case <-time.After(1 * time.Second):
324 | t.Fatalf("Subscribe did not return within 1s after Close")
325 | }
326 | }
327 |
328 | // This test reproduces https://github.com/ethereum/go-ethereum/issues/17837 where the
329 | // client hangs during shutdown when Unsubscribe races with Client.Close.
330 | func TestClientCloseUnsubscribeRace(t *testing.T) {
331 | server := newTestServer()
332 | defer server.Stop()
333 |
334 | for i := 0; i < 20; i++ {
335 | client := DialInProc(server)
336 | nc := make(chan int)
337 | sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", 3, 1)
338 | if err != nil {
339 | t.Fatal(err)
340 | }
341 | go client.Close()
342 | go sub.Unsubscribe()
343 | select {
344 | case <-sub.Err():
345 | case <-time.After(5 * time.Second):
346 | t.Fatal("subscription not closed within timeout")
347 | }
348 | }
349 | }
350 |
351 | // This test checks that Client doesn't lock up when a single subscriber
352 | // doesn't read subscription events.
353 | func TestClientNotificationStorm(t *testing.T) {
354 | server := newTestServer()
355 | defer server.Stop()
356 |
357 | doTest := func(count int, wantError bool) {
358 | client := DialInProc(server)
359 | defer client.Close()
360 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
361 | defer cancel()
362 |
363 | // Subscribe on the server. It will start sending many notifications
364 | // very quickly.
365 | nc := make(chan int)
366 | sub, err := client.Subscribe(ctx, "nftest", nc, "someSubscription", count, 0)
367 | if err != nil {
368 | t.Fatal("can't subscribe:", err)
369 | }
370 | defer sub.Unsubscribe()
371 |
372 | // Process each notification, try to run a call in between each of them.
373 | for i := 0; i < count; i++ {
374 | select {
375 | case val := <-nc:
376 | if val != i {
377 | t.Fatalf("(%d/%d) unexpected value %d", i, count, val)
378 | }
379 | case err := <-sub.Err():
380 | if wantError && err != ErrSubscriptionQueueOverflow {
381 | t.Fatalf("(%d/%d) got error %q, want %q", i, count, err, ErrSubscriptionQueueOverflow)
382 | } else if !wantError {
383 | t.Fatalf("(%d/%d) got unexpected error %q", i, count, err)
384 | }
385 | return
386 | }
387 | var r int
388 | err := client.CallContext(ctx, &r, "nftest_echo", i)
389 | if err != nil {
390 | if !wantError {
391 | t.Fatalf("(%d/%d) call error: %v", i, count, err)
392 | }
393 | return
394 | }
395 | }
396 | if wantError {
397 | t.Fatalf("didn't get expected error")
398 | }
399 | }
400 |
401 | doTest(8000, false)
402 | doTest(24000, true)
403 | }
404 |
405 | func TestClientHTTP(t *testing.T) {
406 | server := newTestServer()
407 | defer server.Stop()
408 |
409 | client, hs := httpTestClient(server, "http", nil)
410 | defer hs.Close()
411 | defer client.Close()
412 |
413 | // Launch concurrent requests.
414 | var (
415 | results = make([]echoResult, 100)
416 | errc = make(chan error, len(results))
417 | wantResult = echoResult{"a", 1, new(echoArgs)}
418 | )
419 | defer client.Close()
420 | for i := range results {
421 | i := i
422 | go func() {
423 | errc <- client.Call(&results[i], "test_echo", wantResult.String, wantResult.Int, wantResult.Args)
424 | }()
425 | }
426 |
427 | // Wait for all of them to complete.
428 | timeout := time.NewTimer(5 * time.Second)
429 | defer timeout.Stop()
430 | for i := range results {
431 | select {
432 | case err := <-errc:
433 | if err != nil {
434 | t.Fatal(err)
435 | }
436 | case <-timeout.C:
437 | t.Fatalf("timeout (got %d/%d) results)", i+1, len(results))
438 | }
439 | }
440 |
441 | // Check results.
442 | for i := range results {
443 | if !reflect.DeepEqual(results[i], wantResult) {
444 | t.Errorf("result %d mismatch: got %#v, want %#v", i, results[i], wantResult)
445 | }
446 | }
447 | }
448 |
449 | func TestClientReconnect(t *testing.T) {
450 | startServer := func(addr string) (*Server, net.Listener) {
451 | srv := newTestServer()
452 | l, err := net.Listen("tcp", addr)
453 | if err != nil {
454 | t.Fatal("can't listen:", err)
455 | }
456 | go http.Serve(l, srv.WebsocketHandler([]string{"*"}))
457 | return srv, l
458 | }
459 |
460 | ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
461 | defer cancel()
462 |
463 | // Start a server and corresponding client.
464 | s1, l1 := startServer("127.0.0.1:0")
465 | client, err := DialContext(ctx, "ws://"+l1.Addr().String())
466 | if err != nil {
467 | t.Fatal("can't dial", err)
468 | }
469 |
470 | // Perform a call. This should work because the server is up.
471 | var resp echoResult
472 | if err := client.CallContext(ctx, &resp, "test_echo", "", 1, nil); err != nil {
473 | t.Fatal(err)
474 | }
475 |
476 | // Shut down the server and allow for some cool down time so we can listen on the same
477 | // address again.
478 | l1.Close()
479 | s1.Stop()
480 | time.Sleep(2 * time.Second)
481 |
482 | // Try calling again. It shouldn't work.
483 | if err := client.CallContext(ctx, &resp, "test_echo", "", 2, nil); err == nil {
484 | t.Error("successful call while the server is down")
485 | t.Logf("resp: %#v", resp)
486 | }
487 |
488 | // Start it up again and call again. The connection should be reestablished.
489 | // We spawn multiple calls here to check whether this hangs somehow.
490 | s2, l2 := startServer(l1.Addr().String())
491 | defer l2.Close()
492 | defer s2.Stop()
493 |
494 | start := make(chan struct{})
495 | errors := make(chan error, 20)
496 | for i := 0; i < cap(errors); i++ {
497 | go func() {
498 | <-start
499 | var resp echoResult
500 | errors <- client.CallContext(ctx, &resp, "test_echo", "", 3, nil)
501 | }()
502 | }
503 | close(start)
504 | errcount := 0
505 | for i := 0; i < cap(errors); i++ {
506 | if err = <-errors; err != nil {
507 | errcount++
508 | }
509 | }
510 | t.Logf("%d errors, last error: %v", errcount, err)
511 | if errcount > 1 {
512 | t.Errorf("expected one error after disconnect, got %d", errcount)
513 | }
514 | }
515 |
516 | func httpTestClient(srv *Server, transport string, fl *flakeyListener) (*Client, *httptest.Server) {
517 | // Create the HTTP server.
518 | var hs *httptest.Server
519 | switch transport {
520 | case "ws":
521 | hs = httptest.NewUnstartedServer(srv.WebsocketHandler([]string{"*"}))
522 | case "http":
523 | hs = httptest.NewUnstartedServer(srv)
524 | default:
525 | panic("unknown HTTP transport: " + transport)
526 | }
527 | // Wrap the listener if required.
528 | if fl != nil {
529 | fl.Listener = hs.Listener
530 | hs.Listener = fl
531 | }
532 | // Connect the client.
533 | hs.Start()
534 | client, err := Dial(transport + "://" + hs.Listener.Addr().String())
535 | if err != nil {
536 | panic(err)
537 | }
538 | return client, hs
539 | }
540 |
541 | func ipcTestClient(srv *Server, fl *flakeyListener) (*Client, net.Listener) {
542 | // Listen on a random endpoint.
543 | endpoint := fmt.Sprintf("go-ethereum-test-ipc-%d-%d", os.Getpid(), rand.Int63())
544 | if runtime.GOOS == "windows" {
545 | endpoint = `\\.\pipe\` + endpoint
546 | } else {
547 | endpoint = os.TempDir() + "/" + endpoint
548 | }
549 | l, err := ipcListen(endpoint)
550 | if err != nil {
551 | panic(err)
552 | }
553 | // Connect the listener to the server.
554 | if fl != nil {
555 | fl.Listener = l
556 | l = fl
557 | }
558 | go srv.ServeListener(l)
559 | // Connect the client.
560 | client, err := Dial(endpoint)
561 | if err != nil {
562 | panic(err)
563 | }
564 | return client, l
565 | }
566 |
567 | // flakeyListener kills accepted connections after a random timeout.
568 | type flakeyListener struct {
569 | net.Listener
570 | maxKillTimeout time.Duration
571 | maxAcceptDelay time.Duration
572 | }
573 |
574 | func (l *flakeyListener) Accept() (net.Conn, error) {
575 | delay := time.Duration(rand.Int63n(int64(l.maxAcceptDelay)))
576 | time.Sleep(delay)
577 |
578 | c, err := l.Listener.Accept()
579 | if err == nil {
580 | timeout := time.Duration(rand.Int63n(int64(l.maxKillTimeout)))
581 | time.AfterFunc(timeout, func() {
582 | log.Debug(fmt.Sprintf("killing conn %v after %v", c.LocalAddr(), timeout))
583 | c.Close()
584 | })
585 | }
586 | return c, err
587 | }
588 |
--------------------------------------------------------------------------------
/constants_unix.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | // +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
18 |
19 | package rpc
20 |
21 | /*
22 | #include
23 |
24 | int max_socket_path_size() {
25 | struct sockaddr_un s;
26 | return sizeof(s.sun_path);
27 | }
28 | */
29 | // import "C"
30 |
31 | // var (
32 | // max_path_size = C.max_socket_path_size()
33 | // )
34 |
--------------------------------------------------------------------------------
/constants_unix_nocgo.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | // +build !cgo,!windows
18 |
19 | package rpc
20 |
21 | const (
22 | // On Linux, sun_path is 108 bytes in size
23 | // see http://man7.org/linux/man-pages/man7/unix.7.html
24 | max_path_size = 108
25 | )
26 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | /*
18 |
19 | Package rpc implements bi-directional JSON-RPC 2.0 on multiple transports.
20 |
21 | It provides access to the exported methods of an object across a network or other I/O
22 | connection. After creating a server or client instance, objects can be registered to make
23 | them visible as 'services'. Exported methods that follow specific conventions can be
24 | called remotely. It also has support for the publish/subscribe pattern.
25 |
26 | RPC Methods
27 |
28 | Methods that satisfy the following criteria are made available for remote access:
29 |
30 | - method must be exported
31 | - method returns 0, 1 (response or error) or 2 (response and error) values
32 |
33 | An example method:
34 |
35 | func (s *CalcService) Add(a, b int) (int, error)
36 |
37 | When the returned error isn't nil the returned integer is ignored and the error is sent
38 | back to the client. Otherwise the returned integer is sent back to the client.
39 |
40 | Optional arguments are supported by accepting pointer values as arguments. E.g. if we want
41 | to do the addition in an optional finite field we can accept a mod argument as pointer
42 | value.
43 |
44 | func (s *CalcService) Add(a, b int, mod *int) (int, error)
45 |
46 | This RPC method can be called with 2 integers and a null value as third argument. In that
47 | case the mod argument will be nil. Or it can be called with 3 integers, in that case mod
48 | will be pointing to the given third argument. Since the optional argument is the last
49 | argument the RPC package will also accept 2 integers as arguments. It will pass the mod
50 | argument as nil to the RPC method.
51 |
52 | The server offers the ServeCodec method which accepts a ServerCodec instance. It will read
53 | requests from the codec, process the request and sends the response back to the client
54 | using the codec. The server can execute requests concurrently. Responses can be sent back
55 | to the client out of order.
56 |
57 | An example server which uses the JSON codec:
58 |
59 | type CalculatorService struct {}
60 |
61 | func (s *CalculatorService) Add(a, b int) int {
62 | return a + b
63 | }
64 |
65 | func (s *CalculatorService) Div(a, b int) (int, error) {
66 | if b == 0 {
67 | return 0, errors.New("divide by zero")
68 | }
69 | return a/b, nil
70 | }
71 |
72 | calculator := new(CalculatorService)
73 | server := NewServer()
74 | server.RegisterName("calculator", calculator)
75 | l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"})
76 | server.ServeListener(l)
77 |
78 | Subscriptions
79 |
80 | The package also supports the publish subscribe pattern through the use of subscriptions.
81 | A method that is considered eligible for notifications must satisfy the following
82 | criteria:
83 |
84 | - method must be exported
85 | - first method argument type must be context.Context
86 | - method must have return types (rpc.Subscription, error)
87 |
88 | An example method:
89 |
90 | func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) {
91 | ...
92 | }
93 |
94 | When the service containing the subscription method is registered to the server, for
95 | example under the "blockchain" namespace, a subscription is created by calling the
96 | "blockchain_subscribe" method.
97 |
98 | Subscriptions are deleted when the user sends an unsubscribe request or when the
99 | connection which was used to create the subscription is closed. This can be initiated by
100 | the client and server. The server will close the connection for any write error.
101 |
102 | For more information about subscriptions, see https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB.
103 |
104 | Reverse Calls
105 |
106 | In any method handler, an instance of rpc.Client can be accessed through the
107 | ClientFromContext method. Using this client instance, server-to-client method calls can be
108 | performed on the RPC connection.
109 | */
110 | package rpc
111 |
--------------------------------------------------------------------------------
/endpoints.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "net"
21 |
22 | "github.com/ethereum/go-ethereum/log"
23 | )
24 |
25 | // StartIPCEndpoint starts an IPC endpoint.
26 | func StartIPCEndpoint(ipcEndpoint string, apis []API) (net.Listener, *Server, error) {
27 | // Register all the APIs exposed by the services.
28 | handler := NewServer()
29 | for _, api := range apis {
30 | if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
31 | return nil, nil, err
32 | }
33 | log.Debug("IPC registered", "namespace", api.Namespace)
34 | }
35 | // All APIs registered, start the IPC listener.
36 | listener, err := ipcListen(ipcEndpoint)
37 | if err != nil {
38 | return nil, nil, err
39 | }
40 | go handler.ServeListener(listener)
41 | return listener, handler, nil
42 | }
43 |
--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import "fmt"
20 |
21 | const defaultErrorCode = -32000
22 |
23 | type methodNotFoundError struct{ method string }
24 |
25 | func (e *methodNotFoundError) ErrorCode() int { return -32601 }
26 |
27 | func (e *methodNotFoundError) Error() string {
28 | return fmt.Sprintf("the method %s does not exist/is not available", e.method)
29 | }
30 |
31 | type subscriptionNotFoundError struct{ namespace, subscription string }
32 |
33 | func (e *subscriptionNotFoundError) ErrorCode() int { return -32601 }
34 |
35 | func (e *subscriptionNotFoundError) Error() string {
36 | return fmt.Sprintf("no %q subscription in %s namespace", e.subscription, e.namespace)
37 | }
38 |
39 | // Invalid JSON was received by the server.
40 | type parseError struct{ message string }
41 |
42 | func (e *parseError) ErrorCode() int { return -32700 }
43 |
44 | func (e *parseError) Error() string { return e.message }
45 |
46 | // received message isn't a valid request
47 | type invalidRequestError struct{ message string }
48 |
49 | func (e *invalidRequestError) ErrorCode() int { return -32600 }
50 |
51 | func (e *invalidRequestError) Error() string { return e.message }
52 |
53 | // received message is invalid
54 | type invalidMessageError struct{ message string }
55 |
56 | func (e *invalidMessageError) ErrorCode() int { return -32700 }
57 |
58 | func (e *invalidMessageError) Error() string { return e.message }
59 |
60 | // unable to decode supplied params, or an invalid number of parameters
61 | type invalidParamsError struct{ message string }
62 |
63 | func (e *invalidParamsError) ErrorCode() int { return -32602 }
64 |
65 | func (e *invalidParamsError) Error() string { return e.message }
66 |
--------------------------------------------------------------------------------
/event.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/valyala/fasthttp"
7 | )
8 |
9 | var beforeSendHttpHandlers []BeforeSendHttp
10 |
11 | type BeforeSendHttp func(ctx context.Context, req *fasthttp.Request) error
12 |
13 | func RegisterBeforeSendHttp(fn BeforeSendHttp) {
14 | if fn != nil {
15 | beforeSendHttpHandlers = append(beforeSendHttpHandlers, fn)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/openweb3/go-rpc-provider
2 |
3 | go 1.21
4 |
5 | toolchain go1.22.1
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.1
9 | github.com/deckarep/golang-set v1.8.0
10 | github.com/ethereum/go-ethereum v1.14.5
11 | github.com/fatih/color v1.16.0
12 | github.com/gorilla/websocket v1.5.0
13 | github.com/mcuadros/go-defaults v1.2.0
14 | github.com/pkg/errors v0.9.1
15 | github.com/valyala/fasthttp v1.40.0
16 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce
17 | gotest.tools v2.2.0+incompatible
18 | )
19 |
20 | require (
21 | github.com/Microsoft/go-winio v0.6.2 // indirect
22 | github.com/StackExchange/wmi v1.2.1 // indirect
23 | github.com/andybalholm/brotli v1.0.4 // indirect
24 | github.com/deckarep/golang-set/v2 v2.6.0 // indirect
25 | github.com/go-ole/go-ole v1.3.0 // indirect
26 | github.com/google/go-cmp v0.5.9 // indirect
27 | github.com/holiman/uint256 v1.2.4 // indirect
28 | github.com/klauspost/compress v1.15.15 // indirect
29 | github.com/mattn/go-colorable v0.1.13 // indirect
30 | github.com/mattn/go-isatty v0.0.20 // indirect
31 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
32 | github.com/tklauser/go-sysconf v0.3.12 // indirect
33 | github.com/tklauser/numcpus v0.6.1 // indirect
34 | github.com/valyala/bytebufferpool v1.0.0 // indirect
35 | golang.org/x/crypto v0.22.0 // indirect
36 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
37 | golang.org/x/sys v0.20.0 // indirect
38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
39 | )
40 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
2 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
3 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
4 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
5 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
6 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
7 | github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88=
8 | github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
9 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
10 | github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
11 | github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
12 | github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
13 | github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
14 | github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
15 | github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
16 | github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 | github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
20 | github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
21 | github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
22 | github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
23 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
24 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
25 | github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
26 | github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
27 | github.com/ethereum/go-ethereum v1.14.5 h1:szuFzO1MhJmweXjoM5nSAeDvjNUH3vIQoMzzQnfvjpw=
28 | github.com/ethereum/go-ethereum v1.14.5/go.mod h1:VEDGGhSxY7IEjn98hJRFXl/uFvpRgbIIf2PpXiyGGgc=
29 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
30 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
31 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
32 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
33 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
34 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
35 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
36 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
37 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
38 | github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
39 | github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
40 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
41 | github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
42 | github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
43 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
44 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
45 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
50 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
51 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
52 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
53 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
54 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
55 | github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
56 | github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
57 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
58 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
59 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
60 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
61 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
64 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
65 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
66 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
67 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
68 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
69 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
70 | github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4=
71 | github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
72 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
73 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
74 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
75 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
76 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
77 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
78 | github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
79 | github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
80 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
81 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
82 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
83 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
84 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
85 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
86 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
87 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
88 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
89 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
90 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
91 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
92 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
93 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
94 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
95 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
96 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
102 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
103 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
104 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
105 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
106 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
107 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
108 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
109 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
110 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
111 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
112 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
113 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
114 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
115 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
116 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
117 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
118 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
119 |
--------------------------------------------------------------------------------
/handle_batch_middleware.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | type HandleBatchFunc func(ctx context.Context, msgs []*jsonrpcMessage) []*JsonRpcMessage
8 | type HandleBatchMiddleware func(next HandleBatchFunc) HandleBatchFunc
9 |
10 | var (
11 | handleBatchFuncMiddlewares []HandleBatchMiddleware
12 | )
13 |
14 | func HookHandleBatch(middleware HandleBatchMiddleware) {
15 | handleBatchFuncMiddlewares = append(handleBatchFuncMiddlewares, middleware)
16 | }
17 |
18 | func (h *handler) getHandleBatchNestedware() HandleBatchFunc {
19 | if h.handleBatchNestedWare == nil || h.handleBatchMiddlewareLen != len(handleBatchFuncMiddlewares) {
20 | h.handleBatchMiddlewareLen = len(handleBatchFuncMiddlewares)
21 | nestedWare := func(ctx context.Context, msgs []*jsonrpcMessage) []*JsonRpcMessage {
22 | c := h.handleBatchCore(ctx, msgs)
23 | if c == nil {
24 | return nil
25 | }
26 | result := <-c
27 | return result
28 | }
29 | for i := len(handleBatchFuncMiddlewares) - 1; i >= 0; i-- {
30 | nestedWare = handleBatchFuncMiddlewares[i](nestedWare)
31 | }
32 | h.handleBatchNestedWare = nestedWare
33 | }
34 | return h.handleBatchNestedWare
35 | }
36 |
--------------------------------------------------------------------------------
/handle_msg_middleware.go:
--------------------------------------------------------------------------------
1 | package rpc
2 |
3 | import "context"
4 |
5 | type JsonRpcMessage = jsonrpcMessage
6 |
7 | // Handle CallMsg Middleware
8 | type HandleCallMsgFunc func(ctx context.Context, msg *JsonRpcMessage) *JsonRpcMessage
9 | type HandleCallMsgMiddleware func(next HandleCallMsgFunc) HandleCallMsgFunc
10 |
11 | var (
12 | handleCallMsgFuncMiddlewares []HandleCallMsgMiddleware
13 | )
14 |
15 | func HookHandleCallMsg(middleware HandleCallMsgMiddleware) {
16 | handleCallMsgFuncMiddlewares = append(handleCallMsgFuncMiddlewares, middleware)
17 | }
18 |
19 | func (h *handler) getHandleCallMsgNestedware(cp *callProc) HandleCallMsgFunc {
20 | nestedWare := func(ctx context.Context, msg *JsonRpcMessage) *JsonRpcMessage {
21 | cp.ctx = ctx
22 | return h.handleCallMsgCore(cp, msg)
23 | }
24 | for i := len(handleCallMsgFuncMiddlewares) - 1; i >= 0; i-- {
25 | nestedWare = handleCallMsgFuncMiddlewares[i](nestedWare)
26 | }
27 | return nestedWare
28 | }
29 |
30 | // PreventMessagesWithouID is a HandleCallMsgMiddleware for preventing messages without ID
31 | var PreventMessagesWithouID HandleCallMsgMiddleware = func(next HandleCallMsgFunc) HandleCallMsgFunc {
32 | return func(ctx context.Context, msg *JsonRpcMessage) *JsonRpcMessage {
33 | resp := next(ctx, msg)
34 | if resp == nil && msg.isNotification() {
35 | return errorMessage(&invalidRequestError{"invalid request"})
36 | }
37 | return resp
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/handler.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "encoding/json"
22 | "reflect"
23 | "strconv"
24 | "strings"
25 | "sync"
26 | "time"
27 |
28 | "github.com/ethereum/go-ethereum/log"
29 | )
30 |
31 | // client connection handler
32 | type ConnHandler = handler
33 |
34 | // ParseCallArguments parses call arguments from JSON-RPC message.
35 | func (h *ConnHandler) ParseCallArguments(msg *jsonrpcMessage) (res []interface{}, err error) {
36 | callb := h.reg.callback(msg.Method)
37 | if callb == nil {
38 | return nil, &methodNotFoundError{method: msg.Method}
39 | }
40 |
41 | args, err := parsePositionalArguments(msg.Params, callb.argTypes, callb.isVariadic)
42 | if err != nil {
43 | return nil, &invalidParamsError{err.Error()}
44 | }
45 |
46 | for i := range args {
47 | res = append(res, args[i].Interface())
48 | }
49 |
50 | return res, nil
51 | }
52 |
53 | // handler handles JSON-RPC messages. There is one handler per connection. Note that
54 | // handler is not safe for concurrent use. Message handling never blocks indefinitely
55 | // because RPCs are processed on background goroutines launched by handler.
56 | //
57 | // The entry points for incoming messages are:
58 | //
59 | // h.handleMsg(message)
60 | // h.handleBatch(message)
61 | //
62 | // Outgoing calls use the requestOp struct. Register the request before sending it
63 | // on the connection:
64 | //
65 | // op := &requestOp{ids: ...}
66 | // h.addRequestOp(op)
67 | //
68 | // Now send the request, then wait for the reply to be delivered through handleMsg:
69 | //
70 | // if err := op.wait(...); err != nil {
71 | // h.removeRequestOp(op) // timeout, etc.
72 | // }
73 | //
74 | type handler struct {
75 | reg *serviceRegistry
76 | unsubscribeCb *callback
77 | idgen func() ID // subscription ID generator
78 | respWait map[string]*requestOp // active client requests
79 | clientSubs map[string]*ClientSubscription // active client subscriptions
80 | callWG sync.WaitGroup // pending call goroutines
81 | rootCtx context.Context // canceled by close()
82 | cancelRoot func() // cancel function for rootCtx
83 | conn jsonWriter // where responses will be sent
84 | log log.Logger
85 | allowSubscribe bool
86 |
87 | subLock sync.Mutex
88 | serverSubs map[ID]*Subscription
89 |
90 | handleBatchNestedWare HandleBatchFunc
91 | handleBatchMiddlewareLen int
92 | }
93 |
94 | type callProc struct {
95 | ctx context.Context
96 | notifiers []*Notifier
97 | }
98 |
99 | func newHandler(connCtx context.Context, conn jsonWriter, idgen func() ID, reg *serviceRegistry) *handler {
100 | h := &handler{
101 | reg: reg,
102 | idgen: idgen,
103 | conn: conn,
104 | respWait: make(map[string]*requestOp),
105 | clientSubs: make(map[string]*ClientSubscription),
106 | allowSubscribe: true,
107 | serverSubs: make(map[ID]*Subscription),
108 | log: log.Root(),
109 | }
110 |
111 | connCtx = context.WithValue(connCtx, "handler", h)
112 | h.rootCtx, h.cancelRoot = context.WithCancel(connCtx)
113 |
114 | if conn.remoteAddr() != "" {
115 | h.log = h.log.New("conn", conn.remoteAddr())
116 | }
117 | h.unsubscribeCb = newCallback(reflect.Value{}, reflect.ValueOf(h.unsubscribe))
118 | return h
119 | }
120 |
121 | func (h *handler) handleBatch(msgs []*jsonrpcMessage) {
122 | nestedFunc := h.getHandleBatchNestedware()
123 | nestedFunc(h.rootCtx, msgs)
124 | }
125 |
126 | // handleBatch executes all messages in a batch and returns the responses.
127 | func (h *handler) handleBatchCore(ctx context.Context, msgs []*jsonrpcMessage) <-chan []*JsonRpcMessage {
128 | // Emit error response for empty batches:
129 | if len(msgs) == 0 {
130 | h.startCallProc(ctx, func(cp *callProc) {
131 | h.conn.writeJSON(cp.ctx, errorMessage(&invalidRequestError{"empty batch"}))
132 | })
133 | return nil
134 | }
135 |
136 | // Handle non-call messages first:
137 | calls := make([]*jsonrpcMessage, 0, len(msgs))
138 | for _, msg := range msgs {
139 | if handled := h.handleImmediate(msg); !handled {
140 | calls = append(calls, msg)
141 | }
142 | }
143 | if len(calls) == 0 {
144 | return nil
145 | }
146 |
147 | // pass result to channel beacuse the process is goroutine
148 | msgsC := make(chan []*JsonRpcMessage)
149 |
150 | // Process calls on a goroutine because they may block indefinitely:
151 | h.startCallProc(ctx, func(cp *callProc) {
152 | answers := make([]*jsonrpcMessage, 0, len(msgs))
153 |
154 | for _, msg := range calls {
155 | if answer := h.handleCallMsg(cp, msg); answer != nil {
156 | answers = append(answers, answer)
157 | }
158 | }
159 |
160 | msgsC <- answers
161 | close(msgsC)
162 |
163 | h.addSubscriptions(cp.notifiers)
164 | if len(answers) > 0 {
165 | h.conn.writeJSON(cp.ctx, answers)
166 | }
167 | for _, n := range cp.notifiers {
168 | n.activate()
169 | }
170 | })
171 | return msgsC
172 | }
173 |
174 | // handleMsg handles a single message.
175 | func (h *handler) handleMsg(msg *jsonrpcMessage) {
176 | if ok := h.handleImmediate(msg); ok {
177 | return
178 | }
179 |
180 | h.startCallProc(h.rootCtx, func(cp *callProc) {
181 | answer := h.handleCallMsg(cp, msg)
182 |
183 | h.addSubscriptions(cp.notifiers)
184 | if answer != nil {
185 | h.conn.writeJSON(cp.ctx, answer)
186 | }
187 | for _, n := range cp.notifiers {
188 | n.activate()
189 | }
190 | })
191 |
192 | }
193 |
194 | // close cancels all requests except for inflightReq and waits for
195 | // call goroutines to shut down.
196 | func (h *handler) close(err error, inflightReq *requestOp) {
197 | h.cancelAllRequests(err, inflightReq)
198 | h.callWG.Wait()
199 | h.cancelRoot()
200 | h.cancelServerSubscriptions(err)
201 | }
202 |
203 | // addRequestOp registers a request operation.
204 | func (h *handler) addRequestOp(op *requestOp) {
205 | for _, id := range op.ids {
206 | h.respWait[string(id)] = op
207 | }
208 | }
209 |
210 | // removeRequestOps stops waiting for the given request IDs.
211 | func (h *handler) removeRequestOp(op *requestOp) {
212 | for _, id := range op.ids {
213 | delete(h.respWait, string(id))
214 | }
215 | }
216 |
217 | // cancelAllRequests unblocks and removes pending requests and active subscriptions.
218 | func (h *handler) cancelAllRequests(err error, inflightReq *requestOp) {
219 | didClose := make(map[*requestOp]bool)
220 | if inflightReq != nil {
221 | didClose[inflightReq] = true
222 | }
223 |
224 | for id, op := range h.respWait {
225 | // Remove the op so that later calls will not close op.resp again.
226 | delete(h.respWait, id)
227 |
228 | if !didClose[op] {
229 | op.err = err
230 | close(op.resp)
231 | didClose[op] = true
232 | }
233 | }
234 | for id, sub := range h.clientSubs {
235 | delete(h.clientSubs, id)
236 | sub.quitWithError(false, err)
237 | }
238 | }
239 |
240 | func (h *handler) addSubscriptions(nn []*Notifier) {
241 | h.subLock.Lock()
242 | defer h.subLock.Unlock()
243 |
244 | for _, n := range nn {
245 | if sub := n.takeSubscription(); sub != nil {
246 | h.serverSubs[sub.ID] = sub
247 | }
248 | }
249 | }
250 |
251 | // cancelServerSubscriptions removes all subscriptions and closes their error channels.
252 | func (h *handler) cancelServerSubscriptions(err error) {
253 | h.subLock.Lock()
254 | defer h.subLock.Unlock()
255 |
256 | for id, s := range h.serverSubs {
257 | s.err <- err
258 | close(s.err)
259 | delete(h.serverSubs, id)
260 | }
261 | }
262 |
263 | // startCallProc runs fn in a new goroutine and starts tracking it in the h.calls wait group.
264 | func (h *handler) startCallProc(ctx context.Context, fn func(*callProc)) {
265 | h.callWG.Add(1)
266 | go func() {
267 | // ctx, cancel := context.WithCancel(h.rootCtx)
268 | ctx, cancel := context.WithCancel(ctx)
269 | defer h.callWG.Done()
270 | defer cancel()
271 | fn(&callProc{ctx: ctx})
272 | }()
273 | }
274 |
275 | // handleImmediate executes non-call messages. It returns false if the message is a
276 | // call or requires a reply.
277 | func (h *handler) handleImmediate(msg *jsonrpcMessage) bool {
278 | start := time.Now()
279 | switch {
280 | case msg.isNotification():
281 | if strings.HasSuffix(msg.Method, notificationMethodSuffix) {
282 | h.handleSubscriptionResult(msg)
283 | return true
284 | }
285 | return false
286 | case msg.isResponse():
287 | h.handleResponse(msg)
288 | h.log.Trace("Handled RPC response", "reqid", idForLog{msg.ID}, "t", time.Since(start))
289 | return true
290 | default:
291 | return false
292 | }
293 | }
294 |
295 | // handleSubscriptionResult processes subscription notifications.
296 | func (h *handler) handleSubscriptionResult(msg *jsonrpcMessage) {
297 | var result subscriptionResult
298 | if err := json.Unmarshal(msg.Params, &result); err != nil {
299 | h.log.Debug("Dropping invalid subscription message")
300 | return
301 | }
302 | if h.clientSubs[result.ID] != nil {
303 | h.clientSubs[result.ID].deliver(result.Result)
304 | }
305 | }
306 |
307 | // handleResponse processes method call responses.
308 | func (h *handler) handleResponse(msg *jsonrpcMessage) {
309 | op := h.respWait[string(msg.ID)]
310 | if op == nil {
311 | h.log.Debug("Unsolicited RPC response", "reqid", idForLog{msg.ID})
312 | return
313 | }
314 | delete(h.respWait, string(msg.ID))
315 | // For normal responses, just forward the reply to Call/BatchCall.
316 | if op.sub == nil {
317 | op.resp <- msg
318 | return
319 | }
320 | // For subscription responses, start the subscription if the server
321 | // indicates success. EthSubscribe gets unblocked in either case through
322 | // the op.resp channel.
323 | defer close(op.resp)
324 | if msg.Error != nil {
325 | op.err = msg.Error
326 | return
327 | }
328 | if op.err = json.Unmarshal(msg.Result, &op.sub.subid); op.err == nil {
329 | go op.sub.start()
330 | h.clientSubs[op.sub.subid] = op.sub
331 | }
332 | }
333 |
334 | func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
335 | nestedFunc := h.getHandleCallMsgNestedware(ctx)
336 | return nestedFunc(ctx.ctx, msg)
337 | }
338 |
339 | // handleCallMsg executes a call message and returns the answer.
340 | func (h *handler) handleCallMsgCore(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
341 | start := time.Now()
342 | switch {
343 | case msg.isNotification():
344 | h.handleCall(ctx, msg)
345 | h.log.Debug("Served "+msg.Method, "t", time.Since(start))
346 | return nil
347 | case msg.isCall():
348 | resp := h.handleCall(ctx, msg)
349 | if resp.Error != nil {
350 | h.log.Warn("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start), "err", resp.Error.Message)
351 | } else {
352 | h.log.Debug("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start))
353 | }
354 | return resp
355 | case msg.hasValidID():
356 | return msg.errorResponse(&invalidRequestError{"invalid request"})
357 | default:
358 | return errorMessage(&invalidRequestError{"invalid request"})
359 | }
360 | }
361 |
362 | // handleCall processes method calls.
363 | func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
364 | if msg.isSubscribe() {
365 | return h.handleSubscribe(cp, msg)
366 | }
367 | var callb *callback
368 | if msg.isUnsubscribe() {
369 | callb = h.unsubscribeCb
370 | } else {
371 | callb = h.reg.callback(msg.Method)
372 | }
373 | if callb == nil {
374 | return msg.errorResponse(&methodNotFoundError{method: msg.Method})
375 | }
376 | args, err := parsePositionalArguments(msg.Params, callb.argTypes, callb.isVariadic)
377 | if err != nil {
378 | return msg.errorResponse(&invalidParamsError{err.Error()})
379 | }
380 | start := time.Now()
381 | answer := h.runMethod(cp.ctx, msg, callb, args)
382 |
383 | // Collect the statistics for RPC calls if metrics is enabled.
384 | // We only care about pure rpc call. Filter out subscription.
385 | if callb != h.unsubscribeCb {
386 | rpcRequestGauge.Inc(1)
387 | if answer.Error != nil {
388 | failedReqeustGauge.Inc(1)
389 | } else {
390 | successfulRequestGauge.Inc(1)
391 | }
392 | rpcServingTimer.UpdateSince(start)
393 | newRPCServingTimer(msg.Method, answer.Error == nil).UpdateSince(start)
394 | }
395 | return answer
396 | }
397 |
398 | // handleSubscribe processes *_subscribe method calls.
399 | func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
400 | if !h.allowSubscribe {
401 | return msg.errorResponse(ErrNotificationsUnsupported)
402 | }
403 |
404 | // Subscription method name is first argument.
405 | name, err := parseSubscriptionName(msg.Params)
406 | if err != nil {
407 | return msg.errorResponse(&invalidParamsError{err.Error()})
408 | }
409 | namespace := msg.namespace()
410 | callb := h.reg.subscription(namespace, name)
411 | if callb == nil {
412 | return msg.errorResponse(&subscriptionNotFoundError{namespace, name})
413 | }
414 |
415 | // Parse subscription name arg too, but remove it before calling the callback.
416 | argTypes := append([]reflect.Type{stringType}, callb.argTypes...)
417 | args, err := parsePositionalArguments(msg.Params, argTypes, callb.isVariadic)
418 | if err != nil {
419 | return msg.errorResponse(&invalidParamsError{err.Error()})
420 | }
421 | args = args[1:]
422 |
423 | // Install notifier in context so the subscription handler can find it.
424 | n := &Notifier{h: h, namespace: namespace}
425 | cp.notifiers = append(cp.notifiers, n)
426 | ctx := context.WithValue(cp.ctx, notifierKey{}, n)
427 |
428 | return h.runMethod(ctx, msg, callb, args)
429 | }
430 |
431 | // runMethod runs the Go callback for an RPC method.
432 | func (h *handler) runMethod(ctx context.Context, msg *jsonrpcMessage, callb *callback, args []reflect.Value) *jsonrpcMessage {
433 | result, err := callb.call(ctx, msg.Method, args)
434 | if err != nil {
435 | return msg.errorResponse(err)
436 | }
437 | return msg.response(result)
438 | }
439 |
440 | // unsubscribe is the callback function for all *_unsubscribe calls.
441 | func (h *handler) unsubscribe(ctx context.Context, id ID) (bool, error) {
442 | h.subLock.Lock()
443 | defer h.subLock.Unlock()
444 |
445 | s := h.serverSubs[id]
446 | if s == nil {
447 | return false, ErrSubscriptionNotFound
448 | }
449 | close(s.err)
450 | delete(h.serverSubs, id)
451 | return true, nil
452 | }
453 |
454 | type idForLog struct{ json.RawMessage }
455 |
456 | func (id idForLog) String() string {
457 | if s, err := strconv.Unquote(string(id.RawMessage)); err == nil {
458 | return s
459 | }
460 | return string(id.RawMessage)
461 | }
462 |
--------------------------------------------------------------------------------
/http.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "bytes"
21 | "context"
22 | "encoding/json"
23 | "errors"
24 | "fmt"
25 | "io"
26 | "mime"
27 | "net/http"
28 | "sync"
29 | "time"
30 |
31 | "github.com/valyala/fasthttp"
32 | )
33 |
34 | const (
35 | maxRequestContentLength = 1024 * 1024 * 5
36 | contentType = "application/json"
37 | )
38 |
39 | // https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13
40 | var acceptedContentTypes = []string{contentType, "application/json-rpc", "application/jsonrequest"}
41 |
42 | type httpConn struct {
43 | // client *http.Client
44 | // req *http.Request
45 | client *fasthttp.Client
46 | req *fasthttp.Request
47 | closeOnce sync.Once
48 | closeCh chan interface{}
49 | }
50 |
51 | // httpConn is treated specially by Client.
52 | func (hc *httpConn) writeJSON(context.Context, interface{}) error {
53 | panic("writeJSON called on httpConn")
54 | }
55 |
56 | func (hc *httpConn) remoteAddr() string {
57 | return hc.req.URI().String()
58 | }
59 |
60 | func (hc *httpConn) readBatch() ([]*jsonrpcMessage, bool, error) {
61 | <-hc.closeCh
62 | return nil, false, io.EOF
63 | }
64 |
65 | func (hc *httpConn) close() {
66 | hc.closeOnce.Do(func() { close(hc.closeCh) })
67 | }
68 |
69 | func (hc *httpConn) closed() <-chan interface{} {
70 | return hc.closeCh
71 | }
72 |
73 | // HTTPTimeouts represents the configuration params for the HTTP RPC server.
74 | type HTTPTimeouts struct {
75 | // ReadTimeout is the maximum duration for reading the entire
76 | // request, including the body.
77 | //
78 | // Because ReadTimeout does not let Handlers make per-request
79 | // decisions on each request body's acceptable deadline or
80 | // upload rate, most users will prefer to use
81 | // ReadHeaderTimeout. It is valid to use them both.
82 | ReadTimeout time.Duration
83 |
84 | // WriteTimeout is the maximum duration before timing out
85 | // writes of the response. It is reset whenever a new
86 | // request's header is read. Like ReadTimeout, it does not
87 | // let Handlers make decisions on a per-request basis.
88 | WriteTimeout time.Duration
89 |
90 | // IdleTimeout is the maximum amount of time to wait for the
91 | // next request when keep-alives are enabled. If IdleTimeout
92 | // is zero, the value of ReadTimeout is used. If both are
93 | // zero, ReadHeaderTimeout is used.
94 | IdleTimeout time.Duration
95 | }
96 |
97 | // DefaultHTTPTimeouts represents the default timeout values used if further
98 | // configuration is not provided.
99 | var DefaultHTTPTimeouts = HTTPTimeouts{
100 | ReadTimeout: 30 * time.Second,
101 | WriteTimeout: 30 * time.Second,
102 | IdleTimeout: 120 * time.Second,
103 | }
104 |
105 | // DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP
106 | // using the provided HTTP Client.
107 | func DialHTTPWithClient(endpoint string, client *fasthttp.Client) (*Client, error) {
108 | initctx := context.Background()
109 | return newClient(initctx, func(context.Context) (ServerCodec, error) {
110 | req := fasthttp.Request{}
111 | req.SetRequestURI(endpoint)
112 |
113 | req.Header.SetMethod("POST")
114 | req.Header.Set("Content-Type", contentType)
115 | req.Header.Set("Accept", contentType)
116 | // req.Header.Set("Connection", "close")
117 | req.Header.Set("Connection", "Keep-Alive")
118 | return &httpConn{client: client, req: &req, closeCh: make(chan interface{})}, nil
119 | })
120 | }
121 |
122 | // DialHTTP creates a new RPC client that connects to an RPC server over HTTP.
123 | func DialHTTP(endpoint string) (*Client, error) {
124 | return DialHTTPWithClient(endpoint, new(fasthttp.Client))
125 | }
126 |
127 | func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error {
128 | hc := c.writeConn.(*httpConn)
129 | respBody, err := hc.doRequest(ctx, msg)
130 |
131 | if err != nil {
132 | if respBody != nil {
133 | buf := new(bytes.Buffer)
134 | if _, err2 := buf.ReadFrom(respBody); err2 == nil {
135 | return fmt.Errorf("%v %v", err, buf.String())
136 | }
137 | }
138 | return err
139 | }
140 |
141 | var respmsg jsonrpcMessage
142 | if err := json.NewDecoder(respBody).Decode(&respmsg); err != nil {
143 | return err
144 | }
145 |
146 | op.resp <- &respmsg
147 | return nil
148 | }
149 |
150 | func (c *Client) sendBatchHTTP(ctx context.Context, op *requestOp, msgs []*jsonrpcMessage) error {
151 | hc := c.writeConn.(*httpConn)
152 | respBody, err := hc.doRequest(ctx, msgs)
153 | if err != nil {
154 | return err
155 | }
156 |
157 | var respmsgs []jsonrpcMessage
158 | if err := json.NewDecoder(respBody).Decode(&respmsgs); err != nil {
159 | return err
160 | }
161 |
162 | for i := 0; i < len(respmsgs); i++ {
163 | op.resp <- &respmsgs[i]
164 | }
165 | return nil
166 | }
167 |
168 | func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.Reader, error) {
169 | body, err := json.Marshal(msg)
170 | if err != nil {
171 | return nil, err
172 | }
173 |
174 | req := &fasthttp.Request{}
175 | hc.req.CopyTo(req)
176 | req.SetBody(body)
177 |
178 | for _, f := range beforeSendHttpHandlers {
179 | if err := f(ctx, req); err != nil {
180 | return nil, err
181 | }
182 | }
183 |
184 | resp := &fasthttp.Response{}
185 | deadline, ok := ctx.Deadline()
186 |
187 | if ok {
188 | err = hc.client.DoDeadline(req, resp, deadline)
189 | } else {
190 | fCh := make(chan struct{})
191 | go func() {
192 | err = hc.client.Do(req, resp)
193 | fCh <- struct{}{}
194 | }()
195 | select {
196 | case <-ctx.Done():
197 | err = ctx.Err()
198 | case <-fCh:
199 | }
200 | }
201 |
202 | if err != nil {
203 | return nil, err
204 | }
205 |
206 | if resp.StatusCode() < 200 || resp.StatusCode() >= 300 {
207 | return bytes.NewReader(resp.Body()), fmt.Errorf("%v", resp.StatusCode())
208 | }
209 | return bytes.NewReader(resp.Body()), nil
210 | }
211 |
212 | // httpServerConn turns a HTTP connection into a Conn.
213 | type httpServerConn struct {
214 | io.Reader
215 | io.Writer
216 | r *http.Request
217 | }
218 |
219 | func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec {
220 | body := io.LimitReader(r.Body, maxRequestContentLength)
221 | conn := &httpServerConn{Reader: body, Writer: w, r: r}
222 | return NewCodec(conn)
223 | }
224 |
225 | // Close does nothing and always returns nil.
226 | func (t *httpServerConn) Close() error { return nil }
227 |
228 | // RemoteAddr returns the peer address of the underlying connection.
229 | func (t *httpServerConn) RemoteAddr() string {
230 | return t.r.RemoteAddr
231 | }
232 |
233 | // SetWriteDeadline does nothing and always returns nil.
234 | func (t *httpServerConn) SetWriteDeadline(time.Time) error { return nil }
235 |
236 | // serveHTTP serves JSON-RPC requests over HTTP.
237 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
238 | // Permit dumb empty requests for remote health-checks (AWS)
239 | if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" {
240 | w.WriteHeader(http.StatusOK)
241 | return
242 | }
243 | if code, err := validateRequest(r); err != nil {
244 | http.Error(w, err.Error(), code)
245 | return
246 | }
247 | // All checks passed, create a codec that reads directly from the request body
248 | // until EOF, writes the response to w, and orders the server to process a
249 | // single request.
250 | ctx := r.Context()
251 | ctx = context.WithValue(ctx, "remote", r.RemoteAddr)
252 | ctx = context.WithValue(ctx, "request", r)
253 | ctx = context.WithValue(ctx, "scheme", r.Proto)
254 | ctx = context.WithValue(ctx, "local", r.Host)
255 | if ua := r.Header.Get("User-Agent"); ua != "" {
256 | ctx = context.WithValue(ctx, "User-Agent", ua)
257 | }
258 | if origin := r.Header.Get("Origin"); origin != "" {
259 | ctx = context.WithValue(ctx, "Origin", origin)
260 | }
261 |
262 | w.Header().Set("content-type", contentType)
263 | codec := newHTTPServerConn(r, w)
264 | defer codec.close()
265 | s.serveSingleRequest(ctx, codec)
266 | }
267 |
268 | // validateRequest returns a non-zero response code and error message if the
269 | // request is invalid.
270 | func validateRequest(r *http.Request) (int, error) {
271 | if r.Method == http.MethodPut || r.Method == http.MethodDelete {
272 | return http.StatusMethodNotAllowed, errors.New("method not allowed")
273 | }
274 | if r.ContentLength > maxRequestContentLength {
275 | err := fmt.Errorf("content length too large (%d>%d)", r.ContentLength, maxRequestContentLength)
276 | return http.StatusRequestEntityTooLarge, err
277 | }
278 | // Allow OPTIONS (regardless of content-type)
279 | if r.Method == http.MethodOptions {
280 | return 0, nil
281 | }
282 | // Check content-type
283 | if mt, _, err := mime.ParseMediaType(r.Header.Get("content-type")); err == nil {
284 | for _, accepted := range acceptedContentTypes {
285 | if accepted == mt {
286 | return 0, nil
287 | }
288 | }
289 | }
290 | // Invalid content-type
291 | err := fmt.Errorf("invalid content type, only %s is supported", contentType)
292 | return http.StatusUnsupportedMediaType, err
293 | }
294 |
--------------------------------------------------------------------------------
/http_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "net/http"
21 | "net/http/httptest"
22 | "strings"
23 | "testing"
24 | )
25 |
26 | func confirmStatusCode(t *testing.T, got, want int) {
27 | t.Helper()
28 | if got == want {
29 | return
30 | }
31 | if gotName := http.StatusText(got); len(gotName) > 0 {
32 | if wantName := http.StatusText(want); len(wantName) > 0 {
33 | t.Fatalf("response status code: got %d (%s), want %d (%s)", got, gotName, want, wantName)
34 | }
35 | }
36 | t.Fatalf("response status code: got %d, want %d", got, want)
37 | }
38 |
39 | func confirmRequestValidationCode(t *testing.T, method, contentType, body string, expectedStatusCode int) {
40 | t.Helper()
41 | request := httptest.NewRequest(method, "http://url.com", strings.NewReader(body))
42 | if len(contentType) > 0 {
43 | request.Header.Set("Content-Type", contentType)
44 | }
45 | code, err := validateRequest(request)
46 | if code == 0 {
47 | if err != nil {
48 | t.Errorf("validation: got error %v, expected nil", err)
49 | }
50 | } else if err == nil {
51 | t.Errorf("validation: code %d: got nil, expected error", code)
52 | }
53 | confirmStatusCode(t, code, expectedStatusCode)
54 | }
55 |
56 | func TestHTTPErrorResponseWithDelete(t *testing.T) {
57 | confirmRequestValidationCode(t, http.MethodDelete, contentType, "", http.StatusMethodNotAllowed)
58 | }
59 |
60 | func TestHTTPErrorResponseWithPut(t *testing.T) {
61 | confirmRequestValidationCode(t, http.MethodPut, contentType, "", http.StatusMethodNotAllowed)
62 | }
63 |
64 | func TestHTTPErrorResponseWithMaxContentLength(t *testing.T) {
65 | body := make([]rune, maxRequestContentLength+1)
66 | confirmRequestValidationCode(t,
67 | http.MethodPost, contentType, string(body), http.StatusRequestEntityTooLarge)
68 | }
69 |
70 | func TestHTTPErrorResponseWithEmptyContentType(t *testing.T) {
71 | confirmRequestValidationCode(t, http.MethodPost, "", "", http.StatusUnsupportedMediaType)
72 | }
73 |
74 | func TestHTTPErrorResponseWithValidRequest(t *testing.T) {
75 | confirmRequestValidationCode(t, http.MethodPost, contentType, "", 0)
76 | }
77 |
78 | func confirmHTTPRequestYieldsStatusCode(t *testing.T, method, contentType, body string, expectedStatusCode int) {
79 | t.Helper()
80 | s := Server{}
81 | ts := httptest.NewServer(&s)
82 | defer ts.Close()
83 |
84 | request, err := http.NewRequest(method, ts.URL, strings.NewReader(body))
85 | if err != nil {
86 | t.Fatalf("failed to create a valid HTTP request: %v", err)
87 | }
88 | if len(contentType) > 0 {
89 | request.Header.Set("Content-Type", contentType)
90 | }
91 | resp, err := http.DefaultClient.Do(request)
92 | if err != nil {
93 | t.Fatalf("request failed: %v", err)
94 | }
95 | confirmStatusCode(t, resp.StatusCode, expectedStatusCode)
96 | }
97 |
98 | func TestHTTPResponseWithEmptyGet(t *testing.T) {
99 | confirmHTTPRequestYieldsStatusCode(t, http.MethodGet, "", "", http.StatusOK)
100 | }
101 |
--------------------------------------------------------------------------------
/inproc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "net"
22 | )
23 |
24 | // DialInProc attaches an in-process connection to the given RPC server.
25 | func DialInProc(handler *Server) *Client {
26 | initctx := context.Background()
27 | c, _ := newClient(initctx, func(context.Context) (ServerCodec, error) {
28 | p1, p2 := net.Pipe()
29 | go handler.ServeCodec(NewCodec(p1), 0)
30 | return NewCodec(p2), nil
31 | })
32 | return c
33 | }
34 |
--------------------------------------------------------------------------------
/interfaces/provider.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/openweb3/go-rpc-provider"
7 | )
8 |
9 | type Provider interface {
10 | // Call(resultPtr interface{}, method string, args ...interface{}) error
11 | CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error
12 | // BatchCall(b []rpc.BatchElem) error
13 | BatchCallContext(ctx context.Context, b []rpc.BatchElem) error
14 | Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (*rpc.ClientSubscription, error)
15 | SubscribeWithReconn(ctx context.Context, namespace string, channel interface{}, args ...interface{}) *rpc.ReconnClientSubscription
16 | Close()
17 | }
18 |
--------------------------------------------------------------------------------
/ipc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "net"
22 |
23 | "github.com/ethereum/go-ethereum/log"
24 | "github.com/ethereum/go-ethereum/p2p/netutil"
25 | )
26 |
27 | // ServeListener accepts connections on l, serving JSON-RPC on them.
28 | func (s *Server) ServeListener(l net.Listener) error {
29 | for {
30 | conn, err := l.Accept()
31 | if netutil.IsTemporaryError(err) {
32 | log.Warn("RPC accept error", "err", err)
33 | continue
34 | } else if err != nil {
35 | return err
36 | }
37 | log.Trace("Accepted RPC connection", "conn", conn.RemoteAddr())
38 | go s.ServeCodec(NewCodec(conn), 0)
39 | }
40 | }
41 |
42 | // DialIPC create a new IPC client that connects to the given endpoint. On Unix it assumes
43 | // the endpoint is the full path to a unix socket, and Windows the endpoint is an
44 | // identifier for a named pipe.
45 | //
46 | // The context is used for the initial connection establishment. It does not
47 | // affect subsequent interactions with the client.
48 | func DialIPC(ctx context.Context, endpoint string) (*Client, error) {
49 | return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
50 | conn, err := newIPCConnection(ctx, endpoint)
51 | if err != nil {
52 | return nil, err
53 | }
54 | return NewCodec(conn), err
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/ipc_js.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | // +build js
18 |
19 | package rpc
20 |
21 | import (
22 | "context"
23 | "errors"
24 | "net"
25 | )
26 |
27 | var errNotSupported = errors.New("rpc: not supported")
28 |
29 | // ipcListen will create a named pipe on the given endpoint.
30 | func ipcListen(endpoint string) (net.Listener, error) {
31 | return nil, errNotSupported
32 | }
33 |
34 | // newIPCConnection will connect to a named pipe with the given endpoint as name.
35 | func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) {
36 | return nil, errNotSupported
37 | }
38 |
--------------------------------------------------------------------------------
/ipc_unix.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | // +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
18 |
19 | package rpc
20 |
21 | import (
22 | "context"
23 | "fmt"
24 | "net"
25 | "os"
26 | "path/filepath"
27 |
28 | "github.com/ethereum/go-ethereum/log"
29 | )
30 |
31 | // ipcListen will create a Unix socket on the given endpoint.
32 | func ipcListen(endpoint string) (net.Listener, error) {
33 | if len(endpoint) > int(108) {
34 | log.Warn(fmt.Sprintf("The ipc endpoint is longer than %d characters. ", 108),
35 | "endpoint", endpoint)
36 | }
37 |
38 | // Ensure the IPC path exists and remove any previous leftover
39 | if err := os.MkdirAll(filepath.Dir(endpoint), 0751); err != nil {
40 | return nil, err
41 | }
42 | os.Remove(endpoint)
43 | l, err := net.Listen("unix", endpoint)
44 | if err != nil {
45 | return nil, err
46 | }
47 | os.Chmod(endpoint, 0600)
48 | return l, nil
49 | }
50 |
51 | // newIPCConnection will connect to a Unix socket on the given endpoint.
52 | func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) {
53 | return new(net.Dialer).DialContext(ctx, "unix", endpoint)
54 | }
55 |
--------------------------------------------------------------------------------
/ipc_windows.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | // +build windows
18 |
19 | package rpc
20 |
21 | import (
22 | "context"
23 | "net"
24 | "time"
25 |
26 | "gopkg.in/natefinch/npipe.v2"
27 | )
28 |
29 | // This is used if the dialing context has no deadline. It is much smaller than the
30 | // defaultDialTimeout because named pipes are local and there is no need to wait so long.
31 | const defaultPipeDialTimeout = 2 * time.Second
32 |
33 | // ipcListen will create a named pipe on the given endpoint.
34 | func ipcListen(endpoint string) (net.Listener, error) {
35 | return npipe.Listen(endpoint)
36 | }
37 |
38 | // newIPCConnection will connect to a named pipe with the given endpoint as name.
39 | func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) {
40 | timeout := defaultPipeDialTimeout
41 | if deadline, ok := ctx.Deadline(); ok {
42 | timeout = deadline.Sub(time.Now())
43 | if timeout < 0 {
44 | timeout = 0
45 | }
46 | }
47 | return npipe.DialTimeout(endpoint, timeout)
48 | }
49 |
--------------------------------------------------------------------------------
/json.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "bytes"
21 | "context"
22 | "encoding/json"
23 | "errors"
24 | "fmt"
25 | "io"
26 | "reflect"
27 | "strings"
28 | "sync"
29 | "time"
30 |
31 | "github.com/gorilla/websocket"
32 | )
33 |
34 | const (
35 | vsn = "2.0"
36 | serviceMethodSeparator = "_"
37 | subscribeMethodSuffix = "_subscribe"
38 | unsubscribeMethodSuffix = "_unsubscribe"
39 | notificationMethodSuffix = "_subscription"
40 |
41 | defaultWriteTimeout = 10 * time.Second // used if context has no deadline
42 | )
43 |
44 | var null = json.RawMessage("null")
45 |
46 | type subscriptionResult struct {
47 | ID string `json:"subscription"`
48 | Result json.RawMessage `json:"result,omitempty"`
49 | }
50 |
51 | // A value of this type can a JSON-RPC request, notification, successful response or
52 | // error response. Which one it is depends on the fields.
53 | type jsonrpcMessage struct {
54 | Version string `json:"jsonrpc,omitempty"`
55 | ID json.RawMessage `json:"id,omitempty"`
56 | Method string `json:"method,omitempty"`
57 | Params json.RawMessage `json:"params,omitempty"`
58 | Error *jsonError `json:"error,omitempty"`
59 | Result json.RawMessage `json:"result,omitempty"`
60 | }
61 |
62 | func (msg *jsonrpcMessage) isNotification() bool {
63 | return msg.ID == nil && msg.Method != ""
64 | }
65 |
66 | func (msg *jsonrpcMessage) isCall() bool {
67 | return msg.hasValidID() && msg.Method != ""
68 | }
69 |
70 | func (msg *jsonrpcMessage) isResponse() bool {
71 | return msg.hasValidID() && msg.Method == "" && msg.Params == nil && (msg.Result != nil || msg.Error != nil)
72 | }
73 |
74 | func (msg *jsonrpcMessage) hasValidID() bool {
75 | return len(msg.ID) > 0 && msg.ID[0] != '{' && msg.ID[0] != '['
76 | }
77 |
78 | func (msg *jsonrpcMessage) isSubscribe() bool {
79 | return strings.HasSuffix(msg.Method, subscribeMethodSuffix)
80 | }
81 |
82 | func (msg *jsonrpcMessage) isUnsubscribe() bool {
83 | return strings.HasSuffix(msg.Method, unsubscribeMethodSuffix)
84 | }
85 |
86 | func (msg *jsonrpcMessage) namespace() string {
87 | elem := strings.SplitN(msg.Method, serviceMethodSeparator, 2)
88 | return elem[0]
89 | }
90 |
91 | func (msg *jsonrpcMessage) String() string {
92 | b, _ := json.Marshal(msg)
93 | return string(b)
94 | }
95 |
96 | func (msg *jsonrpcMessage) errorResponse(err error) *jsonrpcMessage {
97 | resp := errorMessage(err)
98 | resp.ID = msg.ID
99 | return resp
100 | }
101 |
102 | func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage {
103 | enc, err := json.Marshal(result)
104 | if err != nil {
105 | // TODO: wrap with 'internal server error'
106 | return msg.errorResponse(err)
107 | }
108 | return &jsonrpcMessage{Version: vsn, ID: msg.ID, Result: enc}
109 | }
110 |
111 | func (msg *JsonRpcMessage) ErrorResponse(err error) *JsonRpcMessage {
112 | return msg.errorResponse(err)
113 | }
114 |
115 | func errorMessage(err error) *jsonrpcMessage {
116 | if jsonErr, ok := err.(*jsonError); ok {
117 | return &jsonrpcMessage{Version: vsn, ID: null, Error: jsonErr}
118 | }
119 |
120 | msg := &jsonrpcMessage{Version: vsn, ID: null, Error: &jsonError{
121 | Code: defaultErrorCode,
122 | Message: err.Error(),
123 | inner: err,
124 | }}
125 | ec, ok := err.(Error)
126 | if ok {
127 | msg.Error.Code = ec.ErrorCode()
128 | }
129 | return msg
130 | }
131 |
132 | type JsonError = jsonError
133 |
134 | type jsonError struct {
135 | Code int `json:"code"`
136 | Message string `json:"message"`
137 | Data interface{} `json:"data,omitempty"`
138 | inner error
139 | }
140 |
141 | func (err *jsonError) Error() string {
142 | if err.Message == "" && err.Data == nil {
143 | return fmt.Sprintf("json-rpc error %d", err.Code)
144 | }
145 |
146 | if err.Data == nil {
147 | return err.Message
148 | }
149 |
150 | return fmt.Sprintf("%v; data: %v", err.Message, err.Data)
151 | }
152 |
153 | func (err *jsonError) ErrorCode() int {
154 | return err.Code
155 | }
156 |
157 | func (err *jsonError) Inner() error {
158 | if err == nil {
159 | return nil
160 | }
161 | return err.inner
162 | }
163 |
164 | // Conn is a subset of the methods of net.Conn which are sufficient for ServerCodec.
165 | type Conn interface {
166 | io.ReadWriteCloser
167 | SetWriteDeadline(time.Time) error
168 | }
169 |
170 | type deadlineCloser interface {
171 | io.Closer
172 | SetWriteDeadline(time.Time) error
173 | }
174 |
175 | // ConnRemoteAddr wraps the RemoteAddr operation, which returns a description
176 | // of the peer address of a connection. If a Conn also implements ConnRemoteAddr, this
177 | // description is used in log messages.
178 | type ConnRemoteAddr interface {
179 | RemoteAddr() string
180 | }
181 |
182 | // jsonCodec reads and writes JSON-RPC messages to the underlying connection. It also has
183 | // support for parsing arguments and serializing (result) objects.
184 | type jsonCodec struct {
185 | remote string
186 | closer sync.Once // close closed channel once
187 | closeCh chan interface{} // closed on Close
188 | decode func(v interface{}) error // decoder to allow multiple transports
189 | encMu sync.Mutex // guards the encoder
190 | encode func(v interface{}) error // encoder to allow multiple transports
191 | conn deadlineCloser
192 | }
193 |
194 | // NewFuncCodec creates a codec which uses the given functions to read and write. If conn
195 | // implements ConnRemoteAddr, log messages will use it to include the remote address of
196 | // the connection.
197 | func NewFuncCodec(conn deadlineCloser, encode, decode func(v interface{}) error) ServerCodec {
198 | codec := &jsonCodec{
199 | closeCh: make(chan interface{}),
200 | encode: encode,
201 | decode: decode,
202 | conn: conn,
203 | }
204 |
205 | if ra, ok := conn.(ConnRemoteAddr); ok {
206 | codec.remote = ra.RemoteAddr()
207 | } else if c, ok := conn.(*websocket.Conn); ok { // extract remote address from websocket connection
208 | codec.remote = c.RemoteAddr().String()
209 | }
210 |
211 | return codec
212 | }
213 |
214 | // NewCodec creates a codec on the given connection. If conn implements ConnRemoteAddr, log
215 | // messages will use it to include the remote address of the connection.
216 | func NewCodec(conn Conn) ServerCodec {
217 | enc := json.NewEncoder(conn)
218 | dec := json.NewDecoder(conn)
219 | dec.UseNumber()
220 | return NewFuncCodec(conn, enc.Encode, dec.Decode)
221 | }
222 |
223 | func (c *jsonCodec) remoteAddr() string {
224 | return c.remote
225 | }
226 |
227 | func (c *jsonCodec) readBatch() (msg []*jsonrpcMessage, batch bool, err error) {
228 | // Decode the next JSON object in the input stream.
229 | // This verifies basic syntax, etc.
230 | var rawmsg json.RawMessage
231 | if err := c.decode(&rawmsg); err != nil {
232 | return nil, false, err
233 | }
234 | msg, batch, err = parseMessage(rawmsg)
235 | return msg, batch, err
236 | }
237 |
238 | func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}) error {
239 | c.encMu.Lock()
240 | defer c.encMu.Unlock()
241 |
242 | deadline, ok := ctx.Deadline()
243 | if !ok {
244 | deadline = time.Now().Add(defaultWriteTimeout)
245 | }
246 | c.conn.SetWriteDeadline(deadline)
247 | return c.encode(v)
248 | }
249 |
250 | func (c *jsonCodec) close() {
251 | c.closer.Do(func() {
252 | close(c.closeCh)
253 | c.conn.Close()
254 | })
255 | }
256 |
257 | // Closed returns a channel which will be closed when Close is called
258 | func (c *jsonCodec) closed() <-chan interface{} {
259 | return c.closeCh
260 | }
261 |
262 | // parseMessage parses raw bytes as a (batch of) JSON-RPC message(s). There are no error
263 | // checks in this function because the raw message has already been syntax-checked when it
264 | // is called. Any non-JSON-RPC messages in the input return the zero value of
265 | // jsonrpcMessage.
266 | func parseMessage(raw json.RawMessage) ([]*jsonrpcMessage, bool, error) {
267 | if !isBatch(raw) {
268 | msgs := []*jsonrpcMessage{{}}
269 | json.Unmarshal(raw, &msgs[0])
270 | if msgs[0] == nil {
271 | return nil, false, &invalidRequestError{"invalid request"}
272 | }
273 | return msgs, false, nil
274 | }
275 | dec := json.NewDecoder(bytes.NewReader(raw))
276 | dec.Token() // skip '['
277 | var msgs []*jsonrpcMessage
278 | for dec.More() {
279 | msgs = append(msgs, new(jsonrpcMessage))
280 | dec.Decode(&msgs[len(msgs)-1])
281 | if msgs[len(msgs)-1] == nil {
282 | return nil, false, &invalidRequestError{"invalid request"}
283 | }
284 | }
285 | return msgs, true, nil
286 | }
287 |
288 | // isBatch returns true when the first non-whitespace characters is '['
289 | func isBatch(raw json.RawMessage) bool {
290 | for _, c := range raw {
291 | // skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt)
292 | if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d {
293 | continue
294 | }
295 | return c == '['
296 | }
297 | return false
298 | }
299 |
300 | // parsePositionalArguments tries to parse the given args to an array of values with the
301 | // given types. It returns the parsed values or an error when the args could not be
302 | // parsed. Missing optional arguments are returned as reflect.Zero values.
303 | func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type, isVariadic bool) ([]reflect.Value, error) {
304 | dec := json.NewDecoder(bytes.NewReader(rawArgs))
305 | var args []reflect.Value
306 | tok, err := dec.Token()
307 | switch {
308 | case err == io.EOF || tok == nil && err == nil:
309 | // "params" is optional and may be empty. Also allow "params":null even though it's
310 | // not in the spec because our own client used to send it.
311 | case err != nil:
312 | return nil, err
313 | case tok == json.Delim('['):
314 | // Read argument array.
315 | if args, err = parseArgumentArray(dec, types, isVariadic); err != nil {
316 | return nil, err
317 | }
318 | default:
319 | return nil, errors.New("non-array args")
320 | }
321 | // Set any missing args to nil.
322 | for i := len(args); i < len(types); i++ {
323 | // If the function type's final input parameter is a "..." parameter, the final argument type must be slice.
324 | // We don't even need to append any zero value neither, since they are optional.
325 | if isVariadic && i == len(types)-1 && types[i].Kind() == reflect.Slice {
326 | continue
327 | }
328 |
329 | if types[i].Kind() != reflect.Ptr {
330 | return nil, fmt.Errorf("missing value for required argument %d", i)
331 | }
332 |
333 | args = append(args, reflect.Zero(types[i]))
334 | }
335 | return args, nil
336 | }
337 |
338 | func parseArgumentArray(dec *json.Decoder, types []reflect.Type, isVariadic bool) ([]reflect.Value, error) {
339 | // Sanity check to make sure final argument type must be slice for variadic parameters.
340 | if isVariadic && types[len(types)-1].Kind() != reflect.Slice {
341 | return []reflect.Value{}, fmt.Errorf("argument type of variadic parameters must be slice rathan than %v", types[len(types)-1].String())
342 | }
343 |
344 | args := make([]reflect.Value, 0, len(types))
345 | for i := 0; dec.More(); i++ {
346 | if i >= len(types) && !isVariadic { // more parameters than argument types are only allowed for variadic parameters.
347 | return args, fmt.Errorf("too many arguments, want at most %d", len(types))
348 | }
349 |
350 | var argval reflect.Value
351 | if isVariadic && i >= len(types)-1 { // final argument type is slice, but variadic parameters are passed one by one as an element item type.
352 | argval = reflect.New(types[len(types)-1].Elem())
353 | } else {
354 | argval = reflect.New(types[i])
355 | }
356 |
357 | if err := dec.Decode(argval.Interface()); err != nil {
358 | return args, fmt.Errorf("invalid argument %d: %v", i, err)
359 | }
360 | if argval.IsNil() && types[i].Kind() != reflect.Ptr {
361 | return args, fmt.Errorf("missing value for required argument %d", i)
362 | }
363 | args = append(args, argval.Elem())
364 | }
365 | // Read end of args array.
366 | _, err := dec.Token()
367 | return args, err
368 | }
369 |
370 | // parseSubscriptionName extracts the subscription name from an encoded argument array.
371 | func parseSubscriptionName(rawArgs json.RawMessage) (string, error) {
372 | dec := json.NewDecoder(bytes.NewReader(rawArgs))
373 | if tok, _ := dec.Token(); tok != json.Delim('[') {
374 | return "", errors.New("non-array args")
375 | }
376 | v, _ := dec.Token()
377 | method, ok := v.(string)
378 | if !ok {
379 | return "", errors.New("expected subscription name as first argument")
380 | }
381 | return method, nil
382 | }
383 |
--------------------------------------------------------------------------------
/metrics.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "fmt"
21 |
22 | "github.com/ethereum/go-ethereum/metrics"
23 | ethrpc "github.com/ethereum/go-ethereum/rpc"
24 | )
25 |
26 | var (
27 | rpcRequestGauge = metrics.GetOrRegisterGauge("rpc/requests", nil)
28 | successfulRequestGauge = metrics.GetOrRegisterGauge("rpc/success", nil)
29 | failedReqeustGauge = metrics.GetOrRegisterGauge("rpc/failure", nil)
30 | rpcServingTimer = metrics.GetOrRegisterTimer("rpc/duration/all", nil)
31 | // just for first import ethrpc to avoid above metrics be empty.
32 | _ = ethrpc.API{}
33 | )
34 |
35 | func newRPCServingTimer(method string, valid bool) metrics.Timer {
36 | flag := "success"
37 | if !valid {
38 | flag = "failure"
39 | }
40 | m := fmt.Sprintf("rpc/duration/%s/%s", method, flag)
41 | return metrics.GetOrRegisterTimer(m, nil)
42 | }
43 |
--------------------------------------------------------------------------------
/provider_wrapper/circuit_breaker.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "errors"
5 | "math"
6 | "sync"
7 | "time"
8 |
9 | "github.com/mcuadros/go-defaults"
10 | "github.com/openweb3/go-rpc-provider/utils"
11 | )
12 |
13 | type CircuitBreaker interface {
14 | Do(handler func() error) error
15 | State() BreakerState
16 | }
17 |
18 | type BreakerState int
19 |
20 | const (
21 | BREAKER_CLOSED BreakerState = iota
22 | BREAKER_HALF_OPEN
23 | BREAKER_OPEN
24 | )
25 |
26 | var ErrCircuitOpen = errors.New("circuit breaked")
27 | var ErrUnknownCircuitState = errors.New("unknown circuit state")
28 |
29 | type DefaultCircuitBreakerOption struct {
30 | MaxFail int `default:"5"`
31 | FailTimeWindow time.Duration `default:"10s"` // continuous fail maxFail times between failTimeWindow, close -> open
32 | OpenColdTime time.Duration `default:"10s"` // after openColdTime, open -> halfopen
33 | }
34 |
35 | type DefaultCircuitBreaker struct {
36 | DefaultCircuitBreakerOption
37 | failHistory []time.Time
38 | lastState BreakerState // the state changed when Do
39 | sync.Mutex
40 | }
41 |
42 | func NewDefaultCircuitBreaker(option ...DefaultCircuitBreakerOption) *DefaultCircuitBreaker {
43 | if len(option) == 0 {
44 | option = append(option, DefaultCircuitBreakerOption{})
45 | }
46 | defaults.SetDefaults(&option[0])
47 |
48 | return &DefaultCircuitBreaker{
49 | DefaultCircuitBreakerOption: option[0],
50 | }
51 | }
52 |
53 | func (c *DefaultCircuitBreaker) Do(handler func() error) error {
54 | switch c.State() {
55 | case BREAKER_CLOSED:
56 | err := handler()
57 |
58 | c.Lock()
59 | defer c.Unlock()
60 |
61 | if err == nil || utils.IsRPCJSONError(err) {
62 | c.failHistory = []time.Time{}
63 | c.lastState = BREAKER_CLOSED
64 | return err
65 | } else {
66 | c.failHistory = append(c.failHistory, time.Now())
67 | if !c.isMaxfailAcheived() {
68 | return err
69 | }
70 |
71 | c.lastState = BREAKER_OPEN
72 | }
73 |
74 | return err
75 |
76 | case BREAKER_HALF_OPEN:
77 | err := handler()
78 |
79 | c.Lock()
80 | defer c.Unlock()
81 |
82 | c.failHistory = []time.Time{}
83 | if err == nil || utils.IsRPCJSONError(err) {
84 | c.lastState = BREAKER_CLOSED
85 | } else {
86 | c.failHistory = append(c.failHistory, time.Now())
87 | c.lastState = BREAKER_OPEN
88 | }
89 | return err
90 |
91 | case BREAKER_OPEN:
92 | return ErrCircuitOpen
93 |
94 | default:
95 | return ErrUnknownCircuitState
96 | }
97 | }
98 |
99 | func (c *DefaultCircuitBreaker) State() BreakerState {
100 | c.Lock()
101 | defer c.Unlock()
102 |
103 | if c.lastState == BREAKER_OPEN && c.sinceLastFail() > c.OpenColdTime {
104 | return BREAKER_HALF_OPEN
105 | }
106 |
107 | return c.lastState
108 | }
109 |
110 | func (c *DefaultCircuitBreaker) isMaxfailAcheived() bool {
111 | isReached, maxfailUsedTime := c.maxfailUsedTime()
112 |
113 | if !isReached || maxfailUsedTime > c.FailTimeWindow {
114 | return false
115 | }
116 | return true
117 | }
118 |
119 | // 1st return means if reached max fail.
120 | func (c *DefaultCircuitBreaker) maxfailUsedTime() (bool, time.Duration) {
121 | failLen := len(c.failHistory)
122 | if failLen < c.MaxFail {
123 | return false, 0
124 | }
125 |
126 | return true, time.Since(c.failHistory[failLen-c.MaxFail]) - c.sinceLastFail()
127 | }
128 |
129 | // note: return max int64 when failHistory is empty becase that means no failure before.
130 | func (c *DefaultCircuitBreaker) sinceLastFail() time.Duration {
131 | if len(c.failHistory) == 0 {
132 | return math.MaxInt64
133 | }
134 | lastFail := c.failHistory[len(c.failHistory)-1]
135 | return time.Since(lastFail)
136 | }
137 |
--------------------------------------------------------------------------------
/provider_wrapper/circuit_breaker_test.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | "time"
7 |
8 | "gotest.tools/assert"
9 | )
10 |
11 | func TestCircuitBreakerClose2Open(t *testing.T) {
12 | cb := DefaultCircuitBreaker{
13 | DefaultCircuitBreakerOption: DefaultCircuitBreakerOption{
14 | MaxFail: 3,
15 | FailTimeWindow: time.Second,
16 | OpenColdTime: time.Millisecond * 500,
17 | },
18 | }
19 |
20 | ErrTest := errors.New("test")
21 | // is closed
22 | assert.Equal(t, cb.State(), BREAKER_CLOSED)
23 |
24 | // closed -> open
25 | cb.Do(func() error { return ErrTest })
26 | cb.Do(func() error { return ErrTest })
27 | assert.Equal(t, cb.State(), BREAKER_CLOSED)
28 |
29 | time.Sleep(cb.FailTimeWindow)
30 |
31 | cb.Do(func() error { return ErrTest })
32 | assert.Equal(t, cb.State(), BREAKER_CLOSED)
33 |
34 | cb.Do(func() error { return ErrTest })
35 | cb.Do(func() error { return ErrTest })
36 | assert.Equal(t, cb.State(), BREAKER_OPEN)
37 | }
38 |
39 | func TestCircuitBreakerOpen2HalfOpen(t *testing.T) {
40 | cb := DefaultCircuitBreaker{
41 | DefaultCircuitBreakerOption: DefaultCircuitBreakerOption{
42 | MaxFail: 3,
43 | FailTimeWindow: time.Second,
44 | OpenColdTime: time.Millisecond * 500,
45 | },
46 | failHistory: []time.Time{time.Now(), time.Now(), time.Now()},
47 | lastState: BREAKER_OPEN,
48 | }
49 |
50 | // is open
51 | assert.Equal(t, cb.State(), BREAKER_OPEN)
52 |
53 | err := cb.Do(func() error { return errors.New("") })
54 | assert.Equal(t, err, ErrCircuitOpen)
55 |
56 | // open -> half open
57 | time.Sleep(cb.OpenColdTime)
58 | assert.Equal(t, cb.State(), BREAKER_HALF_OPEN)
59 | }
60 |
61 | func TestCircuitBreakerHalfOpen2Other(t *testing.T) {
62 | cb := DefaultCircuitBreaker{
63 | DefaultCircuitBreakerOption: DefaultCircuitBreakerOption{
64 | MaxFail: 3,
65 | FailTimeWindow: time.Second,
66 | OpenColdTime: time.Millisecond * 500,
67 | },
68 | lastState: BREAKER_HALF_OPEN,
69 | }
70 |
71 | ErrTest := errors.New("test")
72 |
73 | // half open -> open
74 | err := cb.Do(func() error { return ErrTest })
75 | assert.Equal(t, err, ErrTest)
76 | assert.Equal(t, cb.State(), BREAKER_OPEN)
77 |
78 | // half open -> closed
79 | time.Sleep(cb.OpenColdTime)
80 | assert.Equal(t, cb.State(), BREAKER_HALF_OPEN)
81 | cb.Do(func() error { return nil })
82 | assert.Equal(t, cb.State(), BREAKER_CLOSED)
83 | }
84 |
--------------------------------------------------------------------------------
/provider_wrapper/generic.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/openweb3/go-rpc-provider"
7 | "github.com/openweb3/go-rpc-provider/interfaces"
8 | )
9 |
10 | // Call is a generic method to call RPC.
11 | func Call[T any](provider interfaces.Provider, method string, args ...any) (result T, err error) {
12 | return CallContext[T](provider, context.Background(), method, args...)
13 | }
14 |
15 | // CallContext is a generic method to call RPC with context.
16 | func CallContext[T any](provider interfaces.Provider, ctx context.Context, method string, args ...any) (result T, err error) {
17 | err = provider.CallContext(ctx, &result, method, args...)
18 | return
19 | }
20 |
21 | type Request struct {
22 | Method string
23 | Args []any
24 | }
25 |
26 | type Response[T any] struct {
27 | Data T
28 | Error error
29 | }
30 |
31 | // BatchCallContext is a generic method to call RPC with context in batch.
32 | func BatchCallContext[T any](provider interfaces.Provider, ctx context.Context, requests ...Request) ([]Response[T], error) {
33 | batch := make([]rpc.BatchElem, 0, len(requests))
34 | responses := make([]Response[T], len(requests))
35 |
36 | for i, v := range requests {
37 | batch = append(batch, rpc.BatchElem{
38 | Method: v.Method,
39 | Args: v.Args,
40 | Result: &responses[i].Data,
41 | })
42 | }
43 |
44 | if err := provider.BatchCallContext(ctx, batch); err != nil {
45 | return nil, err
46 | }
47 |
48 | for i, v := range batch {
49 | if v.Error != nil {
50 | responses[i].Error = v.Error
51 | }
52 | }
53 |
54 | return responses, nil
55 | }
56 |
57 | // BatchCallOneContext is generic method to call a single RPC with context in batch.
58 | func BatchCallOneContext[T any](provider interfaces.Provider, ctx context.Context, method string, batchArgs ...[]any) ([]Response[T], error) {
59 | requests := make([]Request, 0, len(batchArgs))
60 |
61 | for _, v := range batchArgs {
62 | requests = append(requests, Request{
63 | Method: method,
64 | Args: v,
65 | })
66 | }
67 |
68 | return BatchCallContext[T](provider, ctx, requests...)
69 | }
70 |
71 | // BatchCall is a generic method to call RPC in batch.
72 | func BatchCall[T any](provider interfaces.Provider, requests ...Request) ([]Response[T], error) {
73 | return BatchCallContext[T](provider, context.Background(), requests...)
74 | }
75 |
76 | // BatchCallOne is a generic method to call a single RPC in batch.
77 | func BatchCallOne[T any](provider interfaces.Provider, method string, batchArgs ...[]any) ([]Response[T], error) {
78 | return BatchCallOneContext[T](provider, context.Background(), method, batchArgs...)
79 | }
80 |
--------------------------------------------------------------------------------
/provider_wrapper/provider.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 | "io"
6 | "time"
7 |
8 | "github.com/mcuadros/go-defaults"
9 | )
10 |
11 | // Option for set retry and timeout options
12 | // Note: user could overwrite RequestTimeout when CallContext with timeout context or cancel context
13 | type Option struct {
14 | // retry
15 | RetryCount int
16 | RetryInterval time.Duration `default:"1s"`
17 |
18 | RequestTimeout time.Duration `default:"30s"`
19 |
20 | MaxConnectionPerHost int
21 | Logger io.Writer
22 | CircuitBreaker CircuitBreaker
23 | }
24 |
25 | func (o *Option) WithRetry(retryCount int, retryInterval time.Duration) *Option {
26 | o.RetryCount = retryCount
27 | o.RetryInterval = retryInterval
28 | return o
29 | }
30 |
31 | func (o *Option) WithTimout(requestTimeout time.Duration) *Option {
32 | o.RequestTimeout = requestTimeout
33 | return o
34 | }
35 |
36 | func (o *Option) WithMaxConnectionPerHost(maxConnectionPerHost int) *Option {
37 | o.MaxConnectionPerHost = maxConnectionPerHost
38 | return o
39 | }
40 |
41 | func (o *Option) WithLooger(w io.Writer) *Option {
42 | o.Logger = w
43 | return o
44 | }
45 |
46 | func (o *Option) WithCircuitBreaker(circuitBreakerOption DefaultCircuitBreakerOption) *Option {
47 | o.CircuitBreaker = NewDefaultCircuitBreaker(circuitBreakerOption)
48 | return o
49 | }
50 |
51 | // NewProviderWithOption returns a new MiddlewareProvider with hook handlers build according to options
52 | // Note: user could overwrite RequestTimeout when CallContext with timeout context or cancel context
53 | func NewProviderWithOption(rawurl string, option Option) (*MiddlewarableProvider, error) {
54 | p, err := NewBaseProvider(context.Background(), rawurl, option.MaxConnectionPerHost)
55 | if err != nil {
56 | return nil, err
57 | }
58 |
59 | defaults.SetDefaults(&option)
60 | p = wrapProvider(p, option)
61 | return p, nil
62 | }
63 |
64 | // wrapProvider wrap provider accroding to option
65 | func wrapProvider(p *MiddlewarableProvider, option Option) *MiddlewarableProvider {
66 | if option.CircuitBreaker != nil {
67 | p = NewCircuitBreakerProvider(p, option.CircuitBreaker)
68 | }
69 | p = NewTimeoutableProvider(p, option.RequestTimeout)
70 | p = NewRetriableProvider(p, option.RetryCount, option.RetryInterval)
71 | p = NewLoggerProvider(p, option.Logger)
72 | return p
73 | }
74 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_base.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 | "net/url"
6 |
7 | "github.com/openweb3/go-rpc-provider"
8 |
9 | "github.com/valyala/fasthttp"
10 | )
11 |
12 | // NewBaseProvider returns a new BaseProvider.
13 | // maxConnsPerHost is the maximum number of concurrent connections, only works for http(s) protocal
14 | func NewBaseProvider(ctx context.Context, nodeUrl string, maxConnectionNum ...int) (*MiddlewarableProvider, error) {
15 | if len(maxConnectionNum) > 0 && maxConnectionNum[0] > 0 {
16 | if u, err := url.Parse(nodeUrl); err == nil {
17 | if u.Scheme == "http" || u.Scheme == "https" {
18 | fasthttpClient := new(fasthttp.Client)
19 | fasthttpClient.MaxConnsPerHost = maxConnectionNum[0]
20 | p, err := rpc.DialHTTPWithClient(nodeUrl, fasthttpClient)
21 | if err != nil {
22 | return nil, err
23 | }
24 | return NewMiddlewarableProvider(p), nil
25 | }
26 | }
27 | }
28 |
29 | p, err := rpc.DialContext(ctx, nodeUrl)
30 | if err != nil {
31 | return nil, err
32 | }
33 | return NewMiddlewarableProvider(p), nil
34 | }
35 |
36 | // MustNewBaseProvider returns a new BaseProvider. Panic if error.
37 | func MustNewBaseProvider(ctx context.Context, nodeUrl string, maxConnectionNum ...int) *MiddlewarableProvider {
38 | p, err := NewBaseProvider(ctx, nodeUrl, maxConnectionNum...)
39 | if err != nil {
40 | panic(err)
41 | }
42 | return p
43 | }
44 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_breaker.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/openweb3/go-rpc-provider"
7 | "github.com/openweb3/go-rpc-provider/interfaces"
8 | )
9 |
10 | func NewCircuitBreakerProvider(inner interfaces.Provider, breaker CircuitBreaker) *MiddlewarableProvider {
11 | m := NewMiddlewarableProvider(inner)
12 |
13 | b := &CircuitBreakerMiddleware{breaker}
14 | m.HookCallContext(b.callContextMiddleware)
15 | m.HookBatchCallContext(b.batchCallContextMiddleware)
16 | return m
17 | }
18 |
19 | type CircuitBreakerMiddleware struct {
20 | breaker CircuitBreaker
21 | }
22 |
23 | func (r *CircuitBreakerMiddleware) callContextMiddleware(call CallContextFunc) CallContextFunc {
24 | return func(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
25 | handler := func() error {
26 | return call(ctx, resultPtr, method, args...)
27 | }
28 | return r.breaker.Do(handler)
29 | }
30 | }
31 |
32 | func (r *CircuitBreakerMiddleware) batchCallContextMiddleware(call BatchCallContextFunc) BatchCallContextFunc {
33 | return func(ctx context.Context, b []rpc.BatchElem) error {
34 | handler := func() error {
35 | return call(ctx, b)
36 | }
37 | return r.breaker.Do(handler)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_logger.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "os"
9 | "time"
10 |
11 | "github.com/fatih/color"
12 | "github.com/openweb3/go-rpc-provider"
13 | "github.com/openweb3/go-rpc-provider/interfaces"
14 | "github.com/openweb3/go-rpc-provider/utils"
15 | )
16 |
17 | type LogMiddleware struct {
18 | Writer io.Writer
19 | }
20 |
21 | func NewLoggerProvider(p interfaces.Provider, w io.Writer) *MiddlewarableProvider {
22 | mp := NewMiddlewarableProvider(p)
23 | if w != nil {
24 | logMiddleware := &LogMiddleware{Writer: w}
25 | mp.HookCallContext(logMiddleware.callContextMiddleware)
26 | mp.HookBatchCallContext(logMiddleware.batchCallContextMiddleware)
27 | }
28 | return mp
29 | }
30 |
31 | func (m *LogMiddleware) callContextMiddleware(call CallContextFunc) CallContextFunc {
32 | return func(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
33 | argsStr := fmt.Sprintf("%+v", args)
34 | argsJson, err := json.Marshal(args)
35 | if err == nil {
36 | argsStr = string(argsJson)
37 | }
38 |
39 | start := time.Now()
40 | err = call(ctx, resultPtr, method, args...)
41 | duration := time.Since(start)
42 |
43 | if m.Writer == os.Stdout {
44 | if err == nil {
45 | fmt.Printf("%v Method %v, Params %v, Result %v, Use %v\n",
46 | color.GreenString("[Call RPC Done]"),
47 | color.YellowString(method),
48 | color.CyanString(argsStr),
49 | color.CyanString(utils.PrettyJSON(resultPtr)),
50 | color.CyanString(duration.String()))
51 | return nil
52 | }
53 |
54 | color.Red("%v Method %v, Params %v, Error %v, Use %v\n",
55 | color.RedString("[Call RPC Fail]"),
56 | color.YellowString(method),
57 | color.CyanString(string(argsJson)),
58 | color.RedString(fmt.Sprintf("%+v", err)),
59 | color.CyanString(duration.String()))
60 | return err
61 | }
62 |
63 | if err == nil {
64 | l := fmt.Sprintf("%v Method %v, Params %v, Result %v, Use %v\n",
65 | "[Call RPC Done]", method, argsStr, utils.PrettyJSON(resultPtr), duration.String())
66 | m.Writer.Write([]byte(l))
67 | return nil
68 | }
69 |
70 | el := fmt.Sprintf("%v Method %v, Params %v, Error %v, Use %v\n",
71 | "[Call RPC Fail]", method, string(argsJson), fmt.Sprintf("%+v", err), duration.String())
72 | m.Writer.Write([]byte(el))
73 | return err
74 | }
75 | }
76 |
77 | func (m *LogMiddleware) batchCallContextMiddleware(batchCall BatchCallContextFunc) BatchCallContextFunc {
78 | return func(ctx context.Context, b []rpc.BatchElem) error {
79 | start := time.Now()
80 |
81 | err := batchCall(ctx, b)
82 |
83 | duration := time.Since(start)
84 |
85 | if m.Writer == os.Stdout {
86 | if err == nil {
87 | fmt.Printf("%v BatchElems %v, Use %v\n",
88 | color.GreenString("[Batch Call RPC Done]"),
89 | color.CyanString(utils.PrettyJSON(b)),
90 | color.CyanString(duration.String()))
91 | return nil
92 | }
93 | fmt.Printf("%v BatchElems %v, Error: %v, Use %v\n",
94 | color.RedString("[Batch Call RPC Fail]"),
95 | color.CyanString(utils.PrettyJSON(b)),
96 | color.RedString(fmt.Sprintf("%+v", err)),
97 | duration)
98 | return err
99 | }
100 |
101 | if err == nil {
102 | l := fmt.Sprintf("%v BatchElems %v, Use %v\n",
103 | "[Batch Call RPC Done]",
104 | utils.PrettyJSON(b),
105 | duration.String())
106 | m.Writer.Write([]byte(l))
107 | return nil
108 | }
109 |
110 | el := fmt.Sprintf("%v BatchElems %v, Error: %v, Use %v\n",
111 | "[Batch Call RPC Fail]",
112 | utils.PrettyJSON(b),
113 | fmt.Sprintf("%+v", err),
114 | duration)
115 | m.Writer.Write([]byte(el))
116 | return err
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_middleware.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/openweb3/go-rpc-provider"
7 | "github.com/openweb3/go-rpc-provider/interfaces"
8 | )
9 |
10 | type MiddlewarableProvider struct {
11 | Inner interfaces.Provider
12 |
13 | callContextMiddles []CallContextMiddleware
14 | batchCallContextMiddles []BatchCallContextMiddleware
15 | subscribeMiddles []SubscribeMiddleware
16 | subscribeWithReconnMiddles []SubscribeWithReconnMiddleware
17 |
18 | callContextNestedWare CallContextFunc
19 | batchcallContextNestedWare BatchCallContextFunc
20 | subscribeNestedWare SubscribeFunc
21 | subscribeWithReconnNestedWare SubscribeWithReconnFunc
22 | }
23 |
24 | func NewMiddlewarableProvider(p interfaces.Provider) *MiddlewarableProvider {
25 | if _, ok := p.(*MiddlewarableProvider); ok {
26 | return p.(*MiddlewarableProvider)
27 | }
28 |
29 | m := &MiddlewarableProvider{Inner: p,
30 | callContextNestedWare: p.CallContext,
31 | batchcallContextNestedWare: p.BatchCallContext,
32 | subscribeNestedWare: p.Subscribe,
33 | subscribeWithReconnNestedWare: p.SubscribeWithReconn,
34 | }
35 | return m
36 | }
37 |
38 | // type CallFunc func(resultPtr interface{}, method string, args ...interface{}) error
39 | type CallContextFunc func(ctx context.Context, result interface{}, method string, args ...interface{}) error
40 |
41 | // type BatchCallFunc func(b []rpc.BatchElem) error
42 | type BatchCallContextFunc func(ctx context.Context, b []rpc.BatchElem) error
43 |
44 | type SubscribeFunc func(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (*rpc.ClientSubscription, error)
45 |
46 | type SubscribeWithReconnFunc func(ctx context.Context, namespace string, channel interface{}, args ...interface{}) *rpc.ReconnClientSubscription
47 |
48 | // type CallMiddleware func(CallFunc) CallFunc
49 | type CallContextMiddleware func(CallContextFunc) CallContextFunc
50 |
51 | // type BatchCallMiddleware func(BatchCallFunc) BatchCallFunc
52 | type BatchCallContextMiddleware func(BatchCallContextFunc) BatchCallContextFunc
53 |
54 | type SubscribeMiddleware func(SubscribeFunc) SubscribeFunc
55 |
56 | type SubscribeWithReconnMiddleware func(SubscribeWithReconnFunc) SubscribeWithReconnFunc
57 |
58 | // callMiddles: A -> B -> C, execute order is A -> B -> C
59 | func (mp *MiddlewarableProvider) HookCallContext(cm CallContextMiddleware) {
60 | mp.callContextMiddles = append(mp.callContextMiddles, cm)
61 | mp.callContextNestedWare = mp.Inner.CallContext
62 | for i := len(mp.callContextMiddles) - 1; i >= 0; i-- {
63 | mp.callContextNestedWare = mp.callContextMiddles[i](mp.callContextNestedWare)
64 | }
65 | }
66 |
67 | func (mp *MiddlewarableProvider) HookBatchCallContext(cm BatchCallContextMiddleware) {
68 | mp.batchCallContextMiddles = append(mp.batchCallContextMiddles, cm)
69 | mp.batchcallContextNestedWare = mp.Inner.BatchCallContext
70 | for i := len(mp.batchCallContextMiddles) - 1; i >= 0; i-- {
71 | mp.batchcallContextNestedWare = mp.batchCallContextMiddles[i](mp.batchcallContextNestedWare)
72 | }
73 | }
74 |
75 | func (mp *MiddlewarableProvider) HookSubscribe(cm SubscribeMiddleware) {
76 | mp.subscribeMiddles = append(mp.subscribeMiddles, cm)
77 | mp.subscribeNestedWare = mp.Inner.Subscribe
78 | for i := len(mp.subscribeMiddles) - 1; i >= 0; i-- {
79 | mp.subscribeNestedWare = mp.subscribeMiddles[i](mp.subscribeNestedWare)
80 | }
81 | }
82 |
83 | func (mp *MiddlewarableProvider) HookSubscribeWithReconn(cm SubscribeWithReconnMiddleware) {
84 | mp.subscribeWithReconnMiddles = append(mp.subscribeWithReconnMiddles, cm)
85 | mp.subscribeWithReconnNestedWare = mp.Inner.SubscribeWithReconn
86 | for i := len(mp.subscribeWithReconnMiddles) - 1; i >= 0; i-- {
87 | mp.subscribeWithReconnNestedWare = mp.subscribeWithReconnMiddles[i](mp.subscribeWithReconnNestedWare)
88 | }
89 | }
90 |
91 | func (mp *MiddlewarableProvider) CallContext(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
92 | return mp.callContextNestedWare(ctx, resultPtr, method, args...)
93 | }
94 |
95 | func (mp *MiddlewarableProvider) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
96 | return mp.batchcallContextNestedWare(ctx, b)
97 | }
98 |
99 | func (mp *MiddlewarableProvider) Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (*rpc.ClientSubscription, error) {
100 | return mp.subscribeNestedWare(ctx, namespace, channel, args...)
101 | }
102 |
103 | func (mp *MiddlewarableProvider) SubscribeWithReconn(ctx context.Context, namespace string, channel interface{}, args ...interface{}) *rpc.ReconnClientSubscription {
104 | return mp.subscribeWithReconnNestedWare(ctx, namespace, channel, args...)
105 | }
106 |
107 | func (mp *MiddlewarableProvider) Close() {
108 | mp.Inner.Close()
109 | }
110 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_middleware_test.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "testing"
8 |
9 | "github.com/ethereum/go-ethereum/common/hexutil"
10 | "github.com/openweb3/go-rpc-provider"
11 | "gotest.tools/assert"
12 | )
13 |
14 | var executeStack []byte = make([]byte, 0)
15 |
16 | func TestHookCallContext(t *testing.T) {
17 | executeStack = make([]byte, 0)
18 | p, e := rpc.DialContext(context.Background(), "http://localhost:8545")
19 | if e != nil {
20 | t.Fatal(e)
21 | }
22 | mp := NewMiddlewarableProvider(p)
23 |
24 | mp.HookCallContext(callContextMiddle1)
25 | mp.HookCallContext(callContextMiddle2)
26 |
27 | b := new(hexutil.Big)
28 | mp.CallContext(context.Background(), b, "eth_blockNumber")
29 | assert.DeepEqual(t, executeStack, []byte{1, 2, 3, 4})
30 | }
31 |
32 | func callContextMiddle1(f CallContextFunc) CallContextFunc {
33 | return func(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
34 | executeStack = append(executeStack, 1)
35 | fmt.Printf("call %v %v\n", method, args)
36 | ctx = context.WithValue(ctx, "foo", "bar")
37 | err := f(ctx, resultPtr, method, args...)
38 | j, _ := json.Marshal(resultPtr)
39 | fmt.Printf("response %s\n", j)
40 | executeStack = append(executeStack, 4)
41 | return err
42 | }
43 | }
44 |
45 | func callContextMiddle2(f CallContextFunc) CallContextFunc {
46 | return func(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
47 | executeStack = append(executeStack, 2)
48 | fmt.Printf("call %v %v with context key foo value %v\n", method, args, ctx.Value("foo"))
49 | err := f(ctx, resultPtr, method, args...)
50 | executeStack = append(executeStack, 3)
51 | return err
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_retry.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/openweb3/go-rpc-provider"
8 | "github.com/openweb3/go-rpc-provider/interfaces"
9 | "github.com/openweb3/go-rpc-provider/utils"
10 | "github.com/pkg/errors"
11 | )
12 |
13 | func NewRetriableProvider(inner interfaces.Provider, maxRetry int, interval time.Duration) *MiddlewarableProvider {
14 | m := NewMiddlewarableProvider(inner)
15 |
16 | r := &RetriableMiddleware{maxRetry, interval}
17 | m.HookCallContext(r.callContextMiddleware)
18 | m.HookBatchCallContext(r.batchCallContextMiddleware)
19 | return m
20 | }
21 |
22 | type RetriableMiddleware struct {
23 | maxRetry int
24 | interval time.Duration
25 | }
26 |
27 | func (r *RetriableMiddleware) callContextMiddleware(call CallContextFunc) CallContextFunc {
28 | return func(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
29 | handler := func() error {
30 | return call(ctx, resultPtr, method, args...)
31 | }
32 | return retry(r.maxRetry, r.interval, handler)
33 | }
34 | }
35 |
36 | func (r *RetriableMiddleware) batchCallContextMiddleware(call BatchCallContextFunc) BatchCallContextFunc {
37 | return func(ctx context.Context, b []rpc.BatchElem) error {
38 | handler := func() error {
39 | return call(ctx, b)
40 | }
41 | return retry(r.maxRetry, r.interval, handler)
42 | }
43 | }
44 |
45 | func retry(maxRetry int, interval time.Duration, handler func() error) error {
46 | remain := maxRetry
47 | for {
48 | err := handler()
49 | if err == nil {
50 | return nil
51 | }
52 |
53 | if utils.IsRPCJSONError(err) {
54 | return err
55 | }
56 |
57 | if remain <= 0 {
58 | return errors.Wrapf(err, "failed after %v retries", maxRetry)
59 | }
60 |
61 | if interval > 0 {
62 | time.Sleep(interval)
63 | }
64 |
65 | remain--
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_test.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 | "time"
10 |
11 | "github.com/mcuadros/go-defaults"
12 | "gotest.tools/assert"
13 | )
14 |
15 | func TestConfigurationDefault(t *testing.T) {
16 | c := Option{}
17 | defaults.SetDefaults(&c)
18 | assert.Equal(t, c.RetryCount, 0)
19 | assert.Equal(t, c.RetryInterval, 1*time.Second)
20 | assert.Equal(t, c.RequestTimeout, 30*time.Second)
21 |
22 | c = Option{RetryCount: 10}
23 | defaults.SetDefaults(&c)
24 | assert.Equal(t, c.RetryCount, 10)
25 | assert.Equal(t, c.RetryInterval, 1*time.Second)
26 | assert.Equal(t, c.RequestTimeout, 30*time.Second)
27 | }
28 |
29 | func TestProviderShouldCircuitBreak(t *testing.T) {
30 | p, err := NewProviderWithOption("http://localhost:1234", *new(Option).WithCircuitBreaker(DefaultCircuitBreakerOption{}))
31 | assert.NilError(t, err)
32 |
33 | var result interface{}
34 | for i := 0; i < 3; i++ {
35 | go func() {
36 | for {
37 | err = p.CallContext(context.Background(), &result, "xx")
38 | fmt.Printf("%v %v\n", time.Now().Format(time.RFC3339), err)
39 | time.Sleep(time.Millisecond * 500)
40 | }
41 | }()
42 | }
43 | select {}
44 | }
45 |
46 | func TestTimeoutProvider(t *testing.T) {
47 | // 创建一个模拟的慢速服务器
48 | slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49 | time.Sleep(2 * time.Second)
50 | fmt.Fprintln(w, `{"jsonrpc":"2.0","id":1,"result":"ok"}`)
51 | }))
52 | defer slowServer.Close()
53 |
54 | // 创建一个超时时间为1秒的Provider
55 | p, err := NewProviderWithOption(slowServer.URL, Option{RequestTimeout: 1 * time.Second})
56 | assert.NilError(t, err)
57 |
58 | var result interface{}
59 | ctx := context.Background()
60 |
61 | // 调用应该因超时而失败
62 | err = p.CallContext(ctx, &result, "test_method")
63 | assert.Assert(t, err != nil, "预期出现超时错误")
64 | assert.ErrorContains(t, err, "timeout", "错误应该是超时")
65 |
66 | // 创建一个超时时间为3秒的Provider
67 | p, err = NewProviderWithOption(slowServer.URL, Option{RequestTimeout: 3 * time.Second})
68 | assert.NilError(t, err)
69 |
70 | // 调用应该成功
71 | err = p.CallContext(ctx, &result, "test_method")
72 | assert.NilError(t, err, "调用应该成功")
73 | assert.Equal(t, result, "ok", "结果应该是'ok'")
74 | }
75 |
--------------------------------------------------------------------------------
/provider_wrapper/provider_timeout.go:
--------------------------------------------------------------------------------
1 | package providers
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | rpc "github.com/openweb3/go-rpc-provider"
8 | "github.com/openweb3/go-rpc-provider/interfaces"
9 | )
10 |
11 | // TimeoutableProvider overwrite Call by CallContext with timeout context, make it to internal package to prevent external package to use it.
12 | type TimeoutableProvider struct {
13 | MiddlewarableProvider
14 | }
15 |
16 | func NewTimeoutableProvider(inner interfaces.Provider, timeout time.Duration) *MiddlewarableProvider {
17 | m := NewMiddlewarableProvider(inner)
18 | timeoutMiddle := TimeoutMiddleware{Timeout: timeout}
19 |
20 | m.HookCallContext(timeoutMiddle.CallContext)
21 | m.HookBatchCallContext(timeoutMiddle.BatchCallContext)
22 | m.HookSubscribe(timeoutMiddle.Subscribe)
23 |
24 | return m
25 | }
26 |
27 | type TimeoutMiddleware struct {
28 | Timeout time.Duration
29 | }
30 |
31 | func (t TimeoutMiddleware) CallContext(call CallContextFunc) CallContextFunc {
32 | return func(ctx context.Context, resultPtr interface{}, method string, args ...interface{}) error {
33 | ctx, f := t.setContext(ctx)
34 | defer f()
35 | return call(ctx, resultPtr, method, args...)
36 | }
37 | }
38 |
39 | func (t TimeoutMiddleware) BatchCallContext(call BatchCallContextFunc) BatchCallContextFunc {
40 | return func(ctx context.Context, b []rpc.BatchElem) error {
41 | ctx, f := t.setContext(ctx)
42 | defer f()
43 | return call(ctx, b)
44 | }
45 | }
46 |
47 | func (t TimeoutMiddleware) Subscribe(sub SubscribeFunc) SubscribeFunc {
48 | return func(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (*rpc.ClientSubscription, error) {
49 | ctx, f := t.setContext(ctx)
50 | defer f()
51 | return sub(ctx, namespace, channel, args...)
52 | }
53 | }
54 |
55 | func (t *TimeoutMiddleware) setContext(ctx context.Context) (context.Context, context.CancelFunc) {
56 | _, ok := ctx.Deadline()
57 | if !ok {
58 | ctx, f := context.WithTimeout(ctx, t.Timeout)
59 | return ctx, f
60 | }
61 | return ctx, func() {}
62 | }
63 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "io"
22 | "sync/atomic"
23 |
24 | mapset "github.com/deckarep/golang-set"
25 | "github.com/ethereum/go-ethereum/log"
26 | )
27 |
28 | const MetadataApi = "rpc"
29 |
30 | // CodecOption specifies which type of messages a codec supports.
31 | //
32 | // Deprecated: this option is no longer honored by Server.
33 | type CodecOption int
34 |
35 | const (
36 | // OptionMethodInvocation is an indication that the codec supports RPC method calls
37 | OptionMethodInvocation CodecOption = 1 << iota
38 |
39 | // OptionSubscriptions is an indication that the codec suports RPC notifications
40 | OptionSubscriptions = 1 << iota // support pub sub
41 | )
42 |
43 | // Server is an RPC server.
44 | type Server struct {
45 | services serviceRegistry
46 | idgen func() ID
47 | run int32
48 | codecs mapset.Set
49 |
50 | // serveHTTPNestedWare ServeHTTPFunc
51 | // serveWebsocketNestedWare http.HandlerFunc
52 | }
53 |
54 | // NewServer creates a new server instance with no registered handlers.
55 | func NewServer() *Server {
56 | server := &Server{idgen: randomIDGenerator(), codecs: mapset.NewSet(), run: 1}
57 | // Register the default service providing meta information about the RPC service such
58 | // as the services and methods it offers.
59 | rpcService := &RPCService{server}
60 | server.RegisterName(MetadataApi, rpcService)
61 | return server
62 | }
63 |
64 | // RegisterName creates a service for the given receiver type under the given name. When no
65 | // methods on the given receiver match the criteria to be either a RPC method or a
66 | // subscription an error is returned. Otherwise a new service is created and added to the
67 | // service collection this server provides to clients.
68 | func (s *Server) RegisterName(name string, receiver interface{}) error {
69 | return s.services.registerName(name, receiver)
70 | }
71 |
72 | // ServeCodec reads incoming requests from codec, calls the appropriate callback and writes
73 | // the response back using the given codec. It will block until the codec is closed or the
74 | // server is stopped. In either case the codec is closed.
75 | //
76 | // Note that codec options are no longer supported.
77 | func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) {
78 | defer codec.close()
79 |
80 | // Don't serve if server is stopped.
81 | if atomic.LoadInt32(&s.run) == 0 {
82 | return
83 | }
84 |
85 | // Add the codec to the set so it can be closed by Stop.
86 | s.codecs.Add(codec)
87 | defer s.codecs.Remove(codec)
88 |
89 | c := initClient(codec, s.idgen, &s.services)
90 | <-codec.closed()
91 | c.Close()
92 | }
93 |
94 | // serveSingleRequest reads and processes a single RPC request from the given codec. This
95 | // is used to serve HTTP connections. Subscriptions and reverse calls are not allowed in
96 | // this mode.
97 | func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
98 | // Don't serve if server is stopped.
99 | if atomic.LoadInt32(&s.run) == 0 {
100 | return
101 | }
102 |
103 | h := newHandler(ctx, codec, s.idgen, &s.services)
104 | h.allowSubscribe = false
105 | defer h.close(io.EOF, nil)
106 |
107 | reqs, batch, err := codec.readBatch()
108 | if err != nil {
109 | if err != io.EOF {
110 | codec.writeJSON(ctx, errorMessage(&invalidMessageError{"parse error"}))
111 | }
112 | return
113 | }
114 | if batch {
115 | h.handleBatch(reqs)
116 | } else {
117 | h.handleMsg(reqs[0])
118 | }
119 | }
120 |
121 | // Stop stops reading new requests, waits for stopPendingRequestTimeout to allow pending
122 | // requests to finish, then closes all codecs which will cancel pending requests and
123 | // subscriptions.
124 | func (s *Server) Stop() {
125 | if atomic.CompareAndSwapInt32(&s.run, 1, 0) {
126 | log.Debug("RPC server shutting down")
127 | s.codecs.Each(func(c interface{}) bool {
128 | c.(ServerCodec).close()
129 | return true
130 | })
131 | }
132 | }
133 |
134 | // RPCService gives meta information about the server.
135 | // e.g. gives information about the loaded modules.
136 | type RPCService struct {
137 | server *Server
138 | }
139 |
140 | // Modules returns the list of RPC services with their version number
141 | func (s *RPCService) Modules() map[string]string {
142 | s.server.services.mu.Lock()
143 | defer s.server.services.mu.Unlock()
144 |
145 | modules := make(map[string]string)
146 | for name := range s.server.services.services {
147 | modules[name] = "1.0"
148 | }
149 | return modules
150 | }
151 |
152 | // PeerInfo contains information about the remote end of the network connection.
153 | //
154 | // This is available within RPC method handlers through the context. Call
155 | // PeerInfoFromContext to get information about the client connection related to
156 | // the current method call.
157 | type PeerInfo struct {
158 | // Transport is name of the protocol used by the client.
159 | // This can be "http", "ws" or "ipc".
160 | Transport string
161 |
162 | // Address of client. This will usually contain the IP address and port.
163 | RemoteAddr string
164 |
165 | // Addditional information for HTTP and WebSocket connections.
166 | HTTP struct {
167 | // Protocol version, i.e. "HTTP/1.1". This is not set for WebSocket.
168 | Version string
169 | // Header values sent by the client.
170 | UserAgent string
171 | Origin string
172 | Host string
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/server_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "bufio"
21 | "bytes"
22 | "io"
23 | "io/ioutil"
24 | "net"
25 | "path/filepath"
26 | "strings"
27 | "testing"
28 | "time"
29 | )
30 |
31 | func TestServerRegisterName(t *testing.T) {
32 | server := NewServer()
33 | service := new(testService)
34 |
35 | if err := server.RegisterName("test", service); err != nil {
36 | t.Fatalf("%v", err)
37 | }
38 |
39 | if len(server.services.services) != 2 {
40 | t.Fatalf("Expected 2 service entries, got %d", len(server.services.services))
41 | }
42 |
43 | svc, ok := server.services.services["test"]
44 | if !ok {
45 | t.Fatalf("Expected service calc to be registered")
46 | }
47 |
48 | wantCallbacks := 9
49 | if len(svc.callbacks) != wantCallbacks {
50 | t.Errorf("Expected %d callbacks for service 'service', got %d", wantCallbacks, len(svc.callbacks))
51 | }
52 | }
53 |
54 | func TestServer(t *testing.T) {
55 | files, err := ioutil.ReadDir("testdata")
56 | if err != nil {
57 | t.Fatal("where'd my testdata go?")
58 | }
59 | for _, f := range files {
60 | if f.IsDir() || strings.HasPrefix(f.Name(), ".") {
61 | continue
62 | }
63 | path := filepath.Join("testdata", f.Name())
64 | name := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
65 | t.Run(name, func(t *testing.T) {
66 | runTestScript(t, path)
67 | })
68 | }
69 | }
70 |
71 | func runTestScript(t *testing.T, file string) {
72 | server := newTestServer()
73 | content, err := ioutil.ReadFile(file)
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 |
78 | clientConn, serverConn := net.Pipe()
79 | defer clientConn.Close()
80 | go server.ServeCodec(NewCodec(serverConn), 0)
81 | readbuf := bufio.NewReader(clientConn)
82 | for _, line := range strings.Split(string(content), "\n") {
83 | line = strings.TrimSpace(line)
84 | switch {
85 | case len(line) == 0 || strings.HasPrefix(line, "//"):
86 | // skip comments, blank lines
87 | continue
88 | case strings.HasPrefix(line, "--> "):
89 | t.Log(line)
90 | // write to connection
91 | clientConn.SetWriteDeadline(time.Now().Add(5 * time.Second))
92 | if _, err := io.WriteString(clientConn, line[4:]+"\n"); err != nil {
93 | t.Fatalf("write error: %v", err)
94 | }
95 | case strings.HasPrefix(line, "<-- "):
96 | t.Log(line)
97 | want := line[4:]
98 | // read line from connection and compare text
99 | clientConn.SetReadDeadline(time.Now().Add(5 * time.Second))
100 | sent, err := readbuf.ReadString('\n')
101 | if err != nil {
102 | t.Fatalf("read error: %v", err)
103 | }
104 | sent = strings.TrimRight(sent, "\r\n")
105 | if sent != want {
106 | t.Errorf("wrong line from server\ngot: %s\nwant: %s", sent, want)
107 | }
108 | default:
109 | panic("invalid line in test script: " + line)
110 | }
111 | }
112 | }
113 |
114 | // This test checks that responses are delivered for very short-lived connections that
115 | // only carry a single request.
116 | func TestServerShortLivedConn(t *testing.T) {
117 | server := newTestServer()
118 | defer server.Stop()
119 |
120 | listener, err := net.Listen("tcp", "127.0.0.1:0")
121 | if err != nil {
122 | t.Fatal("can't listen:", err)
123 | }
124 | defer listener.Close()
125 | go server.ServeListener(listener)
126 |
127 | var (
128 | request = `{"jsonrpc":"2.0","id":1,"method":"rpc_modules"}` + "\n"
129 | wantResp = `{"jsonrpc":"2.0","id":1,"result":{"nftest":"1.0","rpc":"1.0","test":"1.0"}}` + "\n"
130 | deadline = time.Now().Add(10 * time.Second)
131 | )
132 | for i := 0; i < 20; i++ {
133 | conn, err := net.Dial("tcp", listener.Addr().String())
134 | if err != nil {
135 | t.Fatal("can't dial:", err)
136 | }
137 | defer conn.Close()
138 | conn.SetDeadline(deadline)
139 | // Write the request, then half-close the connection so the server stops reading.
140 | conn.Write([]byte(request))
141 | conn.(*net.TCPConn).CloseWrite()
142 | // Now try to get the response.
143 | buf := make([]byte, 2000)
144 | n, err := conn.Read(buf)
145 | if err != nil {
146 | t.Fatal("read error:", err)
147 | }
148 | if !bytes.Equal(buf[:n], []byte(wantResp)) {
149 | t.Fatalf("wrong response: %s", buf[:n])
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/service.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "errors"
22 | "fmt"
23 | "reflect"
24 | "runtime"
25 | "strings"
26 | "sync"
27 | "unicode"
28 |
29 | "github.com/ethereum/go-ethereum/log"
30 | )
31 |
32 | var (
33 | contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
34 | errorType = reflect.TypeOf((*error)(nil)).Elem()
35 | subscriptionType = reflect.TypeOf(Subscription{})
36 | stringType = reflect.TypeOf("")
37 | )
38 |
39 | type serviceRegistry struct {
40 | mu sync.Mutex
41 | services map[string]service
42 | }
43 |
44 | // service represents a registered object.
45 | type service struct {
46 | name string // name for service
47 | callbacks map[string]*callback // registered handlers
48 | subscriptions map[string]*callback // available subscriptions/notifications
49 | }
50 |
51 | // callback is a method callback which was registered in the server
52 | type callback struct {
53 | fn reflect.Value // the function
54 | rcvr reflect.Value // receiver object of method, set if fn is method
55 | argTypes []reflect.Type // input argument types
56 | isVariadic bool // true if the function type's final input parameter is a "..." parameter
57 | hasCtx bool // method's first argument is a context (not included in argTypes)
58 | errPos int // err return idx, of -1 when method cannot return error
59 | isSubscribe bool // true if this is a subscription callback
60 | }
61 |
62 | func (r *serviceRegistry) registerName(name string, rcvr interface{}) error {
63 | rcvrVal := reflect.ValueOf(rcvr)
64 | if name == "" {
65 | return fmt.Errorf("no service name for type %s", rcvrVal.Type().String())
66 | }
67 | callbacks := suitableCallbacks(rcvrVal)
68 | if len(callbacks) == 0 {
69 | return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr)
70 | }
71 |
72 | r.mu.Lock()
73 | defer r.mu.Unlock()
74 | if r.services == nil {
75 | r.services = make(map[string]service)
76 | }
77 | svc, ok := r.services[name]
78 | if !ok {
79 | svc = service{
80 | name: name,
81 | callbacks: make(map[string]*callback),
82 | subscriptions: make(map[string]*callback),
83 | }
84 | r.services[name] = svc
85 | }
86 | for name, cb := range callbacks {
87 | if cb.isSubscribe {
88 | svc.subscriptions[name] = cb
89 | } else {
90 | svc.callbacks[name] = cb
91 | }
92 | }
93 | return nil
94 | }
95 |
96 | // callback returns the callback corresponding to the given RPC method name.
97 | func (r *serviceRegistry) callback(method string) *callback {
98 | elem := strings.SplitN(method, serviceMethodSeparator, 2)
99 | if len(elem) != 2 {
100 | return nil
101 | }
102 | r.mu.Lock()
103 | defer r.mu.Unlock()
104 | return r.services[elem[0]].callbacks[elem[1]]
105 | }
106 |
107 | // subscription returns a subscription callback in the given service.
108 | func (r *serviceRegistry) subscription(service, name string) *callback {
109 | r.mu.Lock()
110 | defer r.mu.Unlock()
111 | return r.services[service].subscriptions[name]
112 | }
113 |
114 | // suitableCallbacks iterates over the methods of the given type. It determines if a method
115 | // satisfies the criteria for a RPC callback or a subscription callback and adds it to the
116 | // collection of callbacks. See server documentation for a summary of these criteria.
117 | func suitableCallbacks(receiver reflect.Value) map[string]*callback {
118 | typ := receiver.Type()
119 | callbacks := make(map[string]*callback)
120 | for m := 0; m < typ.NumMethod(); m++ {
121 | method := typ.Method(m)
122 | if method.PkgPath != "" {
123 | continue // method not exported
124 | }
125 | cb := newCallback(receiver, method.Func)
126 | if cb == nil {
127 | continue // function invalid
128 | }
129 | name := formatName(method.Name)
130 | callbacks[name] = cb
131 | }
132 | return callbacks
133 | }
134 |
135 | // newCallback turns fn (a function) into a callback object. It returns nil if the function
136 | // is unsuitable as an RPC callback.
137 | func newCallback(receiver, fn reflect.Value) *callback {
138 | fntype := fn.Type()
139 | c := &callback{fn: fn, rcvr: receiver, errPos: -1, isSubscribe: isPubSub(fntype), isVariadic: fntype.IsVariadic()}
140 | // Determine parameter types. They must all be exported or builtin types.
141 | c.makeArgTypes()
142 |
143 | // Verify return types. The function must return at most one error
144 | // and/or one other non-error value.
145 | outs := make([]reflect.Type, fntype.NumOut())
146 | for i := 0; i < fntype.NumOut(); i++ {
147 | outs[i] = fntype.Out(i)
148 | }
149 | if len(outs) > 2 {
150 | return nil
151 | }
152 | // If an error is returned, it must be the last returned value.
153 | switch {
154 | case len(outs) == 1 && isErrorType(outs[0]):
155 | c.errPos = 0
156 | case len(outs) == 2:
157 | if isErrorType(outs[0]) || !isErrorType(outs[1]) {
158 | return nil
159 | }
160 | c.errPos = 1
161 | }
162 | return c
163 | }
164 |
165 | // makeArgTypes composes the argTypes list.
166 | func (c *callback) makeArgTypes() {
167 | fntype := c.fn.Type()
168 | // Skip receiver and context.Context parameter (if present).
169 | firstArg := 0
170 | if c.rcvr.IsValid() {
171 | firstArg++
172 | }
173 | if fntype.NumIn() > firstArg && fntype.In(firstArg) == contextType {
174 | c.hasCtx = true
175 | firstArg++
176 | }
177 | // Add all remaining parameters.
178 | c.argTypes = make([]reflect.Type, fntype.NumIn()-firstArg)
179 | for i := firstArg; i < fntype.NumIn(); i++ {
180 | c.argTypes[i-firstArg] = fntype.In(i)
181 | }
182 | }
183 |
184 | // call invokes the callback.
185 | func (c *callback) call(ctx context.Context, method string, args []reflect.Value) (res interface{}, errRes error) {
186 | // Create the argument slice.
187 | fullargs := make([]reflect.Value, 0, 2+len(args))
188 | if c.rcvr.IsValid() {
189 | fullargs = append(fullargs, c.rcvr)
190 | }
191 | if c.hasCtx {
192 | fullargs = append(fullargs, reflect.ValueOf(ctx))
193 | }
194 | fullargs = append(fullargs, args...)
195 |
196 | // Catch panic while running the callback.
197 | defer func() {
198 | if err := recover(); err != nil {
199 | const size = 64 << 10
200 | buf := make([]byte, size)
201 | buf = buf[:runtime.Stack(buf, false)]
202 | log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf))
203 | errRes = errors.New("method handler crashed")
204 | }
205 | }()
206 | // Run the callback.
207 | results := c.fn.Call(fullargs)
208 | if len(results) == 0 {
209 | return nil, nil
210 | }
211 | if c.errPos >= 0 && !results[c.errPos].IsNil() {
212 | // Method has returned non-nil error value.
213 | err := results[c.errPos].Interface().(error)
214 | return reflect.Value{}, err
215 | }
216 | return results[0].Interface(), nil
217 | }
218 |
219 | // Is t context.Context or *context.Context?
220 | func isContextType(t reflect.Type) bool {
221 | for t.Kind() == reflect.Ptr {
222 | t = t.Elem()
223 | }
224 | return t == contextType
225 | }
226 |
227 | // Does t satisfy the error interface?
228 | func isErrorType(t reflect.Type) bool {
229 | for t.Kind() == reflect.Ptr {
230 | t = t.Elem()
231 | }
232 | return t.Implements(errorType)
233 | }
234 |
235 | // Is t Subscription or *Subscription?
236 | func isSubscriptionType(t reflect.Type) bool {
237 | for t.Kind() == reflect.Ptr {
238 | t = t.Elem()
239 | }
240 | return t == subscriptionType
241 | }
242 |
243 | // isPubSub tests whether the given method has as as first argument a context.Context and
244 | // returns the pair (Subscription, error).
245 | func isPubSub(methodType reflect.Type) bool {
246 | // numIn(0) is the receiver type
247 | if methodType.NumIn() < 2 || methodType.NumOut() != 2 {
248 | return false
249 | }
250 | return isContextType(methodType.In(1)) &&
251 | isSubscriptionType(methodType.Out(0)) &&
252 | isErrorType(methodType.Out(1))
253 | }
254 |
255 | // formatName converts to first character of name to lowercase.
256 | func formatName(name string) string {
257 | ret := []rune(name)
258 | if len(ret) > 0 {
259 | ret[0] = unicode.ToLower(ret[0])
260 | }
261 | return string(ret)
262 | }
263 |
--------------------------------------------------------------------------------
/stdio.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "errors"
22 | "io"
23 | "net"
24 | "os"
25 | "time"
26 | )
27 |
28 | // DialStdIO creates a client on stdin/stdout.
29 | func DialStdIO(ctx context.Context) (*Client, error) {
30 | return DialIO(ctx, os.Stdin, os.Stdout)
31 | }
32 |
33 | // DialIO creates a client which uses the given IO channels
34 | func DialIO(ctx context.Context, in io.Reader, out io.Writer) (*Client, error) {
35 | return newClient(ctx, func(_ context.Context) (ServerCodec, error) {
36 | return NewCodec(stdioConn{
37 | in: in,
38 | out: out,
39 | }), nil
40 | })
41 | }
42 |
43 | type stdioConn struct {
44 | in io.Reader
45 | out io.Writer
46 | }
47 |
48 | func (io stdioConn) Read(b []byte) (n int, err error) {
49 | return io.in.Read(b)
50 | }
51 |
52 | func (io stdioConn) Write(b []byte) (n int, err error) {
53 | return io.out.Write(b)
54 | }
55 |
56 | func (io stdioConn) Close() error {
57 | return nil
58 | }
59 |
60 | func (io stdioConn) RemoteAddr() string {
61 | return "/dev/stdin"
62 | }
63 |
64 | func (io stdioConn) SetWriteDeadline(t time.Time) error {
65 | return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
66 | }
67 |
--------------------------------------------------------------------------------
/subscription.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "bufio"
21 | "container/list"
22 | "context"
23 | crand "crypto/rand"
24 | "encoding/binary"
25 | "encoding/hex"
26 | "encoding/json"
27 | "errors"
28 | "math/rand"
29 | "reflect"
30 | "strings"
31 | "sync"
32 | "time"
33 | )
34 |
35 | var (
36 | // ErrNotificationsUnsupported is returned when the connection doesn't support notifications
37 | ErrNotificationsUnsupported = errors.New("notifications not supported")
38 | // ErrNotificationNotFound is returned when the notification for the given id is not found
39 | ErrSubscriptionNotFound = errors.New("subscription not found")
40 | )
41 |
42 | var globalGen = randomIDGenerator()
43 |
44 | // ID defines a pseudo random number that is used to identify RPC subscriptions.
45 | type ID string
46 |
47 | // NewID returns a new, random ID.
48 | func NewID() ID {
49 | return globalGen()
50 | }
51 |
52 | // randomIDGenerator returns a function generates a random IDs.
53 | func randomIDGenerator() func() ID {
54 | seed, err := binary.ReadVarint(bufio.NewReader(crand.Reader))
55 | if err != nil {
56 | seed = int64(time.Now().Nanosecond())
57 | }
58 | var (
59 | mu sync.Mutex
60 | rng = rand.New(rand.NewSource(seed))
61 | )
62 | return func() ID {
63 | mu.Lock()
64 | defer mu.Unlock()
65 | id := make([]byte, 16)
66 | rng.Read(id)
67 | return encodeID(id)
68 | }
69 | }
70 |
71 | func encodeID(b []byte) ID {
72 | id := hex.EncodeToString(b)
73 | id = strings.TrimLeft(id, "0")
74 | if id == "" {
75 | id = "0" // ID's are RPC quantities, no leading zero's and 0 is 0x0.
76 | }
77 | return ID("0x" + id)
78 | }
79 |
80 | type notifierKey struct{}
81 |
82 | // NotifierFromContext returns the Notifier value stored in ctx, if any.
83 | func NotifierFromContext(ctx context.Context) (*Notifier, bool) {
84 | n, ok := ctx.Value(notifierKey{}).(*Notifier)
85 | return n, ok
86 | }
87 |
88 | // Notifier is tied to a RPC connection that supports subscriptions.
89 | // Server callbacks use the notifier to send notifications.
90 | type Notifier struct {
91 | h *handler
92 | namespace string
93 |
94 | mu sync.Mutex
95 | sub *Subscription
96 | buffer []json.RawMessage
97 | callReturned bool
98 | activated bool
99 | }
100 |
101 | // CreateSubscription returns a new subscription that is coupled to the
102 | // RPC connection. By default subscriptions are inactive and notifications
103 | // are dropped until the subscription is marked as active. This is done
104 | // by the RPC server after the subscription ID is send to the client.
105 | func (n *Notifier) CreateSubscription() *Subscription {
106 | n.mu.Lock()
107 | defer n.mu.Unlock()
108 |
109 | if n.sub != nil {
110 | panic("can't create multiple subscriptions with Notifier")
111 | } else if n.callReturned {
112 | panic("can't create subscription after subscribe call has returned")
113 | }
114 | n.sub = &Subscription{ID: n.h.idgen(), namespace: n.namespace, err: make(chan error, 1)}
115 | return n.sub
116 | }
117 |
118 | // Notify sends a notification to the client with the given data as payload.
119 | // If an error occurs the RPC connection is closed and the error is returned.
120 | func (n *Notifier) Notify(id ID, data interface{}) error {
121 | enc, err := json.Marshal(data)
122 | if err != nil {
123 | return err
124 | }
125 |
126 | n.mu.Lock()
127 | defer n.mu.Unlock()
128 |
129 | if n.sub == nil {
130 | panic("can't Notify before subscription is created")
131 | } else if n.sub.ID != id {
132 | panic("Notify with wrong ID")
133 | }
134 | if n.activated {
135 | return n.send(n.sub, enc)
136 | }
137 | n.buffer = append(n.buffer, enc)
138 | return nil
139 | }
140 |
141 | // Closed returns a channel that is closed when the RPC connection is closed.
142 | // Deprecated: use subscription error channel
143 | func (n *Notifier) Closed() <-chan interface{} {
144 | return n.h.conn.closed()
145 | }
146 |
147 | // takeSubscription returns the subscription (if one has been created). No subscription can
148 | // be created after this call.
149 | func (n *Notifier) takeSubscription() *Subscription {
150 | n.mu.Lock()
151 | defer n.mu.Unlock()
152 | n.callReturned = true
153 | return n.sub
154 | }
155 |
156 | // activate is called after the subscription ID was sent to client. Notifications are
157 | // buffered before activation. This prevents notifications being sent to the client before
158 | // the subscription ID is sent to the client.
159 | func (n *Notifier) activate() error {
160 | n.mu.Lock()
161 | defer n.mu.Unlock()
162 |
163 | for _, data := range n.buffer {
164 | if err := n.send(n.sub, data); err != nil {
165 | return err
166 | }
167 | }
168 | n.activated = true
169 | return nil
170 | }
171 |
172 | func (n *Notifier) send(sub *Subscription, data json.RawMessage) error {
173 | params, _ := json.Marshal(&subscriptionResult{ID: string(sub.ID), Result: data})
174 | ctx := context.Background()
175 | return n.h.conn.writeJSON(ctx, &jsonrpcMessage{
176 | Version: vsn,
177 | Method: n.namespace + notificationMethodSuffix,
178 | Params: params,
179 | })
180 | }
181 |
182 | // A Subscription is created by a notifier and tied to that notifier. The client can use
183 | // this subscription to wait for an unsubscribe request for the client, see Err().
184 | type Subscription struct {
185 | ID ID
186 | namespace string
187 | err chan error // closed on unsubscribe
188 | }
189 |
190 | // Err returns a channel that is closed when the client send an unsubscribe request.
191 | func (s *Subscription) Err() <-chan error {
192 | return s.err
193 | }
194 |
195 | // MarshalJSON marshals a subscription as its ID.
196 | func (s *Subscription) MarshalJSON() ([]byte, error) {
197 | return json.Marshal(s.ID)
198 | }
199 |
200 | // ClientSubscription is a subscription established through the Client's Subscribe or
201 | // EthSubscribe methods.
202 | type ClientSubscription struct {
203 | client *Client
204 | etype reflect.Type
205 | channel reflect.Value
206 | namespace string
207 | subid string
208 | in chan json.RawMessage
209 |
210 | quitOnce sync.Once // ensures quit is closed once
211 | quit chan struct{} // quit is closed when the subscription exits
212 | errOnce sync.Once // ensures err is closed once
213 | err chan error
214 | }
215 |
216 | func newClientSubscription(c *Client, namespace string, channel reflect.Value) *ClientSubscription {
217 | sub := &ClientSubscription{
218 | client: c,
219 | namespace: namespace,
220 | etype: channel.Type().Elem(),
221 | channel: channel,
222 | quit: make(chan struct{}),
223 | err: make(chan error, 1),
224 | in: make(chan json.RawMessage),
225 | }
226 | return sub
227 | }
228 |
229 | // Err returns the subscription error channel. The intended use of Err is to schedule
230 | // resubscription when the client connection is closed unexpectedly.
231 | //
232 | // The error channel receives a value when the subscription has ended due
233 | // to an error. The received error is nil if Close has been called
234 | // on the underlying client and no other error has occurred.
235 | //
236 | // The error channel is closed when Unsubscribe is called on the subscription.
237 | func (sub *ClientSubscription) Err() <-chan error {
238 | return sub.err
239 | }
240 |
241 | // Unsubscribe unsubscribes the notification and closes the error channel.
242 | // It can safely be called more than once.
243 | func (sub *ClientSubscription) Unsubscribe() {
244 | sub.quitWithError(true, nil)
245 | sub.errOnce.Do(func() { close(sub.err) })
246 | }
247 |
248 | func (sub *ClientSubscription) quitWithError(unsubscribeServer bool, err error) {
249 | sub.quitOnce.Do(func() {
250 | // The dispatch loop won't be able to execute the unsubscribe call
251 | // if it is blocked on deliver. Close sub.quit first because it
252 | // unblocks deliver.
253 | close(sub.quit)
254 | if unsubscribeServer {
255 | sub.requestUnsubscribe()
256 | }
257 | if err != nil {
258 | if err == ErrClientQuit {
259 | err = nil // Adhere to subscription semantics.
260 | }
261 | sub.err <- err
262 | }
263 | })
264 | }
265 |
266 | func (sub *ClientSubscription) deliver(result json.RawMessage) (ok bool) {
267 | select {
268 | case sub.in <- result:
269 | return true
270 | case <-sub.quit:
271 | return false
272 | }
273 | }
274 |
275 | func (sub *ClientSubscription) start() {
276 | sub.quitWithError(sub.forward())
277 | }
278 |
279 | func (sub *ClientSubscription) forward() (unsubscribeServer bool, err error) {
280 | cases := []reflect.SelectCase{
281 | {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(sub.quit)},
282 | {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(sub.in)},
283 | {Dir: reflect.SelectSend, Chan: sub.channel},
284 | }
285 | buffer := list.New()
286 | defer buffer.Init()
287 | for {
288 | var chosen int
289 | var recv reflect.Value
290 | if buffer.Len() == 0 {
291 | // Idle, omit send case.
292 | chosen, recv, _ = reflect.Select(cases[:2])
293 | } else {
294 | // Non-empty buffer, send the first queued item.
295 | cases[2].Send = reflect.ValueOf(buffer.Front().Value)
296 | chosen, recv, _ = reflect.Select(cases)
297 | }
298 |
299 | switch chosen {
300 | case 0: // <-sub.quit
301 | return false, nil
302 | case 1: // <-sub.in
303 | val, err := sub.unmarshal(recv.Interface().(json.RawMessage))
304 | if err != nil {
305 | return true, err
306 | }
307 | if buffer.Len() == maxClientSubscriptionBuffer {
308 | return true, ErrSubscriptionQueueOverflow
309 | }
310 | buffer.PushBack(val)
311 | case 2: // sub.channel<-
312 | cases[2].Send = reflect.Value{} // Don't hold onto the value.
313 | buffer.Remove(buffer.Front())
314 | }
315 | }
316 | }
317 |
318 | func (sub *ClientSubscription) unmarshal(result json.RawMessage) (interface{}, error) {
319 | val := reflect.New(sub.etype)
320 | err := json.Unmarshal(result, val.Interface())
321 | return val.Elem().Interface(), err
322 | }
323 |
324 | func (sub *ClientSubscription) requestUnsubscribe() error {
325 | var result interface{}
326 | return sub.client.Call(&result, sub.namespace+unsubscribeMethodSuffix, sub.subid)
327 | }
328 |
--------------------------------------------------------------------------------
/subscription_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "encoding/json"
21 | "fmt"
22 | "net"
23 | "strings"
24 | "testing"
25 | "time"
26 | )
27 |
28 | func TestNewID(t *testing.T) {
29 | hexchars := "0123456789ABCDEFabcdef"
30 | for i := 0; i < 100; i++ {
31 | id := string(NewID())
32 | if !strings.HasPrefix(id, "0x") {
33 | t.Fatalf("invalid ID prefix, want '0x...', got %s", id)
34 | }
35 |
36 | id = id[2:]
37 | if len(id) == 0 || len(id) > 32 {
38 | t.Fatalf("invalid ID length, want len(id) > 0 && len(id) <= 32), got %d", len(id))
39 | }
40 |
41 | for i := 0; i < len(id); i++ {
42 | if strings.IndexByte(hexchars, id[i]) == -1 {
43 | t.Fatalf("unexpected byte, want any valid hex char, got %c", id[i])
44 | }
45 | }
46 | }
47 | }
48 |
49 | func TestSubscriptions(t *testing.T) {
50 | var (
51 | namespaces = []string{"eth", "shh", "bzz"}
52 | service = ¬ificationTestService{}
53 | subCount = len(namespaces)
54 | notificationCount = 3
55 |
56 | server = NewServer()
57 | clientConn, serverConn = net.Pipe()
58 | out = json.NewEncoder(clientConn)
59 | in = json.NewDecoder(clientConn)
60 | successes = make(chan subConfirmation)
61 | notifications = make(chan subscriptionResult)
62 | errors = make(chan error, subCount*notificationCount+1)
63 | )
64 |
65 | // setup and start server
66 | for _, namespace := range namespaces {
67 | if err := server.RegisterName(namespace, service); err != nil {
68 | t.Fatalf("unable to register test service %v", err)
69 | }
70 | }
71 | go server.ServeCodec(NewCodec(serverConn), 0)
72 | defer server.Stop()
73 |
74 | // wait for message and write them to the given channels
75 | go waitForMessages(in, successes, notifications, errors)
76 |
77 | // create subscriptions one by one
78 | for i, namespace := range namespaces {
79 | request := map[string]interface{}{
80 | "id": i,
81 | "method": fmt.Sprintf("%s_subscribe", namespace),
82 | "version": "2.0",
83 | "params": []interface{}{"someSubscription", notificationCount, i},
84 | }
85 | if err := out.Encode(&request); err != nil {
86 | t.Fatalf("Could not create subscription: %v", err)
87 | }
88 | }
89 |
90 | timeout := time.After(30 * time.Second)
91 | subids := make(map[string]string, subCount)
92 | count := make(map[string]int, subCount)
93 | allReceived := func() bool {
94 | done := len(count) == subCount
95 | for _, c := range count {
96 | if c < notificationCount {
97 | done = false
98 | }
99 | }
100 | return done
101 | }
102 | for !allReceived() {
103 | select {
104 | case confirmation := <-successes: // subscription created
105 | subids[namespaces[confirmation.reqid]] = string(confirmation.subid)
106 | case notification := <-notifications:
107 | count[notification.ID]++
108 | case err := <-errors:
109 | t.Fatal(err)
110 | case <-timeout:
111 | for _, namespace := range namespaces {
112 | subid, found := subids[namespace]
113 | if !found {
114 | t.Errorf("subscription for %q not created", namespace)
115 | continue
116 | }
117 | if count, found := count[subid]; !found || count < notificationCount {
118 | t.Errorf("didn't receive all notifications (%d<%d) in time for namespace %q", count, notificationCount, namespace)
119 | }
120 | }
121 | t.Fatal("timed out")
122 | }
123 | }
124 | }
125 |
126 | // This test checks that unsubscribing works.
127 | func TestServerUnsubscribe(t *testing.T) {
128 | p1, p2 := net.Pipe()
129 | defer p2.Close()
130 |
131 | // Start the server.
132 | server := newTestServer()
133 | service := ¬ificationTestService{unsubscribed: make(chan string, 1)}
134 | server.RegisterName("nftest2", service)
135 | go server.ServeCodec(NewCodec(p1), 0)
136 |
137 | // Subscribe.
138 | p2.SetDeadline(time.Now().Add(10 * time.Second))
139 | p2.Write([]byte(`{"jsonrpc":"2.0","id":1,"method":"nftest2_subscribe","params":["someSubscription",0,10]}`))
140 |
141 | // Handle received messages.
142 | var (
143 | resps = make(chan subConfirmation)
144 | notifications = make(chan subscriptionResult)
145 | errors = make(chan error, 1)
146 | )
147 | go waitForMessages(json.NewDecoder(p2), resps, notifications, errors)
148 |
149 | // Receive the subscription ID.
150 | var sub subConfirmation
151 | select {
152 | case sub = <-resps:
153 | case err := <-errors:
154 | t.Fatal(err)
155 | }
156 |
157 | // Unsubscribe and check that it is handled on the server side.
158 | p2.Write([]byte(`{"jsonrpc":"2.0","method":"nftest2_unsubscribe","params":["` + sub.subid + `"]}`))
159 | for {
160 | select {
161 | case id := <-service.unsubscribed:
162 | if id != string(sub.subid) {
163 | t.Errorf("wrong subscription ID unsubscribed")
164 | }
165 | return
166 | case err := <-errors:
167 | t.Fatal(err)
168 | case <-notifications:
169 | // drop notifications
170 | }
171 | }
172 | }
173 |
174 | type subConfirmation struct {
175 | reqid int
176 | subid ID
177 | }
178 |
179 | // waitForMessages reads RPC messages from 'in' and dispatches them into the given channels.
180 | // It stops if there is an error.
181 | func waitForMessages(in *json.Decoder, successes chan subConfirmation, notifications chan subscriptionResult, errors chan error) {
182 | for {
183 | resp, notification, err := readAndValidateMessage(in)
184 | if err != nil {
185 | errors <- err
186 | return
187 | } else if resp != nil {
188 | successes <- *resp
189 | } else {
190 | notifications <- *notification
191 | }
192 | }
193 | }
194 |
195 | func readAndValidateMessage(in *json.Decoder) (*subConfirmation, *subscriptionResult, error) {
196 | var msg jsonrpcMessage
197 | if err := in.Decode(&msg); err != nil {
198 | return nil, nil, fmt.Errorf("decode error: %v", err)
199 | }
200 | switch {
201 | case msg.isNotification():
202 | var res subscriptionResult
203 | if err := json.Unmarshal(msg.Params, &res); err != nil {
204 | return nil, nil, fmt.Errorf("invalid subscription result: %v", err)
205 | }
206 | return nil, &res, nil
207 | case msg.isResponse():
208 | var c subConfirmation
209 | if msg.Error != nil {
210 | return nil, nil, msg.Error
211 | } else if err := json.Unmarshal(msg.Result, &c.subid); err != nil {
212 | return nil, nil, fmt.Errorf("invalid response: %v", err)
213 | } else {
214 | json.Unmarshal(msg.ID, &c.reqid)
215 | return &c, nil, nil
216 | }
217 | default:
218 | return nil, nil, fmt.Errorf("unrecognized message: %v", msg)
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/testdata/invalid-badid.js:
--------------------------------------------------------------------------------
1 | // This test checks processing of messages with invalid ID.
2 |
3 | --> {"id":[],"method":"test_foo"}
4 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}
5 |
6 | --> {"id":{},"method":"test_foo"}
7 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}
8 |
--------------------------------------------------------------------------------
/testdata/invalid-batch.js:
--------------------------------------------------------------------------------
1 | // This test checks the behavior of batches with invalid elements.
2 | // Empty batches are not allowed. Batches may contain junk.
3 |
4 | --> []
5 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"empty batch"}}
6 |
7 | --> [null, null]
8 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}
9 |
10 | --> [null, {"jsonrpc":"2.0","id": 3,"method":"test_echo","params":["x",3]}, null]
11 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}
12 |
13 | --> [1]
14 | <-- [{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}]
15 |
16 | --> [1,2,3]
17 | <-- [{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}]
18 |
19 | --> [{"jsonrpc":"2.0","id":1,"method":"test_echo","params":["foo",1]},55,{"jsonrpc":"2.0","id":2,"method":"unknown_method"},{"foo":"bar"}]
20 | <-- [{"jsonrpc":"2.0","id":1,"result":{"String":"foo","Int":1,"Args":null}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"the method unknown_method does not exist/is not available"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}]
21 |
--------------------------------------------------------------------------------
/testdata/invalid-idonly.js:
--------------------------------------------------------------------------------
1 | // This test checks processing of messages that contain just the ID and nothing else.
2 |
3 | --> {"id":1}
4 | <-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
5 |
6 | --> {"jsonrpc":"2.0","id":1}
7 | <-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
8 |
--------------------------------------------------------------------------------
/testdata/invalid-nonobj.js:
--------------------------------------------------------------------------------
1 | // This test checks behavior for invalid requests.
2 |
3 | --> 1
4 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}
5 |
6 | --> null
7 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}
8 |
--------------------------------------------------------------------------------
/testdata/invalid-syntax.json:
--------------------------------------------------------------------------------
1 | // This test checks that an error is written for invalid JSON requests.
2 |
3 | --> 'f
4 | <-- {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"invalid character '\\'' looking for beginning of value"}}
5 |
6 |
--------------------------------------------------------------------------------
/testdata/reqresp-batch.js:
--------------------------------------------------------------------------------
1 | // There is no response for all-notification batches.
2 |
3 | --> [{"jsonrpc":"2.0","method":"test_echo","params":["x",99]}]
4 |
5 | // This test checks regular batch calls.
6 |
7 | --> [{"jsonrpc":"2.0","id":2,"method":"test_echo","params":[]}, {"jsonrpc":"2.0","id": 3,"method":"test_echo","params":["x",3]}]
8 | <-- [{"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 0"}},{"jsonrpc":"2.0","id":3,"result":{"String":"x","Int":3,"Args":null}}]
9 |
--------------------------------------------------------------------------------
/testdata/reqresp-echo.js:
--------------------------------------------------------------------------------
1 | // This test calls the test_echo method.
2 |
3 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": []}
4 | <-- {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 0"}}
5 |
6 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x"]}
7 | <-- {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 1"}}
8 |
9 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x", 3]}
10 | <-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":null}}
11 |
12 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x", 3, {"S": "foo"}]}
13 | <-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":{"S":"foo"}}}
14 |
15 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_echoWithCtx", "params": ["x", 3, {"S": "foo"}]}
16 | <-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":{"S":"foo"}}}
17 |
--------------------------------------------------------------------------------
/testdata/reqresp-namedparam.js:
--------------------------------------------------------------------------------
1 | // This test checks that an error response is sent for calls
2 | // with named parameters.
3 |
4 | --> {"jsonrpc":"2.0","method":"test_echo","params":{"int":23},"id":3}
5 | <-- {"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"non-array args"}}
6 |
--------------------------------------------------------------------------------
/testdata/reqresp-noargsrets.js:
--------------------------------------------------------------------------------
1 | // This test calls the test_noArgsRets method.
2 |
3 | --> {"jsonrpc": "2.0", "id": "foo", "method": "test_noArgsRets", "params": []}
4 | <-- {"jsonrpc":"2.0","id":"foo","result":null}
5 |
--------------------------------------------------------------------------------
/testdata/reqresp-nomethod.js:
--------------------------------------------------------------------------------
1 | // This test calls a method that doesn't exist.
2 |
3 | --> {"jsonrpc": "2.0", "id": 2, "method": "invalid_method", "params": [2, 3]}
4 | <-- {"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"the method invalid_method does not exist/is not available"}}
5 |
--------------------------------------------------------------------------------
/testdata/reqresp-noparam.js:
--------------------------------------------------------------------------------
1 | // This test checks that calls with no parameters work.
2 |
3 | --> {"jsonrpc":"2.0","method":"test_noArgsRets","id":3}
4 | <-- {"jsonrpc":"2.0","id":3,"result":null}
5 |
--------------------------------------------------------------------------------
/testdata/reqresp-paramsnull.js:
--------------------------------------------------------------------------------
1 | // This test checks that calls with "params":null work.
2 |
3 | --> {"jsonrpc":"2.0","method":"test_noArgsRets","params":null,"id":3}
4 | <-- {"jsonrpc":"2.0","id":3,"result":null}
5 |
--------------------------------------------------------------------------------
/testdata/reqresp-paramsvariadic.js:
--------------------------------------------------------------------------------
1 | // This test checks that calls with variadic param(s) work.
2 |
3 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_variadicArgs", "params": ["x", 3]}
4 | <-- {"jsonrpc":"2.0","id":2,"result":[{"String":"x","Int":3,"Args":null}]}
5 |
6 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_variadicArgs", "params": ["x", 3, {"S": "foo"}]}
7 | <-- {"jsonrpc":"2.0","id":2,"result":[{"String":"x","Int":3,"Args":{"S":"foo"}}]}
8 |
9 | --> {"jsonrpc": "2.0", "id": 2, "method": "test_variadicArgs", "params": ["x", 3, {"S": "foo"}, {"S": "bar"}]}
10 | <-- {"jsonrpc":"2.0","id":2,"result":[{"String":"x","Int":3,"Args":{"S":"foo"}},{"String":"x","Int":3,"Args":{"S":"bar"}}]}
--------------------------------------------------------------------------------
/testdata/revcall.js:
--------------------------------------------------------------------------------
1 | // This test checks reverse calls.
2 |
3 | --> {"jsonrpc":"2.0","id":2,"method":"test_callMeBack","params":["foo",[1]]}
4 | <-- {"jsonrpc":"2.0","id":1,"method":"foo","params":[1]}
5 | --> {"jsonrpc":"2.0","id":1,"result":"my result"}
6 | <-- {"jsonrpc":"2.0","id":2,"result":"my result"}
7 |
--------------------------------------------------------------------------------
/testdata/revcall2.js:
--------------------------------------------------------------------------------
1 | // This test checks reverse calls.
2 |
3 | --> {"jsonrpc":"2.0","id":2,"method":"test_callMeBackLater","params":["foo",[1]]}
4 | <-- {"jsonrpc":"2.0","id":2,"result":null}
5 | <-- {"jsonrpc":"2.0","id":1,"method":"foo","params":[1]}
6 | --> {"jsonrpc":"2.0","id":1,"result":"my result"}
7 |
8 |
--------------------------------------------------------------------------------
/testdata/subscription.js:
--------------------------------------------------------------------------------
1 | // This test checks basic subscription support.
2 |
3 | --> {"jsonrpc":"2.0","id":1,"method":"nftest_subscribe","params":["someSubscription",5,1]}
4 | <-- {"jsonrpc":"2.0","id":1,"result":"0x1"}
5 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":1}}
6 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":2}}
7 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":3}}
8 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":4}}
9 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":5}}
10 |
11 | --> {"jsonrpc":"2.0","id":1,"method":"nftest_subscribe","params":["echoVariadicArgs"]}
12 | <-- {"jsonrpc":"2.0","id":1,"result":"0x2"}
13 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x2","result":"hi world"}}
14 |
15 | --> {"jsonrpc":"2.0","id":1,"method":"nftest_subscribe","params":["echoVariadicArgs", "david"]}
16 | <-- {"jsonrpc":"2.0","id":1,"result":"0x3"}
17 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x3","result":"hi world"}}
18 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x3","result":"hi david"}}
19 |
20 | --> {"jsonrpc":"2.0","id":1,"method":"nftest_subscribe","params":["echoVariadicArgs", "jimmy", "lily"]}
21 | <-- {"jsonrpc":"2.0","id":1,"result":"0x4"}
22 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x4","result":"hi world"}}
23 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x4","result":"hi jimmy"}}
24 | <-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x4","result":"hi lily"}}
25 |
26 | --> {"jsonrpc":"2.0","id":2,"method":"nftest_echo","params":[11]}
27 | <-- {"jsonrpc":"2.0","id":2,"result":11}
28 |
--------------------------------------------------------------------------------
/testservice_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "encoding/binary"
22 | "errors"
23 | "sync"
24 | "time"
25 | )
26 |
27 | func newTestServer() *Server {
28 | server := NewServer()
29 | server.idgen = sequentialIDGenerator()
30 | if err := server.RegisterName("test", new(testService)); err != nil {
31 | panic(err)
32 | }
33 | if err := server.RegisterName("nftest", new(notificationTestService)); err != nil {
34 | panic(err)
35 | }
36 | return server
37 | }
38 |
39 | func sequentialIDGenerator() func() ID {
40 | var (
41 | mu sync.Mutex
42 | counter uint64
43 | )
44 | return func() ID {
45 | mu.Lock()
46 | defer mu.Unlock()
47 | counter++
48 | id := make([]byte, 8)
49 | binary.BigEndian.PutUint64(id, counter)
50 | return encodeID(id)
51 | }
52 | }
53 |
54 | type testService struct{}
55 |
56 | type echoArgs struct {
57 | S string
58 | }
59 |
60 | type echoResult struct {
61 | String string
62 | Int int
63 | Args *echoArgs
64 | }
65 |
66 | func (s *testService) NoArgsRets() {}
67 |
68 | func (s *testService) Echo(str string, i int, args *echoArgs) echoResult {
69 | return echoResult{str, i, args}
70 | }
71 |
72 | func (s *testService) EchoWithCtx(ctx context.Context, str string, i int, args *echoArgs) echoResult {
73 | return echoResult{str, i, args}
74 | }
75 |
76 | func (s *testService) Sleep(ctx context.Context, duration time.Duration) {
77 | time.Sleep(duration)
78 | }
79 |
80 | func (s *testService) Block(ctx context.Context) error {
81 | <-ctx.Done()
82 | return errors.New("context canceled in testservice_block")
83 | }
84 |
85 | func (s *testService) Rets() (string, error) {
86 | return "", nil
87 | }
88 |
89 | //lint:ignore ST1008 returns error first on purpose.
90 | func (s *testService) InvalidRets1() (error, string) {
91 | return nil, ""
92 | }
93 |
94 | func (s *testService) InvalidRets2() (string, string) {
95 | return "", ""
96 | }
97 |
98 | func (s *testService) InvalidRets3() (string, string, error) {
99 | return "", "", nil
100 | }
101 |
102 | func (s *testService) CallMeBack(ctx context.Context, method string, args []interface{}) (interface{}, error) {
103 | c, ok := ClientFromContext(ctx)
104 | if !ok {
105 | return nil, errors.New("no client")
106 | }
107 | var result interface{}
108 | err := c.Call(&result, method, args...)
109 | return result, err
110 | }
111 |
112 | func (s *testService) CallMeBackLater(ctx context.Context, method string, args []interface{}) error {
113 | c, ok := ClientFromContext(ctx)
114 | if !ok {
115 | return errors.New("no client")
116 | }
117 | go func() {
118 | <-ctx.Done()
119 | var result interface{}
120 | c.Call(&result, method, args...)
121 | }()
122 | return nil
123 | }
124 |
125 | func (s *testService) Subscription(ctx context.Context) (*Subscription, error) {
126 | return nil, nil
127 | }
128 |
129 | func (s *testService) VariadicArgs(str string, i int, args ...*echoArgs) []echoResult {
130 | res := make([]echoResult, 0, 1)
131 |
132 | if len(args) == 0 {
133 | res = append(res, echoResult{str, i, nil})
134 | return res
135 | }
136 |
137 | for _, arg := range args {
138 | res = append(res, echoResult{str, i, arg})
139 | }
140 |
141 | return res
142 | }
143 |
144 | type notificationTestService struct {
145 | unsubscribed chan string
146 | gotHangSubscriptionReq chan struct{}
147 | unblockHangSubscription chan struct{}
148 | }
149 |
150 | func (s *notificationTestService) Echo(i int) int {
151 | return i
152 | }
153 |
154 | func (s *notificationTestService) Unsubscribe(subid string) {
155 | if s.unsubscribed != nil {
156 | s.unsubscribed <- subid
157 | }
158 | }
159 |
160 | func (s *notificationTestService) SomeSubscription(ctx context.Context, n, val int) (*Subscription, error) {
161 | notifier, supported := NotifierFromContext(ctx)
162 | if !supported {
163 | return nil, ErrNotificationsUnsupported
164 | }
165 |
166 | // By explicitly creating an subscription we make sure that the subscription id is send
167 | // back to the client before the first subscription.Notify is called. Otherwise the
168 | // events might be send before the response for the *_subscribe method.
169 | subscription := notifier.CreateSubscription()
170 | go func() {
171 | for i := 0; i < n; i++ {
172 | if err := notifier.Notify(subscription.ID, val+i); err != nil {
173 | return
174 | }
175 | }
176 | select {
177 | case <-notifier.Closed():
178 | case <-subscription.Err():
179 | }
180 | if s.unsubscribed != nil {
181 | s.unsubscribed <- string(subscription.ID)
182 | }
183 | }()
184 | return subscription, nil
185 | }
186 |
187 | // HangSubscription blocks on s.unblockHangSubscription before sending anything.
188 | func (s *notificationTestService) HangSubscription(ctx context.Context, val int) (*Subscription, error) {
189 | notifier, supported := NotifierFromContext(ctx)
190 | if !supported {
191 | return nil, ErrNotificationsUnsupported
192 | }
193 | s.gotHangSubscriptionReq <- struct{}{}
194 | <-s.unblockHangSubscription
195 | subscription := notifier.CreateSubscription()
196 |
197 | go func() {
198 | notifier.Notify(subscription.ID, val)
199 | }()
200 | return subscription, nil
201 | }
202 |
203 | // EchoVariadicArgs say hello to name(s) passed in as variadic arg(s)
204 | func (s *notificationTestService) EchoVariadicArgs(ctx context.Context, names ...string) (*Subscription, error) {
205 | notifier, supported := NotifierFromContext(ctx)
206 | if !supported {
207 | return nil, ErrNotificationsUnsupported
208 | }
209 |
210 | subscription := notifier.CreateSubscription()
211 | go func() {
212 | echos := append([]string{"world"}, names...)
213 | for _, e := range echos {
214 | if err := notifier.Notify(subscription.ID, "hi "+e); err != nil {
215 | return
216 | }
217 | }
218 |
219 | select {
220 | case <-notifier.Closed():
221 | case <-subscription.Err():
222 | }
223 | if s.unsubscribed != nil {
224 | s.unsubscribed <- string(subscription.ID)
225 | }
226 | }()
227 | return subscription, nil
228 | }
229 |
--------------------------------------------------------------------------------
/types.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "encoding/json"
22 | "fmt"
23 | "math"
24 | "strconv"
25 | "strings"
26 |
27 | "github.com/ethereum/go-ethereum/common"
28 | "github.com/ethereum/go-ethereum/common/hexutil"
29 | )
30 |
31 | // API describes the set of methods offered over the RPC interface
32 | type API struct {
33 | Namespace string // namespace under which the rpc methods of Service are exposed
34 | Version string // api version for DApp's
35 | Service interface{} // receiver instance which holds the methods
36 | Public bool // indication if the methods must be considered safe for public use
37 | }
38 |
39 | // Error wraps RPC errors, which contain an error code in addition to the message.
40 | type Error interface {
41 | Error() string // returns the message
42 | ErrorCode() int // returns the code
43 | }
44 |
45 | // ServerCodec implements reading, parsing and writing RPC messages for the server side of
46 | // a RPC session. Implementations must be go-routine safe since the codec can be called in
47 | // multiple go-routines concurrently.
48 | type ServerCodec interface {
49 | readBatch() (msgs []*jsonrpcMessage, isBatch bool, err error)
50 | close()
51 | jsonWriter
52 | }
53 |
54 | type ServerCodecWithContext struct {
55 | ServerCodec
56 | ctx context.Context
57 | }
58 |
59 | // jsonWriter can write JSON messages to its underlying connection.
60 | // Implementations must be safe for concurrent use.
61 | type jsonWriter interface {
62 | writeJSON(context.Context, interface{}) error
63 | // Closed returns a channel which is closed when the connection is closed.
64 | closed() <-chan interface{}
65 | // RemoteAddr returns the peer address of the connection.
66 | remoteAddr() string
67 | }
68 |
69 | type BlockNumber int64
70 |
71 | const (
72 | SafeBlockNumber = BlockNumber(-4)
73 | FinalizedBlockNumber = BlockNumber(-3)
74 | PendingBlockNumber = BlockNumber(-2)
75 | LatestBlockNumber = BlockNumber(-1)
76 | EarliestBlockNumber = BlockNumber(0)
77 | )
78 |
79 | // UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports:
80 | // - "latest", "earliest" or "pending" as string arguments
81 | // - the block number
82 | // Returned errors:
83 | // - an invalid block number error when the given argument isn't a known strings
84 | // - an out of range error when the given block number is either too little or too large
85 | func (bn *BlockNumber) UnmarshalJSON(data []byte) error {
86 | input := strings.TrimSpace(string(data))
87 | if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
88 | input = input[1 : len(input)-1]
89 | }
90 |
91 | switch input {
92 | case "earliest":
93 | *bn = EarliestBlockNumber
94 | return nil
95 | case "latest":
96 | *bn = LatestBlockNumber
97 | return nil
98 | case "pending":
99 | *bn = PendingBlockNumber
100 | return nil
101 | case "finalized":
102 | *bn = FinalizedBlockNumber
103 | return nil
104 | case "safe":
105 | *bn = SafeBlockNumber
106 | return nil
107 | }
108 |
109 | blckNum, err := hexutil.DecodeUint64(input)
110 | if err != nil {
111 | return err
112 | }
113 | if blckNum > math.MaxInt64 {
114 | return fmt.Errorf("block number larger than int64")
115 | }
116 | *bn = BlockNumber(blckNum)
117 | return nil
118 | }
119 |
120 | // MarshalText implements encoding.TextMarshaler. It marshals:
121 | // - "latest", "earliest" or "pending" as strings
122 | // - other numbers as hex
123 | func (bn BlockNumber) MarshalText() ([]byte, error) {
124 | switch bn {
125 | case EarliestBlockNumber:
126 | return []byte("earliest"), nil
127 | case LatestBlockNumber:
128 | return []byte("latest"), nil
129 | case PendingBlockNumber:
130 | return []byte("pending"), nil
131 | case FinalizedBlockNumber:
132 | return []byte("finalized"), nil
133 | case SafeBlockNumber:
134 | return []byte("safe"), nil
135 | default:
136 | return hexutil.Uint64(bn).MarshalText()
137 | }
138 | }
139 |
140 | func (bn BlockNumber) Int64() int64 {
141 | return (int64)(bn)
142 | }
143 |
144 | type BlockNumberOrHash struct {
145 | BlockNumber *BlockNumber `json:"blockNumber,omitempty"`
146 | BlockHash *common.Hash `json:"blockHash,omitempty"`
147 | RequireCanonical bool `json:"requireCanonical,omitempty"`
148 | }
149 |
150 | func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error {
151 | type erased BlockNumberOrHash
152 | e := erased{}
153 | err := json.Unmarshal(data, &e)
154 | if err == nil {
155 | if e.BlockNumber != nil && e.BlockHash != nil {
156 | return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other")
157 | }
158 | bnh.BlockNumber = e.BlockNumber
159 | bnh.BlockHash = e.BlockHash
160 | bnh.RequireCanonical = e.RequireCanonical
161 | return nil
162 | }
163 | var input string
164 | err = json.Unmarshal(data, &input)
165 | if err != nil {
166 | return err
167 | }
168 | switch input {
169 | case "earliest":
170 | bn := EarliestBlockNumber
171 | bnh.BlockNumber = &bn
172 | return nil
173 | case "latest":
174 | bn := LatestBlockNumber
175 | bnh.BlockNumber = &bn
176 | return nil
177 | case "pending":
178 | bn := PendingBlockNumber
179 | bnh.BlockNumber = &bn
180 | return nil
181 | case "finalized":
182 | bn := FinalizedBlockNumber
183 | bnh.BlockNumber = &bn
184 | return nil
185 | case "safe":
186 | bn := SafeBlockNumber
187 | bnh.BlockNumber = &bn
188 | return nil
189 | default:
190 | if len(input) == 66 {
191 | hash := common.Hash{}
192 | err := hash.UnmarshalText([]byte(input))
193 | if err != nil {
194 | return err
195 | }
196 | bnh.BlockHash = &hash
197 | return nil
198 | } else {
199 | blckNum, err := hexutil.DecodeUint64(input)
200 | if err != nil {
201 | return err
202 | }
203 | if blckNum > math.MaxInt64 {
204 | return fmt.Errorf("blocknumber too high")
205 | }
206 | bn := BlockNumber(blckNum)
207 | bnh.BlockNumber = &bn
208 | return nil
209 | }
210 | }
211 | }
212 |
213 | func (bnh *BlockNumberOrHash) Number() (BlockNumber, bool) {
214 | if bnh.BlockNumber != nil {
215 | return *bnh.BlockNumber, true
216 | }
217 | return BlockNumber(0), false
218 | }
219 |
220 | func (bnh *BlockNumberOrHash) String() string {
221 | if bnh.BlockNumber != nil {
222 | return strconv.Itoa(int(*bnh.BlockNumber))
223 | }
224 | if bnh.BlockHash != nil {
225 | return bnh.BlockHash.String()
226 | }
227 | return "nil"
228 | }
229 |
230 | func (bnh *BlockNumberOrHash) Hash() (common.Hash, bool) {
231 | if bnh.BlockHash != nil {
232 | return *bnh.BlockHash, true
233 | }
234 | return common.Hash{}, false
235 | }
236 |
237 | func BlockNumberOrHashWithNumber(blockNr BlockNumber) BlockNumberOrHash {
238 | return BlockNumberOrHash{
239 | BlockNumber: &blockNr,
240 | BlockHash: nil,
241 | RequireCanonical: false,
242 | }
243 | }
244 |
245 | func BlockNumberOrHashWithHash(hash common.Hash, canonical bool) BlockNumberOrHash {
246 | return BlockNumberOrHash{
247 | BlockNumber: nil,
248 | BlockHash: &hash,
249 | RequireCanonical: canonical,
250 | }
251 | }
252 |
253 | // DecimalOrHex unmarshals a non-negative decimal or hex parameter into a uint64.
254 | type DecimalOrHex uint64
255 |
256 | // UnmarshalJSON implements json.Unmarshaler.
257 | func (dh *DecimalOrHex) UnmarshalJSON(data []byte) error {
258 | input := strings.TrimSpace(string(data))
259 | if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
260 | input = input[1 : len(input)-1]
261 | }
262 |
263 | value, err := strconv.ParseUint(input, 10, 64)
264 | if err != nil {
265 | value, err = hexutil.DecodeUint64(input)
266 | }
267 | if err != nil {
268 | return err
269 | }
270 | *dh = DecimalOrHex(value)
271 | return nil
272 | }
273 |
--------------------------------------------------------------------------------
/types_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "encoding/json"
21 | "testing"
22 |
23 | "github.com/ethereum/go-ethereum/common"
24 | "github.com/ethereum/go-ethereum/common/math"
25 | )
26 |
27 | func TestBlockNumberJSONUnmarshal(t *testing.T) {
28 | tests := []struct {
29 | input string
30 | mustFail bool
31 | expected BlockNumber
32 | }{
33 | 0: {`"0x"`, true, BlockNumber(0)},
34 | 1: {`"0x0"`, false, BlockNumber(0)},
35 | 2: {`"0X1"`, false, BlockNumber(1)},
36 | 3: {`"0x00"`, true, BlockNumber(0)},
37 | 4: {`"0x01"`, true, BlockNumber(0)},
38 | 5: {`"0x1"`, false, BlockNumber(1)},
39 | 6: {`"0x12"`, false, BlockNumber(18)},
40 | 7: {`"0x7fffffffffffffff"`, false, BlockNumber(math.MaxInt64)},
41 | 8: {`"0x8000000000000000"`, true, BlockNumber(0)},
42 | 9: {"0", true, BlockNumber(0)},
43 | 10: {`"ff"`, true, BlockNumber(0)},
44 | 11: {`"pending"`, false, PendingBlockNumber},
45 | 12: {`"latest"`, false, LatestBlockNumber},
46 | 13: {`"earliest"`, false, EarliestBlockNumber},
47 | 14: {`someString`, true, BlockNumber(0)},
48 | 15: {`""`, true, BlockNumber(0)},
49 | 16: {``, true, BlockNumber(0)},
50 | }
51 |
52 | for i, test := range tests {
53 | var num BlockNumber
54 | err := json.Unmarshal([]byte(test.input), &num)
55 | if test.mustFail && err == nil {
56 | t.Errorf("Test %d should fail", i)
57 | continue
58 | }
59 | if !test.mustFail && err != nil {
60 | t.Errorf("Test %d should pass but got err: %v", i, err)
61 | continue
62 | }
63 | if num != test.expected {
64 | t.Errorf("Test %d got unexpected value, want %d, got %d", i, test.expected, num)
65 | }
66 | }
67 | }
68 |
69 | func TestBlockNumberOrHash_UnmarshalJSON(t *testing.T) {
70 | tests := []struct {
71 | input string
72 | mustFail bool
73 | expected BlockNumberOrHash
74 | }{
75 | 0: {`"0x"`, true, BlockNumberOrHash{}},
76 | 1: {`"0x0"`, false, BlockNumberOrHashWithNumber(0)},
77 | 2: {`"0X1"`, false, BlockNumberOrHashWithNumber(1)},
78 | 3: {`"0x00"`, true, BlockNumberOrHash{}},
79 | 4: {`"0x01"`, true, BlockNumberOrHash{}},
80 | 5: {`"0x1"`, false, BlockNumberOrHashWithNumber(1)},
81 | 6: {`"0x12"`, false, BlockNumberOrHashWithNumber(18)},
82 | 7: {`"0x7fffffffffffffff"`, false, BlockNumberOrHashWithNumber(math.MaxInt64)},
83 | 8: {`"0x8000000000000000"`, true, BlockNumberOrHash{}},
84 | 9: {"0", true, BlockNumberOrHash{}},
85 | 10: {`"ff"`, true, BlockNumberOrHash{}},
86 | 11: {`"pending"`, false, BlockNumberOrHashWithNumber(PendingBlockNumber)},
87 | 12: {`"latest"`, false, BlockNumberOrHashWithNumber(LatestBlockNumber)},
88 | 13: {`"earliest"`, false, BlockNumberOrHashWithNumber(EarliestBlockNumber)},
89 | 14: {`someString`, true, BlockNumberOrHash{}},
90 | 15: {`""`, true, BlockNumberOrHash{}},
91 | 16: {``, true, BlockNumberOrHash{}},
92 | 17: {`"0x0000000000000000000000000000000000000000000000000000000000000000"`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)},
93 | 18: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)},
94 | 19: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","requireCanonical":false}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)},
95 | 20: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","requireCanonical":true}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), true)},
96 | 21: {`{"blockNumber":"0x1"}`, false, BlockNumberOrHashWithNumber(1)},
97 | 22: {`{"blockNumber":"pending"}`, false, BlockNumberOrHashWithNumber(PendingBlockNumber)},
98 | 23: {`{"blockNumber":"latest"}`, false, BlockNumberOrHashWithNumber(LatestBlockNumber)},
99 | 24: {`{"blockNumber":"earliest"}`, false, BlockNumberOrHashWithNumber(EarliestBlockNumber)},
100 | 25: {`{"blockNumber":"0x1", "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, true, BlockNumberOrHash{}},
101 | }
102 |
103 | for i, test := range tests {
104 | var bnh BlockNumberOrHash
105 | err := json.Unmarshal([]byte(test.input), &bnh)
106 | if test.mustFail && err == nil {
107 | t.Errorf("Test %d should fail", i)
108 | continue
109 | }
110 | if !test.mustFail && err != nil {
111 | t.Errorf("Test %d should pass but got err: %v", i, err)
112 | continue
113 | }
114 | hash, hashOk := bnh.Hash()
115 | expectedHash, expectedHashOk := test.expected.Hash()
116 | num, numOk := bnh.Number()
117 | expectedNum, expectedNumOk := test.expected.Number()
118 | if bnh.RequireCanonical != test.expected.RequireCanonical ||
119 | hash != expectedHash || hashOk != expectedHashOk ||
120 | num != expectedNum || numOk != expectedNumOk {
121 | t.Errorf("Test %d got unexpected value, want %v, got %v", i, test.expected, bnh)
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "reflect"
7 |
8 | "github.com/pkg/errors"
9 | )
10 |
11 | // IsRPCJSONError returns true if err is rpc error
12 | func IsRPCJSONError(err error) bool {
13 | if err == nil {
14 | return false
15 | }
16 |
17 | t := reflect.TypeOf(errors.Cause(err)).String()
18 | return t == "*rpc.jsonError" || t == "rpc.jsonError" || t == "utils.RpcError" || t == "*utils.RpcError"
19 | }
20 |
21 | // PrettyJSON json marshal value and pretty with indent
22 | func PrettyJSON(value interface{}) string {
23 | j, e := json.Marshal(value)
24 | if e != nil {
25 | panic(e)
26 | }
27 | var str bytes.Buffer
28 | _ = json.Indent(&str, j, "", " ")
29 | return str.String()
30 | }
31 |
--------------------------------------------------------------------------------
/websocket.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "encoding/base64"
22 | "fmt"
23 | "net/http"
24 | "net/url"
25 | "os"
26 | "strings"
27 | "sync"
28 | "time"
29 |
30 | mapset "github.com/deckarep/golang-set"
31 | "github.com/ethereum/go-ethereum/log"
32 | "github.com/gorilla/websocket"
33 | )
34 |
35 | const (
36 | wsReadBuffer = 1024
37 | wsWriteBuffer = 1024
38 | wsPingInterval = 60 * time.Second
39 | wsPingWriteTimeout = 5 * time.Second
40 | wsPongTimeout = 30 * time.Second
41 | wsMessageSizeLimit = 15 * 1024 * 1024
42 | )
43 |
44 | var wsBufferPool = new(sync.Pool)
45 |
46 | type WebsocketOption struct {
47 | WsPingInterval time.Duration
48 | }
49 |
50 | // WebsocketHandler returns a handler that serves JSON-RPC to WebSocket connections.
51 | //
52 | // allowedOrigins should be a comma-separated list of allowed origin URLs.
53 | // To allow connections with any origin, pass "*".
54 | func (s *Server) WebsocketHandler(allowedOrigins []string, option ...WebsocketOption) http.Handler {
55 | var upgrader = websocket.Upgrader{
56 | ReadBufferSize: wsReadBuffer,
57 | WriteBufferSize: wsWriteBuffer,
58 | WriteBufferPool: wsBufferPool,
59 | CheckOrigin: wsHandshakeValidator(allowedOrigins),
60 | }
61 | _option := get1stWsOption(option)
62 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63 | conn, err := upgrader.Upgrade(w, r, nil)
64 | if err != nil {
65 | log.Debug("WebSocket upgrade failed", "err", err)
66 | return
67 | }
68 | codec := newWebsocketCodec(conn, r.Host, r.Header, _option.WsPingInterval)
69 | codecWithCtx := ServerCodecWithContext{codec, r.Context()}
70 | s.ServeCodec(codecWithCtx, 0)
71 | })
72 | }
73 |
74 | // wsHandshakeValidator returns a handler that verifies the origin during the
75 | // websocket upgrade process. When a '*' is specified as an allowed origins all
76 | // connections are accepted.
77 | func wsHandshakeValidator(allowedOrigins []string) func(*http.Request) bool {
78 | origins := mapset.NewSet()
79 | allowAllOrigins := false
80 |
81 | for _, origin := range allowedOrigins {
82 | if origin == "*" {
83 | allowAllOrigins = true
84 | }
85 | if origin != "" {
86 | origins.Add(strings.ToLower(origin))
87 | }
88 | }
89 | // allow localhost if no allowedOrigins are specified.
90 | if len(origins.ToSlice()) == 0 {
91 | origins.Add("http://localhost")
92 | if hostname, err := os.Hostname(); err == nil {
93 | origins.Add("http://" + strings.ToLower(hostname))
94 | }
95 | }
96 | log.Debug(fmt.Sprintf("Allowed origin(s) for WS RPC interface %v", origins.ToSlice()))
97 |
98 | f := func(req *http.Request) bool {
99 | // Skip origin verification if no Origin header is present. The origin check
100 | // is supposed to protect against browser based attacks. Browsers always set
101 | // Origin. Non-browser software can put anything in origin and checking it doesn't
102 | // provide additional security.
103 | if _, ok := req.Header["Origin"]; !ok {
104 | return true
105 | }
106 | // Verify origin against whitelist.
107 | origin := strings.ToLower(req.Header.Get("Origin"))
108 | if allowAllOrigins || origins.Contains(origin) {
109 | return true
110 | }
111 | log.Warn("Rejected WebSocket connection", "origin", origin)
112 | return false
113 | }
114 |
115 | return f
116 | }
117 |
118 | type wsHandshakeError struct {
119 | err error
120 | status string
121 | }
122 |
123 | func (e wsHandshakeError) Error() string {
124 | s := e.err.Error()
125 | if e.status != "" {
126 | s += " (HTTP status " + e.status + ")"
127 | }
128 | return s
129 | }
130 |
131 | // DialWebsocketWithDialer creates a new RPC client that communicates with a JSON-RPC server
132 | // that is listening on the given endpoint using the provided dialer.
133 | func DialWebsocketWithDialer(ctx context.Context, endpoint, origin string, dialer websocket.Dialer, option ...WebsocketOption) (*Client, error) {
134 | endpoint, header, err := wsClientHeaders(endpoint, origin)
135 | if err != nil {
136 | return nil, err
137 | }
138 | _option := get1stWsOption(option)
139 | return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
140 | conn, resp, err := dialer.DialContext(ctx, endpoint, header)
141 | if err != nil {
142 | hErr := wsHandshakeError{err: err}
143 | if resp != nil {
144 | hErr.status = resp.Status
145 | }
146 | return nil, hErr
147 | }
148 | return newWebsocketCodec(conn, endpoint, header, _option.WsPingInterval), nil
149 | })
150 | }
151 |
152 | // DialWebsocket creates a new RPC client that communicates with a JSON-RPC server
153 | // that is listening on the given endpoint.
154 | //
155 | // The context is used for the initial connection establishment. It does not
156 | // affect subsequent interactions with the client.
157 | func DialWebsocket(ctx context.Context, endpoint, origin string, option ...WebsocketOption) (*Client, error) {
158 | dialer := websocket.Dialer{
159 | ReadBufferSize: wsReadBuffer,
160 | WriteBufferSize: wsWriteBuffer,
161 | WriteBufferPool: wsBufferPool,
162 | }
163 | return DialWebsocketWithDialer(ctx, endpoint, origin, dialer, option...)
164 | }
165 |
166 | func wsClientHeaders(endpoint, origin string) (string, http.Header, error) {
167 | endpointURL, err := url.Parse(endpoint)
168 | if err != nil {
169 | return endpoint, nil, err
170 | }
171 | header := make(http.Header)
172 | if origin != "" {
173 | header.Add("origin", origin)
174 | }
175 | if endpointURL.User != nil {
176 | b64auth := base64.StdEncoding.EncodeToString([]byte(endpointURL.User.String()))
177 | header.Add("authorization", "Basic "+b64auth)
178 | endpointURL.User = nil
179 | }
180 | return endpointURL.String(), header, nil
181 | }
182 |
183 | type websocketCodec struct {
184 | *jsonCodec
185 | conn *websocket.Conn
186 | info PeerInfo
187 |
188 | wg sync.WaitGroup
189 | pingReset chan struct{}
190 | }
191 |
192 | func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header, wsPingInterval time.Duration) ServerCodec {
193 | conn.SetReadLimit(wsMessageSizeLimit)
194 | conn.SetPongHandler(func(appData string) error {
195 | conn.SetReadDeadline(time.Time{})
196 | return nil
197 | })
198 | wc := &websocketCodec{
199 | jsonCodec: NewFuncCodec(conn, conn.WriteJSON, conn.ReadJSON).(*jsonCodec),
200 | conn: conn,
201 | pingReset: make(chan struct{}, 1),
202 | info: PeerInfo{
203 | Transport: "ws",
204 | RemoteAddr: conn.RemoteAddr().String(),
205 | },
206 | }
207 | // Fill in connection details.
208 | wc.info.HTTP.Host = host
209 | wc.info.HTTP.Origin = req.Get("Origin")
210 | wc.info.HTTP.UserAgent = req.Get("User-Agent")
211 | // Start pinger.
212 | wc.wg.Add(1)
213 | go wc.pingLoop(wsPingInterval)
214 | return wc
215 | }
216 |
217 | func (wc *websocketCodec) close() {
218 | wc.jsonCodec.close()
219 | wc.wg.Wait()
220 | }
221 |
222 | func (wc *websocketCodec) peerInfo() PeerInfo {
223 | return wc.info
224 | }
225 |
226 | func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}) error {
227 | err := wc.jsonCodec.writeJSON(ctx, v)
228 | if err == nil {
229 | // Notify pingLoop to delay the next idle ping.
230 | select {
231 | case wc.pingReset <- struct{}{}:
232 | default:
233 | }
234 | }
235 | return err
236 | }
237 |
238 | // pingLoop sends periodic ping frames when the connection is idle.
239 | func (wc *websocketCodec) pingLoop(wsPingInterval time.Duration) {
240 | var timer = time.NewTimer(wsPingInterval)
241 | defer wc.wg.Done()
242 | defer timer.Stop()
243 |
244 | for {
245 | select {
246 | case <-wc.closed():
247 | return
248 | case <-wc.pingReset:
249 | if !timer.Stop() {
250 | <-timer.C
251 | }
252 | timer.Reset(wsPingInterval)
253 | case <-timer.C:
254 | wc.jsonCodec.encMu.Lock()
255 | wc.conn.SetWriteDeadline(time.Now().Add(wsPingWriteTimeout))
256 | wc.conn.WriteMessage(websocket.PingMessage, nil)
257 | wc.conn.SetReadDeadline(time.Now().Add(wsPongTimeout))
258 | wc.jsonCodec.encMu.Unlock()
259 | timer.Reset(wsPingInterval)
260 | }
261 | }
262 | }
263 |
264 | func get1stWsOption(option []WebsocketOption) WebsocketOption {
265 | _option := WebsocketOption{WsPingInterval: wsPingInterval}
266 | if len(option) > 0 && _option.WsPingInterval > 0 {
267 | _option = option[0]
268 | }
269 | return _option
270 | }
271 |
--------------------------------------------------------------------------------
/websocket_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The go-ethereum Authors
2 | // This file is part of the go-ethereum library.
3 | //
4 | // The go-ethereum library is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Lesser General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // The go-ethereum library is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Lesser General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Lesser General Public License
15 | // along with the go-ethereum library. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "context"
21 | "net"
22 | "net/http"
23 | "net/http/httptest"
24 | "reflect"
25 | "strings"
26 | "testing"
27 | "time"
28 |
29 | "github.com/gorilla/websocket"
30 | )
31 |
32 | func TestWebsocketClientHeaders(t *testing.T) {
33 | t.Parallel()
34 |
35 | endpoint, header, err := wsClientHeaders("wss://testuser:test-PASS_01@example.com:1234", "https://example.com")
36 | if err != nil {
37 | t.Fatalf("wsGetConfig failed: %s", err)
38 | }
39 | if endpoint != "wss://example.com:1234" {
40 | t.Fatal("User should have been stripped from the URL")
41 | }
42 | if header.Get("authorization") != "Basic dGVzdHVzZXI6dGVzdC1QQVNTXzAx" {
43 | t.Fatal("Basic auth header is incorrect")
44 | }
45 | if header.Get("origin") != "https://example.com" {
46 | t.Fatal("Origin not set")
47 | }
48 | }
49 |
50 | // This test checks that the server rejects connections from disallowed origins.
51 | func TestWebsocketOriginCheck(t *testing.T) {
52 | t.Parallel()
53 |
54 | var (
55 | srv = newTestServer()
56 | httpsrv = httptest.NewServer(srv.WebsocketHandler([]string{"http://example.com"}))
57 | wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
58 | )
59 | defer srv.Stop()
60 | defer httpsrv.Close()
61 |
62 | client, err := DialWebsocket(context.Background(), wsURL, "http://ekzample.com")
63 | if err == nil {
64 | client.Close()
65 | t.Fatal("no error for wrong origin")
66 | }
67 | wantErr := wsHandshakeError{websocket.ErrBadHandshake, "403 Forbidden"}
68 | if !reflect.DeepEqual(err, wantErr) {
69 | t.Fatalf("wrong error for wrong origin: %q", err)
70 | }
71 |
72 | // Connections without origin header should work.
73 | client, err = DialWebsocket(context.Background(), wsURL, "")
74 | if err != nil {
75 | t.Fatal("error for empty origin")
76 | }
77 | client.Close()
78 | }
79 |
80 | // This test checks whether calls exceeding the request size limit are rejected.
81 | func TestWebsocketLargeCall(t *testing.T) {
82 | t.Parallel()
83 |
84 | var (
85 | srv = newTestServer()
86 | httpsrv = httptest.NewServer(srv.WebsocketHandler([]string{"*"}))
87 | wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
88 | )
89 | defer srv.Stop()
90 | defer httpsrv.Close()
91 |
92 | client, err := DialWebsocket(context.Background(), wsURL, "")
93 | if err != nil {
94 | t.Fatalf("can't dial: %v", err)
95 | }
96 | defer client.Close()
97 |
98 | // This call sends slightly less than the limit and should work.
99 | var result echoResult
100 | arg := strings.Repeat("x", maxRequestContentLength-200)
101 | if err := client.Call(&result, "test_echo", arg, 1); err != nil {
102 | t.Fatalf("valid call didn't work: %v", err)
103 | }
104 | if result.String != arg {
105 | t.Fatal("wrong string echoed")
106 | }
107 |
108 | // This call sends twice the allowed size and shouldn't work.
109 | arg = strings.Repeat("x", maxRequestContentLength*2)
110 | err = client.Call(&result, "test_echo", arg)
111 | if err == nil {
112 | t.Fatal("no error for too large call")
113 | }
114 | }
115 |
116 | // This test checks that client handles WebSocket ping frames correctly.
117 | func TestClientWebsocketPing(t *testing.T) {
118 | t.Parallel()
119 |
120 | var (
121 | sendPing = make(chan struct{})
122 | server = wsPingTestServer(t, sendPing)
123 | ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
124 | )
125 | defer cancel()
126 | defer server.Shutdown(ctx)
127 |
128 | client, err := DialContext(ctx, "ws://"+server.Addr)
129 | if err != nil {
130 | t.Fatalf("client dial error: %v", err)
131 | }
132 | resultChan := make(chan int)
133 | sub, err := client.EthSubscribe(ctx, resultChan, "foo")
134 | if err != nil {
135 | t.Fatalf("client subscribe error: %v", err)
136 | }
137 |
138 | // Wait for the context's deadline to be reached before proceeding.
139 | // This is important for reproducing https://github.com/ethereum/go-ethereum/issues/19798
140 | <-ctx.Done()
141 | close(sendPing)
142 |
143 | // Wait for the subscription result.
144 | timeout := time.NewTimer(5 * time.Second)
145 | defer timeout.Stop()
146 | for {
147 | select {
148 | case err := <-sub.Err():
149 | t.Error("client subscription error:", err)
150 | case result := <-resultChan:
151 | t.Log("client got result:", result)
152 | return
153 | case <-timeout.C:
154 | t.Error("didn't get any result within the test timeout")
155 | return
156 | }
157 | }
158 | }
159 |
160 | // wsPingTestServer runs a WebSocket server which accepts a single subscription request.
161 | // When a value arrives on sendPing, the server sends a ping frame, waits for a matching
162 | // pong and finally delivers a single subscription result.
163 | func wsPingTestServer(t *testing.T, sendPing <-chan struct{}) *http.Server {
164 | var srv http.Server
165 | shutdown := make(chan struct{})
166 | srv.RegisterOnShutdown(func() {
167 | close(shutdown)
168 | })
169 | srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
170 | // Upgrade to WebSocket.
171 | upgrader := websocket.Upgrader{
172 | CheckOrigin: func(r *http.Request) bool { return true },
173 | }
174 | conn, err := upgrader.Upgrade(w, r, nil)
175 | if err != nil {
176 | t.Errorf("server WS upgrade error: %v", err)
177 | return
178 | }
179 | defer conn.Close()
180 |
181 | // Handle the connection.
182 | wsPingTestHandler(t, conn, shutdown, sendPing)
183 | })
184 |
185 | // Start the server.
186 | listener, err := net.Listen("tcp", "127.0.0.1:0")
187 | if err != nil {
188 | t.Fatal("can't listen:", err)
189 | }
190 | srv.Addr = listener.Addr().String()
191 | go srv.Serve(listener)
192 | return &srv
193 | }
194 |
195 | func wsPingTestHandler(t *testing.T, conn *websocket.Conn, shutdown, sendPing <-chan struct{}) {
196 | // Canned responses for the eth_subscribe call in TestClientWebsocketPing.
197 | const (
198 | subResp = `{"jsonrpc":"2.0","id":1,"result":"0x00"}`
199 | subNotify = `{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x00","result":1}}`
200 | )
201 |
202 | // Handle subscribe request.
203 | if _, _, err := conn.ReadMessage(); err != nil {
204 | t.Errorf("server read error: %v", err)
205 | return
206 | }
207 | if err := conn.WriteMessage(websocket.TextMessage, []byte(subResp)); err != nil {
208 | t.Errorf("server write error: %v", err)
209 | return
210 | }
211 |
212 | // Read from the connection to process control messages.
213 | var pongCh = make(chan string)
214 | conn.SetPongHandler(func(d string) error {
215 | t.Logf("server got pong: %q", d)
216 | pongCh <- d
217 | return nil
218 | })
219 | go func() {
220 | for {
221 | typ, msg, err := conn.ReadMessage()
222 | if err != nil {
223 | return
224 | }
225 | t.Logf("server got message (%d): %q", typ, msg)
226 | }
227 | }()
228 |
229 | // Write messages.
230 | var (
231 | wantPong string
232 | timer = time.NewTimer(0)
233 | )
234 | defer timer.Stop()
235 | <-timer.C
236 | for {
237 | select {
238 | case _, open := <-sendPing:
239 | if !open {
240 | sendPing = nil
241 | }
242 | t.Logf("server sending ping")
243 | conn.WriteMessage(websocket.PingMessage, []byte("ping"))
244 | wantPong = "ping"
245 | case data := <-pongCh:
246 | if wantPong == "" {
247 | t.Errorf("unexpected pong")
248 | } else if data != wantPong {
249 | t.Errorf("got pong with wrong data %q", data)
250 | }
251 | wantPong = ""
252 | timer.Reset(200 * time.Millisecond)
253 | case <-timer.C:
254 | t.Logf("server sending response")
255 | conn.WriteMessage(websocket.TextMessage, []byte(subNotify))
256 | case <-shutdown:
257 | conn.Close()
258 | return
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------