manual geth v1.11.1 merge

This commit is contained in:
philip-morlier
2023-02-17 07:53:01 -08:00
729 changed files with 29880 additions and 19327 deletions
+39 -11
View File
@@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"net/url"
"os"
"reflect"
"strconv"
"sync/atomic"
@@ -100,7 +101,7 @@ type Client struct {
reqTimeout chan *requestOp // removes response IDs when call timeout expires
}
type reconnectFunc func(ctx context.Context) (ServerCodec, error)
type reconnectFunc func(context.Context) (ServerCodec, error)
type clientContextKey struct{}
@@ -154,14 +155,16 @@ func (op *requestOp) wait(ctx context.Context, c *Client) (*jsonrpcMessage, erro
//
// The currently supported URL schemes are "http", "https", "ws" and "wss". If rawurl is a
// file name with no URL scheme, a local socket connection is established using UNIX
// domain sockets on supported platforms and named pipes on Windows. If you want to
// configure transport options, use DialHTTP, DialWebsocket or DialIPC instead.
// domain sockets on supported platforms and named pipes on Windows.
//
// If you want to further configure the transport, use DialOptions instead of this
// function.
//
// For websocket connections, the origin is set to the local host name.
//
// The client reconnects automatically if the connection is lost.
// The client reconnects automatically when the connection is lost.
func Dial(rawurl string) (*Client, error) {
return DialContext(context.Background(), rawurl)
return DialOptions(context.Background(), rawurl)
}
// DialContext creates a new RPC client, just like Dial.
@@ -169,22 +172,46 @@ func Dial(rawurl string) (*Client, error) {
// The context is used to cancel or time out the initial connection establishment. It does
// not affect subsequent interactions with the client.
func DialContext(ctx context.Context, rawurl string) (*Client, error) {
return DialOptions(ctx, rawurl)
}
// DialOptions creates a new RPC client for the given URL. You can supply any of the
// pre-defined client options to configure the underlying transport.
//
// The context is used to cancel or time out the initial connection establishment. It does
// not affect subsequent interactions with the client.
//
// The client reconnects automatically when the connection is lost.
func DialOptions(ctx context.Context, rawurl string, options ...ClientOption) (*Client, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
cfg := new(clientConfig)
for _, opt := range options {
opt.applyOption(cfg)
}
var reconnect reconnectFunc
switch u.Scheme {
case "http", "https":
return DialHTTP(rawurl)
reconnect = newClientTransportHTTP(rawurl, cfg)
case "ws", "wss":
return DialWebsocket(ctx, rawurl, "")
rc, err := newClientTransportWS(rawurl, cfg)
if err != nil {
return nil, err
}
reconnect = rc
case "stdio":
return DialStdIO(ctx)
reconnect = newClientTransportIO(os.Stdin, os.Stdout)
case "":
return DialIPC(ctx, rawurl)
reconnect = newClientTransportIPC(rawurl)
default:
return nil, fmt.Errorf("no known transport for URL scheme %q", u.Scheme)
}
return newClient(ctx, reconnect)
}
// ClientFromContext retrieves the client from the context, if any. This can be used to perform
@@ -500,7 +527,7 @@ func (c *Client) write(ctx context.Context, msg interface{}, retry bool) error {
return err
}
}
err := c.writeConn.writeJSON(ctx, msg)
err := c.writeConn.writeJSON(ctx, msg, false)
if err != nil {
c.writeConn = nil
if !retry {
@@ -633,7 +660,8 @@ func (c *Client) read(codec ServerCodec) {
for {
msgs, batch, err := codec.readBatch()
if _, ok := err.(*json.SyntaxError); ok {
codec.writeJSON(context.Background(), errorMessage(&parseError{err.Error()}))
msg := errorMessage(&parseError{err.Error()})
codec.writeJSON(context.Background(), msg, true)
}
if err != nil {
c.readErr <- err
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"net/http"
"github.com/gorilla/websocket"
)
// ClientOption is a configuration option for the RPC client.
type ClientOption interface {
applyOption(*clientConfig)
}
type clientConfig struct {
httpClient *http.Client
httpHeaders http.Header
httpAuth HTTPAuth
wsDialer *websocket.Dialer
}
func (cfg *clientConfig) initHeaders() {
if cfg.httpHeaders == nil {
cfg.httpHeaders = make(http.Header)
}
}
func (cfg *clientConfig) setHeader(key, value string) {
cfg.initHeaders()
cfg.httpHeaders.Set(key, value)
}
type optionFunc func(*clientConfig)
func (fn optionFunc) applyOption(opt *clientConfig) {
fn(opt)
}
// WithWebsocketDialer configures the websocket.Dialer used by the RPC client.
func WithWebsocketDialer(dialer websocket.Dialer) ClientOption {
return optionFunc(func(cfg *clientConfig) {
cfg.wsDialer = &dialer
})
}
// WithHeader configures HTTP headers set by the RPC client. Headers set using this option
// will be used for both HTTP and WebSocket connections.
func WithHeader(key, value string) ClientOption {
return optionFunc(func(cfg *clientConfig) {
cfg.initHeaders()
cfg.httpHeaders.Set(key, value)
})
}
// WithHeaders configures HTTP headers set by the RPC client. Headers set using this
// option will be used for both HTTP and WebSocket connections.
func WithHeaders(headers http.Header) ClientOption {
return optionFunc(func(cfg *clientConfig) {
cfg.initHeaders()
for k, vs := range headers {
cfg.httpHeaders[k] = vs
}
})
}
// WithHTTPClient configures the http.Client used by the RPC client.
func WithHTTPClient(c *http.Client) ClientOption {
return optionFunc(func(cfg *clientConfig) {
cfg.httpClient = c
})
}
// WithHTTPAuth configures HTTP request authentication. The given provider will be called
// whenever a request is made. Note that only one authentication provider can be active at
// any time.
func WithHTTPAuth(a HTTPAuth) ClientOption {
if a == nil {
panic("nil auth")
}
return optionFunc(func(cfg *clientConfig) {
cfg.httpAuth = a
})
}
// A HTTPAuth function is called by the client whenever a HTTP request is sent.
// The function must be safe for concurrent use.
//
// Usually, HTTPAuth functions will call h.Set("authorization", "...") to add
// auth information to the request.
type HTTPAuth func(h http.Header) error
+25
View File
@@ -0,0 +1,25 @@
package rpc_test
import (
"context"
"net/http"
"time"
"github.com/ethereum/go-ethereum/rpc"
)
// This example configures a HTTP-based RPC client with two options - one setting the
// overall request timeout, the other adding a custom HTTP header to all requests.
func ExampleDialOptions() {
tokenHeader := rpc.WithHeader("x-token", "foo")
httpClient := rpc.WithHTTPClient(&http.Client{
Timeout: 10 * time.Second,
})
ctx := context.Background()
c, err := rpc.DialOptions(ctx, "http://rpc.example.com", httpClient, tokenHeader)
if err != nil {
panic(err)
}
c.Close()
}
+6 -2
View File
@@ -83,11 +83,15 @@ func TestClientErrorData(t *testing.T) {
}
// Check code.
// The method handler returns an error value which implements the rpc.Error
// interface, i.e. it has a custom error code. The server returns this error code.
expectedCode := testError{}.ErrorCode()
if e, ok := err.(Error); !ok {
t.Fatalf("client did not return rpc.Error, got %#v", e)
} else if e.ErrorCode() != (testError{}.ErrorCode()) {
t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), testError{}.ErrorCode())
} else if e.ErrorCode() != expectedCode {
t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), expectedCode)
}
// Check data.
if e, ok := err.(DataError); !ok {
t.Fatalf("client did not return rpc.DataError, got %#v", e)
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"net/http"
)
type mdHeaderKey struct{}
// NewContextWithHeaders wraps the given context, adding HTTP headers. These headers will
// be applied by Client when making a request using the returned context.
func NewContextWithHeaders(ctx context.Context, h http.Header) context.Context {
if len(h) == 0 {
// This check ensures the header map set in context will never be nil.
return ctx
}
var ctxh http.Header
prev, ok := ctx.Value(mdHeaderKey{}).(http.Header)
if ok {
ctxh = setHeaders(prev.Clone(), h)
} else {
ctxh = h.Clone()
}
return context.WithValue(ctx, mdHeaderKey{}, ctxh)
}
// headersFromContext is used to extract http.Header from context.
func headersFromContext(ctx context.Context) http.Header {
source, _ := ctx.Value(mdHeaderKey{}).(http.Header)
return source
}
// setHeaders sets all headers from src in dst.
func setHeaders(dst http.Header, src http.Header) http.Header {
for key, values := range src {
dst[http.CanonicalHeaderKey(key)] = values
}
return dst
}
+28 -29
View File
@@ -15,7 +15,6 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
/*
Package rpc implements bi-directional JSON-RPC 2.0 on multiple transports.
It provides access to the exported methods of an object across a network or other I/O
@@ -23,16 +22,16 @@ connection. After creating a server or client instance, objects can be registere
them visible as 'services'. Exported methods that follow specific conventions can be
called remotely. It also has support for the publish/subscribe pattern.
RPC Methods
# RPC Methods
Methods that satisfy the following criteria are made available for remote access:
- method must be exported
- method returns 0, 1 (response or error) or 2 (response and error) values
- method must be exported
- method returns 0, 1 (response or error) or 2 (response and error) values
An example method:
func (s *CalcService) Add(a, b int) (int, error)
func (s *CalcService) Add(a, b int) (int, error)
When the returned error isn't nil the returned integer is ignored and the error is sent
back to the client. Otherwise the returned integer is sent back to the client.
@@ -41,7 +40,7 @@ Optional arguments are supported by accepting pointer values as arguments. E.g.
to do the addition in an optional finite field we can accept a mod argument as pointer
value.
func (s *CalcService) Add(a, b int, mod *int) (int, error)
func (s *CalcService) Add(a, b int, mod *int) (int, error)
This RPC method can be called with 2 integers and a null value as third argument. In that
case the mod argument will be nil. Or it can be called with 3 integers, in that case mod
@@ -56,40 +55,40 @@ to the client out of order.
An example server which uses the JSON codec:
type CalculatorService struct {}
type CalculatorService struct {}
func (s *CalculatorService) Add(a, b int) int {
return a + b
}
func (s *CalculatorService) Add(a, b int) int {
return a + b
}
func (s *CalculatorService) Div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a/b, nil
}
func (s *CalculatorService) Div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a/b, nil
}
calculator := new(CalculatorService)
server := NewServer()
server.RegisterName("calculator", calculator)
l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"})
server.ServeListener(l)
calculator := new(CalculatorService)
server := NewServer()
server.RegisterName("calculator", calculator)
l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"})
server.ServeListener(l)
Subscriptions
# Subscriptions
The package also supports the publish subscribe pattern through the use of subscriptions.
A method that is considered eligible for notifications must satisfy the following
criteria:
- method must be exported
- first method argument type must be context.Context
- method must have return types (rpc.Subscription, error)
- method must be exported
- first method argument type must be context.Context
- method must have return types (rpc.Subscription, error)
An example method:
func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) {
...
}
func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) {
...
}
When the service containing the subscription method is registered to the server, for
example under the "blockchain" namespace, a subscription is created by calling the
@@ -101,7 +100,7 @@ the client and server. The server will close the connection for any write error.
For more information about subscriptions, see https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB.
Reverse Calls
# Reverse Calls
In any method handler, an instance of rpc.Client can be accessed through the
ClientFromContext method. Using this client instance, server-to-client method calls can be
+22 -1
View File
@@ -54,9 +54,20 @@ var (
_ Error = new(invalidRequestError)
_ Error = new(invalidMessageError)
_ Error = new(invalidParamsError)
_ Error = new(internalServerError)
)
const defaultErrorCode = -32000
const (
errcodeDefault = -32000
errcodeNotificationsUnsupported = -32001
errcodeTimeout = -32002
errcodePanic = -32603
errcodeMarshalError = -32603
)
const (
errMsgTimeout = "request timed out"
)
type methodNotFoundError struct{ method string }
@@ -101,3 +112,13 @@ type invalidParamsError struct{ message string }
func (e *invalidParamsError) ErrorCode() int { return -32602 }
func (e *invalidParamsError) Error() string { return e.message }
// internalServerError is used for server errors during request processing.
type internalServerError struct {
code int
message string
}
func (e *internalServerError) ErrorCode() int { return e.code }
func (e *internalServerError) Error() string { return e.message }
+143 -19
View File
@@ -34,21 +34,20 @@ import (
//
// The entry points for incoming messages are:
//
// h.handleMsg(message)
// h.handleBatch(message)
// h.handleMsg(message)
// h.handleBatch(message)
//
// Outgoing calls use the requestOp struct. Register the request before sending it
// on the connection:
//
// op := &requestOp{ids: ...}
// h.addRequestOp(op)
// op := &requestOp{ids: ...}
// h.addRequestOp(op)
//
// Now send the request, then wait for the reply to be delivered through handleMsg:
//
// if err := op.wait(...); err != nil {
// h.removeRequestOp(op) // timeout, etc.
// }
//
// if err := op.wait(...); err != nil {
// h.removeRequestOp(op) // timeout, etc.
// }
type handler struct {
reg *serviceRegistry
unsubscribeCb *callback
@@ -92,12 +91,83 @@ func newHandler(connCtx context.Context, conn jsonWriter, idgen func() ID, reg *
return h
}
// batchCallBuffer manages in progress call messages and their responses during a batch
// call. Calls need to be synchronized between the processing and timeout-triggering
// goroutines.
type batchCallBuffer struct {
mutex sync.Mutex
calls []*jsonrpcMessage
resp []*jsonrpcMessage
wrote bool
}
// nextCall returns the next unprocessed message.
func (b *batchCallBuffer) nextCall() *jsonrpcMessage {
b.mutex.Lock()
defer b.mutex.Unlock()
if len(b.calls) == 0 {
return nil
}
// The popping happens in `pushAnswer`. The in progress call is kept
// so we can return an error for it in case of timeout.
msg := b.calls[0]
return msg
}
// pushResponse adds the response to last call returned by nextCall.
func (b *batchCallBuffer) pushResponse(answer *jsonrpcMessage) {
b.mutex.Lock()
defer b.mutex.Unlock()
if answer != nil {
b.resp = append(b.resp, answer)
}
b.calls = b.calls[1:]
}
// write sends the responses.
func (b *batchCallBuffer) write(ctx context.Context, conn jsonWriter) {
b.mutex.Lock()
defer b.mutex.Unlock()
b.doWrite(ctx, conn, false)
}
// timeout sends the responses added so far. For the remaining unanswered call
// messages, it sends a timeout error response.
func (b *batchCallBuffer) timeout(ctx context.Context, conn jsonWriter) {
b.mutex.Lock()
defer b.mutex.Unlock()
for _, msg := range b.calls {
if !msg.isNotification() {
resp := msg.errorResponse(&internalServerError{errcodeTimeout, errMsgTimeout})
b.resp = append(b.resp, resp)
}
}
b.doWrite(ctx, conn, true)
}
// doWrite actually writes the response.
// This assumes b.mutex is held.
func (b *batchCallBuffer) doWrite(ctx context.Context, conn jsonWriter, isErrorResponse bool) {
if b.wrote {
return
}
b.wrote = true // can only write once
if len(b.resp) > 0 {
conn.writeJSON(ctx, b.resp, isErrorResponse)
}
}
// handleBatch executes all messages in a batch and returns the responses.
func (h *handler) handleBatch(msgs []*jsonrpcMessage) {
// Emit error response for empty batches:
if len(msgs) == 0 {
h.startCallProc(func(cp *callProc) {
h.conn.writeJSON(cp.ctx, errorMessage(&invalidRequestError{"empty batch"}))
resp := errorMessage(&invalidRequestError{"empty batch"})
h.conn.writeJSON(cp.ctx, resp, true)
})
return
}
@@ -114,16 +184,42 @@ func (h *handler) handleBatch(msgs []*jsonrpcMessage) {
}
// Process calls on a goroutine because they may block indefinitely:
h.startCallProc(func(cp *callProc) {
answers := make([]*jsonrpcMessage, 0, len(msgs))
for _, msg := range calls {
if answer := h.handleCallMsg(cp, msg); answer != nil {
answers = append(answers, answer)
var (
timer *time.Timer
cancel context.CancelFunc
callBuffer = &batchCallBuffer{calls: calls, resp: make([]*jsonrpcMessage, 0, len(calls))}
)
cp.ctx, cancel = context.WithCancel(cp.ctx)
defer cancel()
// Cancel the request context after timeout and send an error response. Since the
// currently-running method might not return immediately on timeout, we must wait
// for the timeout concurrently with processing the request.
if timeout, ok := ContextRequestTimeout(cp.ctx); ok {
timer = time.AfterFunc(timeout, func() {
cancel()
callBuffer.timeout(cp.ctx, h.conn)
})
}
for {
// No need to handle rest of calls if timed out.
if cp.ctx.Err() != nil {
break
}
msg := callBuffer.nextCall()
if msg == nil {
break
}
resp := h.handleCallMsg(cp, msg)
callBuffer.pushResponse(resp)
}
if timer != nil {
timer.Stop()
}
callBuffer.write(cp.ctx, h.conn)
h.addSubscriptions(cp.notifiers)
if len(answers) > 0 {
h.conn.writeJSON(cp.ctx, answers)
}
for _, n := range cp.notifiers {
n.activate()
}
@@ -136,10 +232,36 @@ func (h *handler) handleMsg(msg *jsonrpcMessage) {
return
}
h.startCallProc(func(cp *callProc) {
var (
responded sync.Once
timer *time.Timer
cancel context.CancelFunc
)
cp.ctx, cancel = context.WithCancel(cp.ctx)
defer cancel()
// Cancel the request context after timeout and send an error response. Since the
// running method might not return immediately on timeout, we must wait for the
// timeout concurrently with processing the request.
if timeout, ok := ContextRequestTimeout(cp.ctx); ok {
timer = time.AfterFunc(timeout, func() {
cancel()
responded.Do(func() {
resp := msg.errorResponse(&internalServerError{errcodeTimeout, errMsgTimeout})
h.conn.writeJSON(cp.ctx, resp, true)
})
})
}
answer := h.handleCallMsg(cp, msg)
if timer != nil {
timer.Stop()
}
h.addSubscriptions(cp.notifiers)
if answer != nil {
h.conn.writeJSON(cp.ctx, answer)
responded.Do(func() {
h.conn.writeJSON(cp.ctx, answer, false)
})
}
for _, n := range cp.notifiers {
n.activate()
@@ -339,7 +461,6 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage
//begin PluGeth code injection
start := time.Now()
answer := h.runMethod(cp.ctx, msg, callb, args)
// Collect the statistics for RPC calls if metrics is enabled.
// We only care about pure rpc call. Filter out subscription.
if callb != h.unsubscribeCb {
@@ -358,7 +479,10 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage
// handleSubscribe processes *_subscribe method calls.
func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
if !h.allowSubscribe {
return msg.errorResponse(ErrNotificationsUnsupported)
return msg.errorResponse(&internalServerError{
code: errcodeNotificationsUnsupported,
message: ErrNotificationsUnsupported.Error(),
})
}
// Subscription method name is first argument.
+115 -19
View File
@@ -23,9 +23,11 @@ import (
"errors"
"fmt"
"io"
"math"
"mime"
"net/http"
"net/url"
"strconv"
"sync"
"time"
)
@@ -45,13 +47,14 @@ type httpConn struct {
closeCh chan interface{}
mu sync.Mutex // protects headers
headers http.Header
auth HTTPAuth
}
// httpConn implements ServerCodec, but it is treated specially by Client
// and some methods don't work. The panic() stubs here exist to ensure
// this special treatment is correct.
func (hc *httpConn) writeJSON(context.Context, interface{}) error {
func (hc *httpConn) writeJSON(context.Context, interface{}, bool) error {
panic("writeJSON called on httpConn")
}
@@ -117,8 +120,15 @@ var DefaultHTTPTimeouts = HTTPTimeouts{
IdleTimeout: 120 * time.Second,
}
// DialHTTP creates a new RPC client that connects to an RPC server over HTTP.
func DialHTTP(endpoint string) (*Client, error) {
return DialHTTPWithClient(endpoint, new(http.Client))
}
// DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP
// using the provided HTTP Client.
//
// Deprecated: use DialOptions and the WithHTTPClient option.
func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) {
// Sanity check URL so we don't end up with a client that will fail every request.
_, err := url.Parse(endpoint)
@@ -126,24 +136,36 @@ func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) {
return nil, err
}
initctx := context.Background()
headers := make(http.Header, 2)
headers.Set("accept", contentType)
headers.Set("content-type", contentType)
return newClient(initctx, func(context.Context) (ServerCodec, error) {
hc := &httpConn{
client: client,
headers: headers,
url: endpoint,
closeCh: make(chan interface{}),
}
return hc, nil
})
var cfg clientConfig
cfg.httpClient = client
fn := newClientTransportHTTP(endpoint, &cfg)
return newClient(context.Background(), fn)
}
// DialHTTP creates a new RPC client that connects to an RPC server over HTTP.
func DialHTTP(endpoint string) (*Client, error) {
return DialHTTPWithClient(endpoint, new(http.Client))
func newClientTransportHTTP(endpoint string, cfg *clientConfig) reconnectFunc {
headers := make(http.Header, 2+len(cfg.httpHeaders))
headers.Set("accept", contentType)
headers.Set("content-type", contentType)
for key, values := range cfg.httpHeaders {
headers[key] = values
}
client := cfg.httpClient
if client == nil {
client = new(http.Client)
}
hc := &httpConn{
client: client,
headers: headers,
url: endpoint,
auth: cfg.httpAuth,
closeCh: make(chan interface{}),
}
return func(ctx context.Context) (ServerCodec, error) {
return hc, nil
}
}
func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error {
@@ -187,7 +209,7 @@ func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadClos
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", hc.url, io.NopCloser(bytes.NewReader(body)))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, hc.url, io.NopCloser(bytes.NewReader(body)))
if err != nil {
return nil, err
}
@@ -198,6 +220,13 @@ func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadClos
hc.mu.Lock()
req.Header = hc.headers.Clone()
hc.mu.Unlock()
setHeaders(req.Header, headersFromContext(ctx))
if hc.auth != nil {
if err := hc.auth(req.Header); err != nil {
return nil, err
}
}
// do request
resp, err := hc.client.Do(req)
@@ -230,7 +259,42 @@ type httpServerConn struct {
func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec {
body := io.LimitReader(r.Body, maxRequestContentLength)
conn := &httpServerConn{Reader: body, Writer: w, r: r}
return NewCodec(conn)
encoder := func(v any, isErrorResponse bool) error {
if !isErrorResponse {
return json.NewEncoder(conn).Encode(v)
}
// It's an error response and requires special treatment.
//
// In case of a timeout error, the response must be written before the HTTP
// server's write timeout occurs. So we need to flush the response. The
// Content-Length header also needs to be set to ensure the client knows
// when it has the full response.
encdata, err := json.Marshal(v)
if err != nil {
return err
}
w.Header().Set("content-length", strconv.Itoa(len(encdata)))
// If this request is wrapped in a handler that might remove Content-Length (such
// as the automatic gzip we do in package node), we need to ensure the HTTP server
// doesn't perform chunked encoding. In case WriteTimeout is reached, the chunked
// encoding might not be finished correctly, and some clients do not like it when
// the final chunk is missing.
w.Header().Set("transfer-encoding", "identity")
_, err = w.Write(encdata)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
return err
}
dec := json.NewDecoder(conn)
dec.UseNumber()
return NewFuncCodec(conn, encoder, dec.Decode)
}
// Close does nothing and always returns nil.
@@ -300,3 +364,35 @@ func validateRequest(r *http.Request) (int, error) {
err := fmt.Errorf("invalid content type, only %s is supported", contentType)
return http.StatusUnsupportedMediaType, err
}
// ContextRequestTimeout returns the request timeout derived from the given context.
func ContextRequestTimeout(ctx context.Context) (time.Duration, bool) {
timeout := time.Duration(math.MaxInt64)
hasTimeout := false
setTimeout := func(d time.Duration) {
if d < timeout {
timeout = d
hasTimeout = true
}
}
if deadline, ok := ctx.Deadline(); ok {
setTimeout(time.Until(deadline))
}
// If the context is an HTTP request context, use the server's WriteTimeout.
httpSrv, ok := ctx.Value(http.ServerContextKey).(*http.Server)
if ok && httpSrv.WriteTimeout > 0 {
wt := httpSrv.WriteTimeout
// When a write timeout is configured, we need to send the response message before
// the HTTP server cuts connection. So our internal timeout must be earlier than
// the server's true timeout.
//
// Note: Timeouts are sanitized to be a minimum of 1 second.
// Also see issue: https://github.com/golang/go/issues/47229
wt -= 100 * time.Millisecond
setTimeout(wt)
}
return timeout, hasTimeout
}
+42
View File
@@ -17,6 +17,8 @@
package rpc
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -198,3 +200,43 @@ func TestHTTPPeerInfo(t *testing.T) {
t.Errorf("wrong HTTP.Origin %q", info.HTTP.UserAgent)
}
}
func TestNewContextWithHeaders(t *testing.T) {
expectedHeaders := 0
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
for i := 0; i < expectedHeaders; i++ {
key, want := fmt.Sprintf("key-%d", i), fmt.Sprintf("val-%d", i)
if have := request.Header.Get(key); have != want {
t.Errorf("wrong request headers for %s, want: %s, have: %s", key, want, have)
}
}
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte(`{}`))
}))
defer server.Close()
client, err := Dial(server.URL)
if err != nil {
t.Fatalf("failed to dial: %s", err)
}
defer client.Close()
newHdr := func(k, v string) http.Header {
header := http.Header{}
header.Set(k, v)
return header
}
ctx1 := NewContextWithHeaders(context.Background(), newHdr("key-0", "val-0"))
ctx2 := NewContextWithHeaders(ctx1, newHdr("key-1", "val-1"))
ctx3 := NewContextWithHeaders(ctx2, newHdr("key-2", "val-2"))
expectedHeaders = 3
if err := client.CallContext(ctx3, nil, "test"); err != ErrNoResult {
t.Error("call failed", err)
}
expectedHeaders = 2
if err := client.CallContext(ctx2, nil, "test"); err != ErrNoResult {
t.Error("call failed:", err)
}
}
+6 -2
View File
@@ -46,11 +46,15 @@ func (s *Server) ServeListener(l net.Listener) error {
// The context is used for the initial connection establishment. It does not
// affect subsequent interactions with the client.
func DialIPC(ctx context.Context, endpoint string) (*Client, error) {
return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
return newClient(ctx, newClientTransportIPC(endpoint))
}
func newClientTransportIPC(endpoint string) reconnectFunc {
return func(ctx context.Context) (ServerCodec, error) {
conn, err := newIPCConnection(ctx, endpoint)
if err != nil {
return nil, err
}
return NewCodec(conn), err
})
}
}
+3 -2
View File
@@ -31,8 +31,9 @@ import (
// ipcListen will create a Unix socket on the given endpoint.
func ipcListen(endpoint string) (net.Listener, error) {
if len(endpoint) > int(max_path_size) {
log.Warn(fmt.Sprintf("The ipc endpoint is longer than %d characters. ", max_path_size),
// account for null-terminator too
if len(endpoint)+1 > int(max_path_size) {
log.Warn(fmt.Sprintf("The ipc endpoint is longer than %d characters. ", max_path_size-1),
"endpoint", endpoint)
}
+26 -15
View File
@@ -58,21 +58,25 @@ type jsonrpcMessage struct {
}
func (msg *jsonrpcMessage) isNotification() bool {
return msg.ID == nil && msg.Method != ""
return msg.hasValidVersion() && msg.ID == nil && msg.Method != ""
}
func (msg *jsonrpcMessage) isCall() bool {
return msg.hasValidID() && msg.Method != ""
return msg.hasValidVersion() && msg.hasValidID() && msg.Method != ""
}
func (msg *jsonrpcMessage) isResponse() bool {
return msg.hasValidID() && msg.Method == "" && msg.Params == nil && (msg.Result != nil || msg.Error != nil)
return msg.hasValidVersion() && msg.hasValidID() && msg.Method == "" && msg.Params == nil && (msg.Result != nil || msg.Error != nil)
}
func (msg *jsonrpcMessage) hasValidID() bool {
return len(msg.ID) > 0 && msg.ID[0] != '{' && msg.ID[0] != '['
}
func (msg *jsonrpcMessage) hasValidVersion() bool {
return msg.Version == vsn
}
func (msg *jsonrpcMessage) isSubscribe() bool {
return strings.HasSuffix(msg.Method, subscribeMethodSuffix)
}
@@ -100,15 +104,14 @@ func (msg *jsonrpcMessage) errorResponse(err error) *jsonrpcMessage {
func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage {
enc, err := json.Marshal(result)
if err != nil {
// TODO: wrap with 'internal server error'
return msg.errorResponse(err)
return msg.errorResponse(&internalServerError{errcodeMarshalError, err.Error()})
}
return &jsonrpcMessage{Version: vsn, ID: msg.ID, Result: enc}
}
func errorMessage(err error) *jsonrpcMessage {
msg := &jsonrpcMessage{Version: vsn, ID: null, Error: &jsonError{
Code: defaultErrorCode,
Code: errcodeDefault,
Message: err.Error(),
}}
ec, ok := err.(Error)
@@ -165,18 +168,22 @@ type ConnRemoteAddr interface {
// support for parsing arguments and serializing (result) objects.
type jsonCodec struct {
remote string
closer sync.Once // close closed channel once
closeCh chan interface{} // closed on Close
decode func(v interface{}) error // decoder to allow multiple transports
encMu sync.Mutex // guards the encoder
encode func(v interface{}) error // encoder to allow multiple transports
closer sync.Once // close closed channel once
closeCh chan interface{} // closed on Close
decode decodeFunc // decoder to allow multiple transports
encMu sync.Mutex // guards the encoder
encode encodeFunc // encoder to allow multiple transports
conn deadlineCloser
}
type encodeFunc = func(v interface{}, isErrorResponse bool) error
type decodeFunc = func(v interface{}) error
// NewFuncCodec creates a codec which uses the given functions to read and write. If conn
// implements ConnRemoteAddr, log messages will use it to include the remote address of
// the connection.
func NewFuncCodec(conn deadlineCloser, encode, decode func(v interface{}) error) ServerCodec {
func NewFuncCodec(conn deadlineCloser, encode encodeFunc, decode decodeFunc) ServerCodec {
codec := &jsonCodec{
closeCh: make(chan interface{}),
encode: encode,
@@ -195,7 +202,11 @@ func NewCodec(conn Conn) ServerCodec {
enc := json.NewEncoder(conn)
dec := json.NewDecoder(conn)
dec.UseNumber()
return NewFuncCodec(conn, enc.Encode, dec.Decode)
encode := func(v interface{}, isErrorResponse bool) error {
return enc.Encode(v)
}
return NewFuncCodec(conn, encode, dec.Decode)
}
func (c *jsonCodec) peerInfo() PeerInfo {
@@ -225,7 +236,7 @@ func (c *jsonCodec) readBatch() (messages []*jsonrpcMessage, batch bool, err err
return messages, batch, nil
}
func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}) error {
func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}, isErrorResponse bool) error {
c.encMu.Lock()
defer c.encMu.Unlock()
@@ -234,7 +245,7 @@ func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}) error {
deadline = time.Now().Add(defaultWriteTimeout)
}
c.conn.SetWriteDeadline(deadline)
return c.encode(v)
return c.encode(v, isErrorResponse)
}
func (c *jsonCodec) close() {
+38 -15
View File
@@ -19,9 +19,9 @@ package rpc
import (
"context"
"io"
"sync"
"sync/atomic"
mapset "github.com/deckarep/golang-set"
"github.com/ethereum/go-ethereum/log"
)
@@ -45,13 +45,19 @@ const (
type Server struct {
services serviceRegistry
idgen func() ID
run int32
codecs mapset.Set
mutex sync.Mutex
codecs map[ServerCodec]struct{}
run int32
}
// NewServer creates a new server instance with no registered handlers.
func NewServer() *Server {
server := &Server{idgen: randomIDGenerator(), codecs: mapset.NewSet(), run: 1}
server := &Server{
idgen: randomIDGenerator(),
codecs: make(map[ServerCodec]struct{}),
run: 1,
}
// Register the default service providing meta information about the RPC service such
// as the services and methods it offers.
rpcService := &RPCService{server}
@@ -75,20 +81,34 @@ func (s *Server) RegisterName(name string, receiver interface{}) error {
func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) {
defer codec.close()
// Don't serve if server is stopped.
if atomic.LoadInt32(&s.run) == 0 {
if !s.trackCodec(codec) {
return
}
// Add the codec to the set so it can be closed by Stop.
s.codecs.Add(codec)
defer s.codecs.Remove(codec)
defer s.untrackCodec(codec)
c := initClient(codec, s.idgen, &s.services)
<-codec.closed()
c.Close()
}
func (s *Server) trackCodec(codec ServerCodec) bool {
s.mutex.Lock()
defer s.mutex.Unlock()
if atomic.LoadInt32(&s.run) == 0 {
return false // Don't serve if server is stopped.
}
s.codecs[codec] = struct{}{}
return true
}
func (s *Server) untrackCodec(codec ServerCodec) {
s.mutex.Lock()
defer s.mutex.Unlock()
delete(s.codecs, codec)
}
// serveSingleRequest reads and processes a single RPC request from the given codec. This
// is used to serve HTTP connections. Subscriptions and reverse calls are not allowed in
// this mode.
@@ -105,7 +125,8 @@ func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
reqs, batch, err := codec.readBatch()
if err != nil {
if err != io.EOF {
codec.writeJSON(ctx, errorMessage(&invalidMessageError{"parse error"}))
resp := errorMessage(&invalidMessageError{"parse error"})
codec.writeJSON(ctx, resp, true)
}
return
}
@@ -120,12 +141,14 @@ func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
// requests to finish, then closes all codecs which will cancel pending requests and
// subscriptions.
func (s *Server) Stop() {
s.mutex.Lock()
defer s.mutex.Unlock()
if atomic.CompareAndSwapInt32(&s.run, 1, 0) {
log.Debug("RPC server shutting down")
s.codecs.Each(func(c interface{}) bool {
c.(ServerCodec).close()
return true
})
for codec := range s.codecs {
codec.close()
}
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ func TestServerRegisterName(t *testing.T) {
t.Fatalf("Expected service calc to be registered")
}
wantCallbacks := 10
wantCallbacks := 12
if len(svc.callbacks) != wantCallbacks {
t.Errorf("Expected %d callbacks for service 'service', got %d", wantCallbacks, len(svc.callbacks))
}
+2 -6
View File
@@ -18,7 +18,6 @@ package rpc
import (
"context"
"errors"
"fmt"
"reflect"
"runtime"
@@ -64,9 +63,6 @@ func (r *serviceRegistry) registerName(name string, rcvr interface{}) error {
return fmt.Errorf("no service name for type %s", rcvrVal.Type().String())
}
callbacks := suitableCallbacks(rcvrVal)
// begin PluGeth code injection
pluginExtendedCallbacks(callbacks, rcvrVal)
// end PluGeth code injection
if len(callbacks) == 0 {
return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr)
}
@@ -201,8 +197,8 @@ func (c *callback) call(ctx context.Context, method string, args []reflect.Value
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf), "args", fullargs)
errRes = errors.New("method handler crashed")
log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf))
errRes = &internalServerError{errcodePanic, "method handler crashed"}
}
}()
// Run the callback.
+6 -2
View File
@@ -32,12 +32,16 @@ func DialStdIO(ctx context.Context) (*Client, error) {
// DialIO creates a client which uses the given IO channels
func DialIO(ctx context.Context, in io.Reader, out io.Writer) (*Client, error) {
return newClient(ctx, func(_ context.Context) (ServerCodec, error) {
return newClient(ctx, newClientTransportIO(in, out))
}
func newClientTransportIO(in io.Reader, out io.Writer) reconnectFunc {
return func(context.Context) (ServerCodec, error) {
return NewCodec(stdioConn{
in: in,
out: out,
}), nil
})
}
}
type stdioConn struct {
+4 -2
View File
@@ -175,11 +175,13 @@ func (n *Notifier) activate() error {
func (n *Notifier) send(sub *Subscription, data json.RawMessage) error {
params, _ := json.Marshal(&subscriptionResult{ID: string(sub.ID), Result: data})
ctx := context.Background()
return n.h.conn.writeJSON(ctx, &jsonrpcMessage{
msg := &jsonrpcMessage{
Version: vsn,
Method: n.namespace + notificationMethodSuffix,
Params: params,
})
}
return n.h.conn.writeJSON(ctx, msg, false)
}
// A Subscription is created by a notifier and tied to that notifier. The client can use
+1 -1
View File
@@ -79,7 +79,7 @@ func TestSubscriptions(t *testing.T) {
request := map[string]interface{}{
"id": i,
"method": fmt.Sprintf("%s_subscribe", namespace),
"version": "2.0",
"jsonrpc": "2.0",
"params": []interface{}{"someSubscription", notificationCount, i},
}
if err := out.Encode(&request); err != nil {
+7
View File
@@ -0,0 +1,7 @@
// These tests trigger various 'internal error' conditions.
--> {"jsonrpc":"2.0","id":1,"method":"test_marshalError","params": []}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"json: error calling MarshalText for type *rpc.MarshalErrObj: marshal error"}}
--> {"jsonrpc":"2.0","id":2,"method":"test_panic","params": []}
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32603,"message":"method handler crashed"}}
+19
View File
@@ -0,0 +1,19 @@
// This test checks processing of messages with invalid Version.
--> {"jsonrpc":"2.0","id":1,"method":"test_echo","params":["x", 3]}
<-- {"jsonrpc":"2.0","id":1,"result":{"String":"x","Int":3,"Args":null}}
--> {"jsonrpc":"2.1","id":1,"method":"test_echo","params":["x", 3]}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
--> {"jsonrpc":"go-ethereum","id":1,"method":"test_echo","params":["x", 3]}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
--> {"jsonrpc":1,"id":1,"method":"test_echo","params":["x", 3]}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
--> {"jsonrpc":2.0,"id":1,"method":"test_echo","params":["x", 3]}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
--> {"id":1,"method":"test_echo","params":["x", 3]}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
+14
View File
@@ -70,6 +70,12 @@ func (testError) Error() string { return "testError" }
func (testError) ErrorCode() int { return 444 }
func (testError) ErrorData() interface{} { return "testError data" }
type MarshalErrObj struct{}
func (o *MarshalErrObj) MarshalText() ([]byte, error) {
return nil, errors.New("marshal error")
}
func (s *testService) NoArgsRets() {}
func (s *testService) Echo(str string, i int, args *echoArgs) echoResult {
@@ -114,6 +120,14 @@ func (s *testService) ReturnError() error {
return testError{}
}
func (s *testService) MarshalError() *MarshalErrObj {
return &MarshalErrObj{}
}
func (s *testService) Panic() string {
panic("service panic")
}
func (s *testService) CallMeBack(ctx context.Context, method string, args []interface{}) (interface{}, error) {
c, ok := ClientFromContext(ctx)
if !ok {
+5 -24
View File
@@ -51,7 +51,9 @@ type ServerCodec interface {
// jsonWriter can write JSON messages to its underlying connection.
// Implementations must be safe for concurrent use.
type jsonWriter interface {
writeJSON(context.Context, interface{}) error
// writeJSON writes a message to the connection.
writeJSON(ctx context.Context, msg interface{}, isError bool) error
// Closed returns a channel which is closed when the connection is closed.
closed() <-chan interface{}
// RemoteAddr returns the peer address of the connection.
@@ -69,7 +71,7 @@ const (
)
// UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports:
// - "latest", "earliest" or "pending" as string arguments
// - "safe", "finalized", "latest", "earliest" or "pending" as string arguments
// - the block number
// Returned errors:
// - an invalid block number error when the given argument isn't a known strings
@@ -110,7 +112,7 @@ func (bn *BlockNumber) UnmarshalJSON(data []byte) error {
}
// MarshalText implements encoding.TextMarshaler. It marshals:
// - "latest", "earliest" or "pending" as strings
// - "safe", "finalized", "latest", "earliest" or "pending" as strings
// - other numbers as hex
func (bn BlockNumber) MarshalText() ([]byte, error) {
switch bn {
@@ -241,24 +243,3 @@ func BlockNumberOrHashWithHash(hash common.Hash, canonical bool) BlockNumberOrHa
RequireCanonical: canonical,
}
}
// DecimalOrHex unmarshals a non-negative decimal or hex parameter into a uint64.
type DecimalOrHex uint64
// UnmarshalJSON implements json.Unmarshaler.
func (dh *DecimalOrHex) UnmarshalJSON(data []byte) error {
input := strings.TrimSpace(string(data))
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
input = input[1 : len(input)-1]
}
value, err := strconv.ParseUint(input, 10, 64)
if err != nil {
value, err = hexutil.DecodeUint64(input)
}
if err != nil {
return err
}
*dh = DecimalOrHex(value)
return nil
}
+71 -27
View File
@@ -27,7 +27,7 @@ import (
"sync"
"time"
mapset "github.com/deckarep/golang-set"
mapset "github.com/deckarep/golang-set/v2"
"github.com/ethereum/go-ethereum/log"
"github.com/gorilla/websocket"
)
@@ -35,7 +35,7 @@ import (
const (
wsReadBuffer = 1024
wsWriteBuffer = 1024
wsPingInterval = 60 * time.Second
wsPingInterval = 30 * time.Second
wsPingWriteTimeout = 5 * time.Second
wsPongTimeout = 30 * time.Second
wsMessageSizeLimit = 15 * 1024 * 1024
@@ -69,7 +69,7 @@ func (s *Server) WebsocketHandler(allowedOrigins []string) http.Handler {
// websocket upgrade process. When a '*' is specified as an allowed origins all
// connections are accepted.
func wsHandshakeValidator(allowedOrigins []string) func(*http.Request) bool {
origins := mapset.NewSet()
origins := mapset.NewSet[string]()
allowAllOrigins := false
for _, origin := range allowedOrigins {
@@ -122,10 +122,10 @@ func (e wsHandshakeError) Error() string {
return s
}
func originIsAllowed(allowedOrigins mapset.Set, browserOrigin string) bool {
func originIsAllowed(allowedOrigins mapset.Set[string], browserOrigin string) bool {
it := allowedOrigins.Iterator()
for origin := range it.C {
if ruleAllowsOrigin(origin.(string), browserOrigin) {
if ruleAllowsOrigin(origin, browserOrigin) {
return true
}
}
@@ -181,24 +181,23 @@ func parseOriginURL(origin string) (string, string, string, error) {
return scheme, hostname, port, nil
}
// DialWebsocketWithDialer creates a new RPC client that communicates with a JSON-RPC server
// that is listening on the given endpoint using the provided dialer.
// DialWebsocketWithDialer creates a new RPC client using WebSocket.
//
// The context is used for the initial connection establishment. It does not
// affect subsequent interactions with the client.
//
// Deprecated: use DialOptions and the WithWebsocketDialer option.
func DialWebsocketWithDialer(ctx context.Context, endpoint, origin string, dialer websocket.Dialer) (*Client, error) {
endpoint, header, err := wsClientHeaders(endpoint, origin)
cfg := new(clientConfig)
cfg.wsDialer = &dialer
if origin != "" {
cfg.setHeader("origin", origin)
}
connect, err := newClientTransportWS(endpoint, cfg)
if err != nil {
return nil, err
}
return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
conn, resp, err := dialer.DialContext(ctx, endpoint, header)
if err != nil {
hErr := wsHandshakeError{err: err}
if resp != nil {
hErr.status = resp.Status
}
return nil, hErr
}
return newWebsocketCodec(conn, endpoint, header), nil
})
return newClient(ctx, connect)
}
// DialWebsocket creates a new RPC client that communicates with a JSON-RPC server
@@ -207,12 +206,53 @@ func DialWebsocketWithDialer(ctx context.Context, endpoint, origin string, diale
// The context is used for the initial connection establishment. It does not
// affect subsequent interactions with the client.
func DialWebsocket(ctx context.Context, endpoint, origin string) (*Client, error) {
dialer := websocket.Dialer{
ReadBufferSize: wsReadBuffer,
WriteBufferSize: wsWriteBuffer,
WriteBufferPool: wsBufferPool,
cfg := new(clientConfig)
if origin != "" {
cfg.setHeader("origin", origin)
}
return DialWebsocketWithDialer(ctx, endpoint, origin, dialer)
connect, err := newClientTransportWS(endpoint, cfg)
if err != nil {
return nil, err
}
return newClient(ctx, connect)
}
func newClientTransportWS(endpoint string, cfg *clientConfig) (reconnectFunc, error) {
dialer := cfg.wsDialer
if dialer == nil {
dialer = &websocket.Dialer{
ReadBufferSize: wsReadBuffer,
WriteBufferSize: wsWriteBuffer,
WriteBufferPool: wsBufferPool,
}
}
dialURL, header, err := wsClientHeaders(endpoint, "")
if err != nil {
return nil, err
}
for key, values := range cfg.httpHeaders {
header[key] = values
}
connect := func(ctx context.Context) (ServerCodec, error) {
header := header.Clone()
if cfg.httpAuth != nil {
if err := cfg.httpAuth(header); err != nil {
return nil, err
}
}
conn, resp, err := dialer.DialContext(ctx, dialURL, header)
if err != nil {
hErr := wsHandshakeError{err: err}
if resp != nil {
hErr.status = resp.Status
}
return nil, hErr
}
return newWebsocketCodec(conn, dialURL, header), nil
}
return connect, nil
}
func wsClientHeaders(endpoint, origin string) (string, http.Header, error) {
@@ -247,8 +287,12 @@ func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header) Serve
conn.SetReadDeadline(time.Time{})
return nil
})
encode := func(v interface{}, isErrorResponse bool) error {
return conn.WriteJSON(v)
}
wc := &websocketCodec{
jsonCodec: NewFuncCodec(conn, conn.WriteJSON, conn.ReadJSON).(*jsonCodec),
jsonCodec: NewFuncCodec(conn, encode, conn.ReadJSON).(*jsonCodec),
conn: conn,
pingReset: make(chan struct{}, 1),
info: PeerInfo{
@@ -275,8 +319,8 @@ func (wc *websocketCodec) peerInfo() PeerInfo {
return wc.info
}
func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}) error {
err := wc.jsonCodec.writeJSON(ctx, v)
func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}, isError bool) error {
err := wc.jsonCodec.writeJSON(ctx, v, isError)
if err == nil {
// Notify pingLoop to delay the next idle ping.
select {
-89
View File
@@ -19,14 +19,10 @@ package rpc
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
@@ -227,63 +223,6 @@ func TestClientWebsocketLargeMessage(t *testing.T) {
}
}
func TestClientWebsocketSevered(t *testing.T) {
t.Parallel()
var (
server = wsPingTestServer(t, nil)
ctx = context.Background()
)
defer server.Shutdown(ctx)
u, err := url.Parse("http://" + server.Addr)
if err != nil {
t.Fatal(err)
}
rproxy := httputil.NewSingleHostReverseProxy(u)
var severable *severableReadWriteCloser
rproxy.ModifyResponse = func(response *http.Response) error {
severable = &severableReadWriteCloser{ReadWriteCloser: response.Body.(io.ReadWriteCloser)}
response.Body = severable
return nil
}
frontendProxy := httptest.NewServer(rproxy)
defer frontendProxy.Close()
wsURL := "ws:" + strings.TrimPrefix(frontendProxy.URL, "http:")
client, err := DialWebsocket(ctx, wsURL, "")
if err != nil {
t.Fatalf("client dial error: %v", err)
}
defer client.Close()
resultChan := make(chan int)
sub, err := client.EthSubscribe(ctx, resultChan, "foo")
if err != nil {
t.Fatalf("client subscribe error: %v", err)
}
// sever the connection
severable.Sever()
// Wait for subscription error.
timeout := time.NewTimer(3 * wsPingInterval)
defer timeout.Stop()
for {
select {
case err := <-sub.Err():
t.Log("client subscription error:", err)
return
case result := <-resultChan:
t.Error("unexpected result:", result)
return
case <-timeout.C:
t.Error("didn't get any error within the test timeout")
return
}
}
}
// wsPingTestServer runs a WebSocket server which accepts a single subscription request.
// When a value arrives on sendPing, the server sends a ping frame, waits for a matching
// pong and finally delivers a single subscription result.
@@ -386,31 +325,3 @@ func wsPingTestHandler(t *testing.T, conn *websocket.Conn, shutdown, sendPing <-
}
}
}
// severableReadWriteCloser wraps an io.ReadWriteCloser and provides a Sever() method to drop writes and read empty.
type severableReadWriteCloser struct {
io.ReadWriteCloser
severed int32 // atomic
}
func (s *severableReadWriteCloser) Sever() {
atomic.StoreInt32(&s.severed, 1)
}
func (s *severableReadWriteCloser) Read(p []byte) (n int, err error) {
if atomic.LoadInt32(&s.severed) > 0 {
return 0, nil
}
return s.ReadWriteCloser.Read(p)
}
func (s *severableReadWriteCloser) Write(p []byte) (n int, err error) {
if atomic.LoadInt32(&s.severed) > 0 {
return len(p), nil
}
return s.ReadWriteCloser.Write(p)
}
func (s *severableReadWriteCloser) Close() error {
return s.ReadWriteCloser.Close()
}