You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

446 lines
13 KiB

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