github.com/franono/tendermint@v0.32.2-0.20200527150959-749313264ce9/rpc/jsonrpc/server/ws_handler.go (about) 1 package server 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "reflect" 9 "runtime/debug" 10 "time" 11 12 "github.com/gorilla/websocket" 13 14 amino "github.com/tendermint/go-amino" 15 16 "github.com/franono/tendermint/libs/log" 17 "github.com/franono/tendermint/libs/service" 18 types "github.com/franono/tendermint/rpc/jsonrpc/types" 19 ) 20 21 /////////////////////////////////////////////////////////////////////////////// 22 // WebSocket handler 23 /////////////////////////////////////////////////////////////////////////////// 24 25 const ( 26 defaultWSWriteChanCapacity = 1000 27 defaultWSWriteWait = 10 * time.Second 28 defaultWSReadWait = 30 * time.Second 29 defaultWSPingPeriod = (defaultWSReadWait * 9) / 10 30 ) 31 32 // WebsocketManager provides a WS handler for incoming connections and passes a 33 // map of functions along with any additional params to new connections. 34 // NOTE: The websocket path is defined externally, e.g. in node/node.go 35 type WebsocketManager struct { 36 websocket.Upgrader 37 38 funcMap map[string]*RPCFunc 39 cdc *amino.Codec 40 logger log.Logger 41 wsConnOptions []func(*wsConnection) 42 } 43 44 // NewWebsocketManager returns a new WebsocketManager that passes a map of 45 // functions, connection options and logger to new WS connections. 46 func NewWebsocketManager( 47 funcMap map[string]*RPCFunc, 48 cdc *amino.Codec, 49 wsConnOptions ...func(*wsConnection), 50 ) *WebsocketManager { 51 return &WebsocketManager{ 52 funcMap: funcMap, 53 cdc: cdc, 54 Upgrader: websocket.Upgrader{ 55 CheckOrigin: func(r *http.Request) bool { 56 // TODO ??? 57 // 58 // The default behaviour would be relevant to browser-based clients, 59 // afaik. I suppose having a pass-through is a workaround for allowing 60 // for more complex security schemes, shifting the burden of 61 // AuthN/AuthZ outside the Tendermint RPC. 62 // I can't think of other uses right now that would warrant a TODO 63 // though. The real backstory of this TODO shall remain shrouded in 64 // mystery 65 return true 66 }, 67 }, 68 logger: log.NewNopLogger(), 69 wsConnOptions: wsConnOptions, 70 } 71 } 72 73 // SetLogger sets the logger. 74 func (wm *WebsocketManager) SetLogger(l log.Logger) { 75 wm.logger = l 76 } 77 78 // WebsocketHandler upgrades the request/response (via http.Hijack) and starts 79 // the wsConnection. 80 func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Request) { 81 wsConn, err := wm.Upgrade(w, r, nil) 82 if err != nil { 83 // TODO - return http error 84 wm.logger.Error("Failed to upgrade connection", "err", err) 85 return 86 } 87 defer func() { 88 if err := wsConn.Close(); err != nil { 89 wm.logger.Error("Failed to close connection", "err", err) 90 } 91 }() 92 93 // register connection 94 con := newWSConnection(wsConn, wm.funcMap, wm.cdc, wm.wsConnOptions...) 95 con.SetLogger(wm.logger.With("remote", wsConn.RemoteAddr())) 96 wm.logger.Info("New websocket connection", "remote", con.remoteAddr) 97 err = con.Start() // BLOCKING 98 if err != nil { 99 wm.logger.Error("Failed to start connection", "err", err) 100 return 101 } 102 con.Stop() 103 } 104 105 /////////////////////////////////////////////////////////////////////////////// 106 // WebSocket connection 107 /////////////////////////////////////////////////////////////////////////////// 108 109 // A single websocket connection contains listener id, underlying ws 110 // connection, and the event switch for subscribing to events. 111 // 112 // In case of an error, the connection is stopped. 113 type wsConnection struct { 114 service.BaseService 115 116 remoteAddr string 117 baseConn *websocket.Conn 118 // writeChan is never closed, to allow WriteRPCResponse() to fail. 119 writeChan chan types.RPCResponse 120 121 // chan, which is closed when/if readRoutine errors 122 // used to abort writeRoutine 123 readRoutineQuit chan struct{} 124 125 funcMap map[string]*RPCFunc 126 cdc *amino.Codec 127 128 // write channel capacity 129 writeChanCapacity int 130 131 // each write times out after this. 132 writeWait time.Duration 133 134 // Connection times out if we haven't received *anything* in this long, not even pings. 135 readWait time.Duration 136 137 // Send pings to server with this period. Must be less than readWait, but greater than zero. 138 pingPeriod time.Duration 139 140 // Maximum message size. 141 readLimit int64 142 143 // callback which is called upon disconnect 144 onDisconnect func(remoteAddr string) 145 146 ctx context.Context 147 cancel context.CancelFunc 148 } 149 150 // NewWSConnection wraps websocket.Conn. 151 // 152 // See the commentary on the func(*wsConnection) functions for a detailed 153 // description of how to configure ping period and pong wait time. NOTE: if the 154 // write buffer is full, pongs may be dropped, which may cause clients to 155 // disconnect. see https://github.com/gorilla/websocket/issues/97 156 func newWSConnection( 157 baseConn *websocket.Conn, 158 funcMap map[string]*RPCFunc, 159 cdc *amino.Codec, 160 options ...func(*wsConnection), 161 ) *wsConnection { 162 wsc := &wsConnection{ 163 remoteAddr: baseConn.RemoteAddr().String(), 164 baseConn: baseConn, 165 funcMap: funcMap, 166 cdc: cdc, 167 writeWait: defaultWSWriteWait, 168 writeChanCapacity: defaultWSWriteChanCapacity, 169 readWait: defaultWSReadWait, 170 pingPeriod: defaultWSPingPeriod, 171 readRoutineQuit: make(chan struct{}), 172 } 173 for _, option := range options { 174 option(wsc) 175 } 176 wsc.baseConn.SetReadLimit(wsc.readLimit) 177 wsc.BaseService = *service.NewBaseService(nil, "wsConnection", wsc) 178 return wsc 179 } 180 181 // OnDisconnect sets a callback which is used upon disconnect - not 182 // Goroutine-safe. Nop by default. 183 func OnDisconnect(onDisconnect func(remoteAddr string)) func(*wsConnection) { 184 return func(wsc *wsConnection) { 185 wsc.onDisconnect = onDisconnect 186 } 187 } 188 189 // WriteWait sets the amount of time to wait before a websocket write times out. 190 // It should only be used in the constructor - not Goroutine-safe. 191 func WriteWait(writeWait time.Duration) func(*wsConnection) { 192 return func(wsc *wsConnection) { 193 wsc.writeWait = writeWait 194 } 195 } 196 197 // WriteChanCapacity sets the capacity of the websocket write channel. 198 // It should only be used in the constructor - not Goroutine-safe. 199 func WriteChanCapacity(cap int) func(*wsConnection) { 200 return func(wsc *wsConnection) { 201 wsc.writeChanCapacity = cap 202 } 203 } 204 205 // ReadWait sets the amount of time to wait before a websocket read times out. 206 // It should only be used in the constructor - not Goroutine-safe. 207 func ReadWait(readWait time.Duration) func(*wsConnection) { 208 return func(wsc *wsConnection) { 209 wsc.readWait = readWait 210 } 211 } 212 213 // PingPeriod sets the duration for sending websocket pings. 214 // It should only be used in the constructor - not Goroutine-safe. 215 func PingPeriod(pingPeriod time.Duration) func(*wsConnection) { 216 return func(wsc *wsConnection) { 217 wsc.pingPeriod = pingPeriod 218 } 219 } 220 221 // ReadLimit sets the maximum size for reading message. 222 // It should only be used in the constructor - not Goroutine-safe. 223 func ReadLimit(readLimit int64) func(*wsConnection) { 224 return func(wsc *wsConnection) { 225 wsc.readLimit = readLimit 226 } 227 } 228 229 // OnStart implements service.Service by starting the read and write routines. It 230 // blocks until there's some error. 231 func (wsc *wsConnection) OnStart() error { 232 wsc.writeChan = make(chan types.RPCResponse, wsc.writeChanCapacity) 233 234 // Read subscriptions/unsubscriptions to events 235 go wsc.readRoutine() 236 // Write responses, BLOCKING. 237 wsc.writeRoutine() 238 239 return nil 240 } 241 242 // OnStop implements service.Service by unsubscribing remoteAddr from all 243 // subscriptions. 244 func (wsc *wsConnection) OnStop() { 245 if wsc.onDisconnect != nil { 246 wsc.onDisconnect(wsc.remoteAddr) 247 } 248 249 if wsc.ctx != nil { 250 wsc.cancel() 251 } 252 } 253 254 // GetRemoteAddr returns the remote address of the underlying connection. 255 // It implements WSRPCConnection 256 func (wsc *wsConnection) GetRemoteAddr() string { 257 return wsc.remoteAddr 258 } 259 260 // WriteRPCResponse pushes a response to the writeChan, and blocks until it is accepted. 261 // It implements WSRPCConnection. It is Goroutine-safe. 262 func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { 263 select { 264 case <-wsc.Quit(): 265 return 266 case wsc.writeChan <- resp: 267 } 268 } 269 270 // TryWriteRPCResponse attempts to push a response to the writeChan, but does not block. 271 // It implements WSRPCConnection. It is Goroutine-safe 272 func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { 273 select { 274 case <-wsc.Quit(): 275 return false 276 case wsc.writeChan <- resp: 277 return true 278 default: 279 return false 280 } 281 } 282 283 // Codec returns an amino codec used to decode parameters and encode results. 284 // It implements WSRPCConnection. 285 func (wsc *wsConnection) Codec() *amino.Codec { 286 return wsc.cdc 287 } 288 289 // Context returns the connection's context. 290 // The context is canceled when the client's connection closes. 291 func (wsc *wsConnection) Context() context.Context { 292 if wsc.ctx != nil { 293 return wsc.ctx 294 } 295 wsc.ctx, wsc.cancel = context.WithCancel(context.Background()) 296 return wsc.ctx 297 } 298 299 // Read from the socket and subscribe to or unsubscribe from events 300 func (wsc *wsConnection) readRoutine() { 301 defer func() { 302 if r := recover(); r != nil { 303 err, ok := r.(error) 304 if !ok { 305 err = fmt.Errorf("WSJSONRPC: %v", r) 306 } 307 wsc.Logger.Error("Panic in WSJSONRPC handler", "err", err, "stack", string(debug.Stack())) 308 wsc.WriteRPCResponse(types.RPCInternalError(types.JSONRPCIntID(-1), err)) 309 go wsc.readRoutine() 310 } 311 }() 312 313 wsc.baseConn.SetPongHandler(func(m string) error { 314 return wsc.baseConn.SetReadDeadline(time.Now().Add(wsc.readWait)) 315 }) 316 317 for { 318 select { 319 case <-wsc.Quit(): 320 return 321 default: 322 // reset deadline for every type of message (control or data) 323 if err := wsc.baseConn.SetReadDeadline(time.Now().Add(wsc.readWait)); err != nil { 324 wsc.Logger.Error("failed to set read deadline", "err", err) 325 } 326 var in []byte 327 _, in, err := wsc.baseConn.ReadMessage() 328 if err != nil { 329 if websocket.IsCloseError(err, websocket.CloseNormalClosure) { 330 wsc.Logger.Info("Client closed the connection") 331 } else { 332 wsc.Logger.Error("Failed to read request", "err", err) 333 } 334 wsc.Stop() 335 close(wsc.readRoutineQuit) 336 return 337 } 338 339 var request types.RPCRequest 340 err = json.Unmarshal(in, &request) 341 if err != nil { 342 wsc.WriteRPCResponse(types.RPCParseError(fmt.Errorf("error unmarshaling request: %w", err))) 343 continue 344 } 345 346 // A Notification is a Request object without an "id" member. 347 // The Server MUST NOT reply to a Notification, including those that are within a batch request. 348 if request.ID == nil { 349 wsc.Logger.Debug( 350 "WSJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)", 351 "req", request, 352 ) 353 continue 354 } 355 356 // Now, fetch the RPCFunc and execute it. 357 rpcFunc := wsc.funcMap[request.Method] 358 if rpcFunc == nil { 359 wsc.WriteRPCResponse(types.RPCMethodNotFoundError(request.ID)) 360 continue 361 } 362 363 ctx := &types.Context{JSONReq: &request, WSConn: wsc} 364 args := []reflect.Value{reflect.ValueOf(ctx)} 365 if len(request.Params) > 0 { 366 fnArgs, err := jsonParamsToArgs(rpcFunc, wsc.cdc, request.Params) 367 if err != nil { 368 wsc.WriteRPCResponse( 369 types.RPCInternalError(request.ID, fmt.Errorf("error converting json params to arguments: %w", err)), 370 ) 371 continue 372 } 373 args = append(args, fnArgs...) 374 } 375 376 returns := rpcFunc.f.Call(args) 377 378 // TODO: Need to encode args/returns to string if we want to log them 379 wsc.Logger.Info("WSJSONRPC", "method", request.Method) 380 381 result, err := unreflectResult(returns) 382 if err != nil { 383 wsc.WriteRPCResponse(types.RPCInternalError(request.ID, err)) 384 continue 385 } 386 387 wsc.WriteRPCResponse(types.NewRPCSuccessResponse(wsc.cdc, request.ID, result)) 388 } 389 } 390 } 391 392 // receives on a write channel and writes out on the socket 393 func (wsc *wsConnection) writeRoutine() { 394 pingTicker := time.NewTicker(wsc.pingPeriod) 395 defer func() { 396 pingTicker.Stop() 397 }() 398 399 // https://github.com/gorilla/websocket/issues/97 400 pongs := make(chan string, 1) 401 wsc.baseConn.SetPingHandler(func(m string) error { 402 select { 403 case pongs <- m: 404 default: 405 } 406 return nil 407 }) 408 409 for { 410 select { 411 case <-wsc.Quit(): 412 return 413 case <-wsc.readRoutineQuit: // error in readRoutine 414 return 415 case m := <-pongs: 416 err := wsc.writeMessageWithDeadline(websocket.PongMessage, []byte(m)) 417 if err != nil { 418 wsc.Logger.Info("Failed to write pong (client may disconnect)", "err", err) 419 } 420 case <-pingTicker.C: 421 err := wsc.writeMessageWithDeadline(websocket.PingMessage, []byte{}) 422 if err != nil { 423 wsc.Logger.Error("Failed to write ping", "err", err) 424 return 425 } 426 case msg := <-wsc.writeChan: 427 jsonBytes, err := json.MarshalIndent(msg, "", " ") 428 if err != nil { 429 wsc.Logger.Error("Failed to marshal RPCResponse to JSON", "err", err) 430 } else if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { 431 wsc.Logger.Error("Failed to write response", "msg", msg, "err", err) 432 return 433 } 434 } 435 } 436 } 437 438 // All writes to the websocket must (re)set the write deadline. 439 // If some writes don't set it while others do, they may timeout incorrectly 440 // (https://github.com/franono/tendermint/issues/553) 441 func (wsc *wsConnection) writeMessageWithDeadline(msgType int, msg []byte) error { 442 if err := wsc.baseConn.SetWriteDeadline(time.Now().Add(wsc.writeWait)); err != nil { 443 return err 444 } 445 return wsc.baseConn.WriteMessage(msgType, msg) 446 }