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 }