├── 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 | --------------------------------------------------------------------------------