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.

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