github.com/0xsequence/ethkit@v1.25.0/go-ethereum/rpc/websocket.go (about)

     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 <http://www.gnu.org/licenses/>.
    16  
    17  package rpc
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"net/http"
    23  	"net/url"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/gorilla/websocket"
    28  )
    29  
    30  const (
    31  	wsReadBuffer       = 1024
    32  	wsWriteBuffer      = 1024
    33  	wsPingInterval     = 60 * time.Second
    34  	wsPingWriteTimeout = 5 * time.Second
    35  	wsPongTimeout      = 30 * time.Second
    36  	wsMessageSizeLimit = 15 * 1024 * 1024
    37  )
    38  
    39  var wsBufferPool = new(sync.Pool)
    40  
    41  type wsHandshakeError struct {
    42  	err    error
    43  	status string
    44  }
    45  
    46  func (e wsHandshakeError) Error() string {
    47  	s := e.err.Error()
    48  	if e.status != "" {
    49  		s += " (HTTP status " + e.status + ")"
    50  	}
    51  	return s
    52  }
    53  
    54  // DialWebsocketWithDialer creates a new RPC client that communicates with a JSON-RPC server
    55  // that is listening on the given endpoint using the provided dialer.
    56  func DialWebsocketWithDialer(ctx context.Context, endpoint, origin string, dialer websocket.Dialer) (*Client, error) {
    57  	endpoint, header, err := wsClientHeaders(endpoint, origin)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
    62  		conn, resp, err := dialer.DialContext(ctx, endpoint, header)
    63  		if err != nil {
    64  			hErr := wsHandshakeError{err: err}
    65  			if resp != nil {
    66  				hErr.status = resp.Status
    67  			}
    68  			return nil, hErr
    69  		}
    70  		return newWebsocketCodec(conn, endpoint, header), nil
    71  	})
    72  }
    73  
    74  // DialWebsocket creates a new RPC client that communicates with a JSON-RPC server
    75  // that is listening on the given endpoint.
    76  //
    77  // The context is used for the initial connection establishment. It does not
    78  // affect subsequent interactions with the client.
    79  func DialWebsocket(ctx context.Context, endpoint, origin string) (*Client, error) {
    80  	dialer := websocket.Dialer{
    81  		ReadBufferSize:  wsReadBuffer,
    82  		WriteBufferSize: wsWriteBuffer,
    83  		WriteBufferPool: wsBufferPool,
    84  	}
    85  	return DialWebsocketWithDialer(ctx, endpoint, origin, dialer)
    86  }
    87  
    88  func wsClientHeaders(endpoint, origin string) (string, http.Header, error) {
    89  	endpointURL, err := url.Parse(endpoint)
    90  	if err != nil {
    91  		return endpoint, nil, err
    92  	}
    93  	header := make(http.Header)
    94  	if origin != "" {
    95  		header.Add("origin", origin)
    96  	}
    97  	if endpointURL.User != nil {
    98  		b64auth := base64.StdEncoding.EncodeToString([]byte(endpointURL.User.String()))
    99  		header.Add("authorization", "Basic "+b64auth)
   100  		endpointURL.User = nil
   101  	}
   102  	return endpointURL.String(), header, nil
   103  }
   104  
   105  type websocketCodec struct {
   106  	*jsonCodec
   107  	conn *websocket.Conn
   108  	info PeerInfo
   109  
   110  	wg        sync.WaitGroup
   111  	pingReset chan struct{}
   112  }
   113  
   114  func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header) ServerCodec {
   115  	conn.SetReadLimit(wsMessageSizeLimit)
   116  	conn.SetPongHandler(func(appData string) error {
   117  		conn.SetReadDeadline(time.Time{})
   118  		return nil
   119  	})
   120  	wc := &websocketCodec{
   121  		jsonCodec: NewFuncCodec(conn, conn.WriteJSON, conn.ReadJSON).(*jsonCodec),
   122  		conn:      conn,
   123  		pingReset: make(chan struct{}, 1),
   124  		info: PeerInfo{
   125  			Transport:  "ws",
   126  			RemoteAddr: conn.RemoteAddr().String(),
   127  		},
   128  	}
   129  	// Fill in connection details.
   130  	wc.info.HTTP.Host = host
   131  	wc.info.HTTP.Origin = req.Get("Origin")
   132  	wc.info.HTTP.UserAgent = req.Get("User-Agent")
   133  	// Start pinger.
   134  	wc.wg.Add(1)
   135  	go wc.pingLoop()
   136  	return wc
   137  }
   138  
   139  func (wc *websocketCodec) close() {
   140  	wc.jsonCodec.close()
   141  	wc.wg.Wait()
   142  }
   143  
   144  func (wc *websocketCodec) peerInfo() PeerInfo {
   145  	return wc.info
   146  }
   147  
   148  func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}) error {
   149  	err := wc.jsonCodec.writeJSON(ctx, v)
   150  	if err == nil {
   151  		// Notify pingLoop to delay the next idle ping.
   152  		select {
   153  		case wc.pingReset <- struct{}{}:
   154  		default:
   155  		}
   156  	}
   157  	return err
   158  }
   159  
   160  // pingLoop sends periodic ping frames when the connection is idle.
   161  func (wc *websocketCodec) pingLoop() {
   162  	var timer = time.NewTimer(wsPingInterval)
   163  	defer wc.wg.Done()
   164  	defer timer.Stop()
   165  
   166  	for {
   167  		select {
   168  		case <-wc.closed():
   169  			return
   170  		case <-wc.pingReset:
   171  			if !timer.Stop() {
   172  				<-timer.C
   173  			}
   174  			timer.Reset(wsPingInterval)
   175  		case <-timer.C:
   176  			wc.jsonCodec.encMu.Lock()
   177  			wc.conn.SetWriteDeadline(time.Now().Add(wsPingWriteTimeout))
   178  			wc.conn.WriteMessage(websocket.PingMessage, nil)
   179  			wc.conn.SetReadDeadline(time.Now().Add(wsPongTimeout))
   180  			wc.jsonCodec.encMu.Unlock()
   181  			timer.Reset(wsPingInterval)
   182  		}
   183  	}
   184  }