// Copyright (c) 2014-2015 Conformal Systems LLC. // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package btcrpcclient import ( "bytes" "container/list" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "net" "net/http" "net/url" "sync" "sync/atomic" "time" "github.com/btcsuite/go-socks/socks" "github.com/btcsuite/websocket" "github.com/conformal/btcjson" "github.com/conformal/btcws" ) var ( // ErrInvalidAuth is an error to describe the condition where the client // is either unable to authenticate or the specified endpoint is // incorrect. ErrInvalidAuth = errors.New("authentication failure") // ErrInvalidEndpoint is an error to describe the condition where the // websocket handshake failed with the specified endpoint. ErrInvalidEndpoint = errors.New("the endpoint either does not support " + "websockets or does not exist") // ErrClientNotConnected is an error to describe the condition where a // websocket client has been created, but the connection was never // established. This condition differs from ErrClientDisconnect, which // represents an established connection that was lost. ErrClientNotConnected = errors.New("the client was never connected") // ErrClientDisconnect is an error to describe the condition where the // client has been disconnected from the RPC server. When the // DisableAutoReconnect option is not set, any outstanding futures // when a client disconnect occurs will return this error as will // any new requests. ErrClientDisconnect = errors.New("the client has been disconnected") // ErrClientShutdown is an error to describe the condition where the // client is either already shutdown, or in the process of shutting // down. Any outstanding futures when a client shutdown occurs will // return this error as will any new requests. ErrClientShutdown = errors.New("the client has been shutdown") // ErrNotWebsocketClient is an error to describe the condition of // calling a Client method intended for a websocket client when the // client has been configured to run in HTTP POST mode instead. ErrNotWebsocketClient = errors.New("client is not configured for " + "websockets") // ErrClientAlreadyConnected is an error to describe the condition where // a new client connection cannot be established due to a websocket // client having already connected to the RPC server. ErrClientAlreadyConnected = errors.New("websocket client has already " + "connected") ) const ( // sendBufferSize is the number of elements the websocket send channel // can queue before blocking. sendBufferSize = 50 // sendPostBufferSize is the number of elements the HTTP POST send // channel can queue before blocking. sendPostBufferSize = 100 // connectionRetryInterval is the amount of time to wait in between // retries when automatically reconnecting to an RPC server. connectionRetryInterval = time.Second * 5 ) // sendPostDetails houses an HTTP POST request to send to an RPC server as well // as the original JSON-RPC command and a channel to reply on when the server // responds with the result. type sendPostDetails struct { command btcjson.Cmd request *http.Request responseChan chan *response } // jsonRequest holds information about a json request that is used to properly // detect, interpret, and deliver a reply to it. type jsonRequest struct { cmd btcjson.Cmd responseChan chan *response } // Client represents a Bitcoin RPC client which allows easy access to the // various RPC methods available on a Bitcoin RPC server. Each of the wrapper // functions handle the details of converting the passed and return types to and // from the underlying JSON types which are required for the JSON-RPC // invocations // // The client provides each RPC in both synchronous (blocking) and asynchronous // (non-blocking) forms. The asynchronous forms are based on the concept of // futures where they return an instance of a type that promises to deliver the // result of the invocation at some future time. Invoking the Receive method on // the returned future will block until the result is available if it's not // already. type Client struct { id uint64 // atomic, so must stay 64-bit aligned // config holds the connection configuration assoiated with this client. config *ConnConfig // wsConn is the underlying websocket connection when not in HTTP POST // mode. wsConn *websocket.Conn // httpClient is the underlying HTTP client to use when running in HTTP // POST mode. httpClient *http.Client // mtx is a mutex to protect access to connection related fields. mtx sync.Mutex // disconnected indicated whether or not the server is disconnected. disconnected bool // retryCount holds the number of times the client has tried to // reconnect to the RPC server. retryCount int64 // Track command and their response channels by ID. requestLock sync.Mutex requestMap map[uint64]*list.Element requestList *list.List // Notifications. ntfnHandlers *NotificationHandlers ntfnState *notificationState // Networking infrastructure. sendChan chan []byte sendPostChan chan *sendPostDetails connEstablished chan struct{} disconnect chan struct{} shutdown chan struct{} wg sync.WaitGroup } // NextID returns the next id to be used when sending a JSON-RPC message. This // ID allows responses to be associated with particular requests per the // JSON-RPC specification. Typically the consumer of the client does not need // to call this function, however, if a custom request is being created and used // this function should be used to ensure the ID is unique amongst all requests // being made. func (c *Client) NextID() uint64 { return atomic.AddUint64(&c.id, 1) } // addRequest associates the passed jsonRequest with the passed id. This allows // the response from the remote server to be unmarshalled to the appropriate // type and sent to the specified channel when it is received. // // If the client has already begun shutting down, ErrClientShutdown is returned // and the request is not added. // // This function is safe for concurrent access. func (c *Client) addRequest(id uint64, request *jsonRequest) error { c.requestLock.Lock() defer c.requestLock.Unlock() // A non-blocking read of the shutdown channel with the request lock // held avoids adding the request to the client's internal data // structures if the client is in the process of shutting down (and // has not yet grabbed the request lock), or has finished shutdown // already (responding to each outstanding request with // ErrClientShutdown). select { case <-c.shutdown: return ErrClientShutdown default: } // TODO(davec): Already there? element := c.requestList.PushBack(request) c.requestMap[id] = element return nil } // removeRequest returns and removes the jsonRequest which contains the response // channel and original method associated with the passed id or nil if there is // no association. // // This function is safe for concurrent access. func (c *Client) removeRequest(id uint64) *jsonRequest { c.requestLock.Lock() defer c.requestLock.Unlock() element := c.requestMap[id] if element != nil { delete(c.requestMap, id) request := c.requestList.Remove(element).(*jsonRequest) return request } return nil } // removeAllRequests removes all the jsonRequests which contain the response // channels for outstanding requests. // // This function MUST be called with the request lock held. func (c *Client) removeAllRequests() { c.requestMap = make(map[uint64]*list.Element) c.requestList.Init() } // trackRegisteredNtfns examines the passed command to see if it is one of // the notification commands and updates the notification state that is used // to automatically re-establish registered notifications on reconnects. func (c *Client) trackRegisteredNtfns(cmd btcjson.Cmd) { // Nothing to do if the caller is not interested in notifications. if c.ntfnHandlers == nil { return } c.ntfnState.Lock() defer c.ntfnState.Unlock() switch bcmd := cmd.(type) { case *btcws.NotifyBlocksCmd: c.ntfnState.notifyBlocks = true case *btcws.NotifyNewTransactionsCmd: if bcmd.Verbose { c.ntfnState.notifyNewTxVerbose = true } else { c.ntfnState.notifyNewTx = true } case *btcws.NotifySpentCmd: for _, op := range bcmd.OutPoints { c.ntfnState.notifySpent[op] = struct{}{} } case *btcws.NotifyReceivedCmd: for _, addr := range bcmd.Addresses { c.ntfnState.notifyReceived[addr] = struct{}{} } } } type ( // inMessage is the first type that an incoming message is unmarshaled // into. It supports both requests (for notification support) and // responses. The partially-unmarshaled message is a notification if // the embedded ID (from the response) is nil. Otherwise, it is a // response. inMessage struct { ID *uint64 `json:"id"` *rawNotification *rawResponse } // rawNotification is a partially-unmarshaled JSON-RPC notification. rawNotification struct { Method string `json:"method"` Params []json.RawMessage `json:"params"` } // rawResponse is a partially-unmarshaled JSON-RPC response. For this // to be valid (according to JSON-RPC 1.0 spec), ID may not be nil. rawResponse struct { Result json.RawMessage `json:"result"` Error *btcjson.Error `json:"error"` } ) // response is the raw bytes of a JSON-RPC result, or the error if the response // error object was non-null. type response struct { result []byte err error } // result checks whether the unmarshaled response contains a non-nil error, // returning an unmarshaled btcjson.Error (or an unmarshaling error) if so. // If the response is not an error, the raw bytes of the request are // returned for further unmashaling into specific result types. func (r rawResponse) result() (result []byte, err error) { if r.Error != nil { return nil, r.Error } return r.Result, nil } // handleMessage is the main handler for incoming notifications and responses. func (c *Client) handleMessage(msg []byte) { // Attempt to unmarshal the message as either a notifiation or response. var in inMessage err := json.Unmarshal(msg, &in) if err != nil { log.Warnf("Remote server sent invalid message: %v", err) return } // JSON-RPC 1.0 notifications are requests with a null id. if in.ID == nil { ntfn := in.rawNotification if ntfn == nil { log.Warn("Malformed notification: missing " + "method and parameters") return } if ntfn.Method == "" { log.Warn("Malformed notification: missing method") return } // params are not optional: nil isn't valid (but len == 0 is) if ntfn.Params == nil { log.Warn("Malformed notification: missing params") return } // Deliver the notification. log.Tracef("Received notification [%s]", in.Method) c.handleNotification(in.rawNotification) return } if in.rawResponse == nil { log.Warn("Malformed response: missing result and error") return } id := *in.ID log.Tracef("Received response for id %d (result %s)", id, in.Result) request := c.removeRequest(id) // Nothing more to do if there is no request associated with this reply. if request == nil || request.responseChan == nil { log.Warnf("Received unexpected reply: %s (id %d)", in.Result, id) return } // Since the command was successful, examine it to see if it's a // notification, and if is, add it to the notification state so it // can automatically be re-established on reconnect. c.trackRegisteredNtfns(request.cmd) // Deliver the response. result, err := in.rawResponse.result() request.responseChan <- &response{result: result, err: err} } // wsInHandler handles all incoming messages for the websocket connection // associated with the client. It must be run as a goroutine. func (c *Client) wsInHandler() { out: for { // Break out of the loop once the shutdown channel has been // closed. Use a non-blocking select here so we fall through // otherwise. select { case <-c.shutdown: break out default: } _, msg, err := c.wsConn.ReadMessage() if err != nil { // Log the error if it's not due to disconnecting. if _, ok := err.(*net.OpError); !ok { log.Errorf("Websocket receive error from "+ "%s: %v", c.config.Host, err) } break out } c.handleMessage(msg) } // Ensure the connection is closed. c.Disconnect() c.wg.Done() log.Tracef("RPC client input handler done for %s", c.config.Host) } // wsOutHandler handles all outgoing messages for the websocket connection. It // uses a buffered channel to serialize output messages while allowing the // sender to continue running asynchronously. It must be run as a goroutine. func (c *Client) wsOutHandler() { out: for { // Send any messages ready for send until the client is // disconnected closed. select { case msg := <-c.sendChan: err := c.wsConn.WriteMessage(websocket.TextMessage, msg) if err != nil { c.Disconnect() break out } case <-c.disconnect: break out } } // Drain any channels before exiting so nothing is left waiting around // to send. cleanup: for { select { case <-c.sendChan: default: break cleanup } } c.wg.Done() log.Tracef("RPC client output handler done for %s", c.config.Host) } // sendMessage sends the passed JSON to the connected server using the // websocket connection. It is backed by a buffered channel, so it will not // block until the send channel is full. func (c *Client) sendMessage(marshalledJSON []byte) { // Don't send the message if disconnected. select { case c.sendChan <- marshalledJSON: case <-c.disconnect: return } } // reregisterNtfns creates and sends commands needed to re-establish the current // notification state associated with the client. It should only be called on // on reconnect by the resendCmds function. func (c *Client) reregisterNtfns() error { // Nothing to do if the caller is not interested in notifications. if c.ntfnHandlers == nil { return nil } // In order to avoid holding the lock on the notification state for the // entire time of the potentially long running RPCs issued below, make a // copy of it and work from that. // // Also, other commands will be running concurrently which could modify // the notification state (while not under the lock of course) which // also register it with the remote RPC server, so this prevents double // registrations. stateCopy := c.ntfnState.Copy() // Reregister notifyblocks if needed. if stateCopy.notifyBlocks { log.Debugf("Reregistering [notifyblocks]") if err := c.NotifyBlocks(); err != nil { return err } } // Reregister notifynewtransactions if needed. if stateCopy.notifyNewTx || stateCopy.notifyNewTxVerbose { log.Debugf("Reregistering [notifynewtransactions] (verbose=%v)", stateCopy.notifyNewTxVerbose) err := c.NotifyNewTransactions(stateCopy.notifyNewTxVerbose) if err != nil { return err } } // Reregister the combination of all previously registered notifyspent // outpoints in one command if needed. nslen := len(stateCopy.notifySpent) if nslen > 0 { outpoints := make([]btcws.OutPoint, 0, nslen) for op := range stateCopy.notifySpent { outpoints = append(outpoints, op) } log.Debugf("Reregistering [notifyspent] outpoints: %v", outpoints) if err := c.notifySpentInternal(outpoints).Receive(); err != nil { return err } } // Reregister the combination of all previously registered // notifyreceived addresses in one command if needed. nrlen := len(stateCopy.notifyReceived) if nrlen > 0 { addresses := make([]string, 0, nrlen) for addr := range stateCopy.notifyReceived { addresses = append(addresses, addr) } log.Debugf("Reregistering [notifyreceived] addresses: %v", addresses) if err := c.notifyReceivedInternal(addresses).Receive(); err != nil { return err } } return nil } // ignoreResends is a set of all methods for requests that are "long running" // are not be reissued by the client on reconnect. var ignoreResends = map[string]struct{}{ "rescan": struct{}{}, } // resendCmds resends any commands that had not completed when the client // disconnected. It is intended to be called once the client has reconnected as // a separate goroutine. func (c *Client) resendCmds() { // Set the notification state back up. If anything goes wrong, // disconnect the client. if err := c.reregisterNtfns(); err != nil { log.Warnf("Unable to re-establish notification state: %v", err) c.Disconnect() return } // Since it's possible to block on send and more commands might be // added by the caller while resending, make a copy of all of the // commands that need to be resent now and work from the copy. This // also allows the lock to be released quickly. c.requestLock.Lock() resendCmds := make([]*jsonRequest, 0, c.requestList.Len()) var nextElem *list.Element for e := c.requestList.Front(); e != nil; e = nextElem { nextElem = e.Next() req := e.Value.(*jsonRequest) if _, ok := ignoreResends[req.cmd.Method()]; ok { // If a request is not sent on reconnect, remove it // from the request structures, since no reply is // expected. delete(c.requestMap, req.cmd.Id().(uint64)) c.requestList.Remove(e) } else { resendCmds = append(resendCmds, req) } } c.requestLock.Unlock() for _, req := range resendCmds { // Stop resending commands if the client disconnected again // since the next reconnect will handle them. if c.Disconnected() { return } c.marshalAndSend(req.cmd, req.responseChan) } } // wsReconnectHandler listens for client disconnects and automatically tries // to reconnect with retry interval that scales based on the number of retries. // It also resends any commands that had not completed when the client // disconnected so the disconnect/reconnect process is largely transparent to // the caller. This function is not run when the DisableAutoReconnect config // options is set. // // This function must be run as a goroutine. func (c *Client) wsReconnectHandler() { out: for { select { case <-c.disconnect: // On disconnect, fallthrough to reestablish the // connection. case <-c.shutdown: break out } reconnect: for { select { case <-c.shutdown: break out default: } wsConn, err := dial(c.config) if err != nil { c.retryCount++ log.Infof("Failed to connect to %s: %v", c.config.Host, err) // Scale the retry interval by the number of // retries so there is a backoff up to a max // of 1 minute. scaledInterval := connectionRetryInterval.Nanoseconds() * c.retryCount scaledDuration := time.Duration(scaledInterval) if scaledDuration > time.Minute { scaledDuration = time.Minute } log.Infof("Retrying connection to %s in "+ "%s", c.config.Host, scaledDuration) time.Sleep(scaledDuration) continue reconnect } log.Infof("Reestablished connection to RPC server %s", c.config.Host) // Reset the connection state and signal the reconnect // has happened. c.wsConn = wsConn c.retryCount = 0 c.disconnect = make(chan struct{}) c.mtx.Lock() c.disconnected = false c.mtx.Unlock() // Start processing input and output for the // new connection. c.start() // Reissue pending commands in another goroutine since // the send can block. go c.resendCmds() // Break out of the reconnect loop back to wait for // disconnect again. break reconnect } } c.wg.Done() log.Tracef("RPC client reconnect handler done for %s", c.config.Host) } // handleSendPostMessage handles performing the passed HTTP request, reading the // result, unmarshalling it, and delivering the unmarhsalled result to the // provided response channel. func (c *Client) handleSendPostMessage(details *sendPostDetails) { // Post the request. cmd := details.command log.Tracef("Sending command [%s] with id %d", cmd.Method(), cmd.Id()) httpResponse, err := c.httpClient.Do(details.request) if err != nil { details.responseChan <- &response{err: err} return } // Read the raw bytes and close the response. respBytes, err := btcjson.GetRaw(httpResponse.Body) if err != nil { details.responseChan <- &response{err: err} return } // Handle unsuccessful HTTP responses if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 { details.responseChan <- &response{err: errors.New(string(respBytes))} return } var resp rawResponse err = json.Unmarshal(respBytes, &resp) if err != nil { details.responseChan <- &response{err: err} return } res, err := resp.result() details.responseChan <- &response{result: res, err: err} } // sendPostHandler handles all outgoing messages when the client is running // in HTTP POST mode. It uses a buffered channel to serialize output messages // while allowing the sender to continue running asynchronously. It must be run // as a goroutine. func (c *Client) sendPostHandler() { out: for { // Send any messages ready for send until the shutdown channel // is closed. select { case details := <-c.sendPostChan: c.handleSendPostMessage(details) case <-c.shutdown: break out } } // Drain any wait channels before exiting so nothing is left waiting // around to send. cleanup: for { select { case details := <-c.sendPostChan: details.responseChan <- &response{ result: nil, err: ErrClientShutdown, } default: break cleanup } } c.wg.Done() log.Tracef("RPC client send handler done for %s", c.config.Host) } // sendPostRequest sends the passed HTTP request to the RPC server using the // HTTP client associated with the client. It is backed by a buffered channel, // so it will not block until the send channel is full. func (c *Client) sendPostRequest(req *http.Request, command btcjson.Cmd, responseChan chan *response) { // Don't send the message if shutting down. select { case <-c.shutdown: responseChan <- &response{result: nil, err: ErrClientShutdown} default: } c.sendPostChan <- &sendPostDetails{ command: command, request: req, responseChan: responseChan, } } // newFutureError returns a new future result channel that already has the // passed error waitin on the channel with the reply set to nil. This is useful // to easily return errors from the various Async functions. func newFutureError(err error) chan *response { responseChan := make(chan *response, 1) responseChan <- &response{err: err} return responseChan } // receiveFuture receives from the passed futureResult channel to extract a // reply or any errors. The examined errors include an error in the // futureResult and the error in the reply from the server. This will block // until the result is available on the passed channel. func receiveFuture(f chan *response) ([]byte, error) { // Wait for a response on the returned channel. r := <-f return r.result, r.err } // marshalAndSendPost marshals the passed command to JSON-RPC and sends it to // the server by issuing an HTTP POST request and returns a response channel // on which the reply will be delivered. Typically a new connection is opened // and closed for each command when using this method, however, the underlying // HTTP client might coalesce multiple commands depending on several factors // including the remote server configuration. func (c *Client) marshalAndSendPost(cmd btcjson.Cmd, responseChan chan *response) { marshalledJSON, err := json.Marshal(cmd) if err != nil { responseChan <- &response{result: nil, err: err} return } // Generate a request to the configured RPC server. protocol := "http" if !c.config.DisableTLS { protocol = "https" } url := protocol + "://" + c.config.Host req, err := http.NewRequest("POST", url, bytes.NewReader(marshalledJSON)) if err != nil { responseChan <- &response{result: nil, err: err} return } req.Close = true req.Header.Set("Content-Type", "application/json") // Configure basic access authorization. req.SetBasicAuth(c.config.User, c.config.Pass) log.Tracef("Sending command [%s] with id %d", cmd.Method(), cmd.Id()) c.sendPostRequest(req, cmd, responseChan) } // marshalAndSend marshals the passed command to JSON-RPC and sends it to the // server. It returns a response channel on which the reply will be delivered. func (c *Client) marshalAndSend(cmd btcjson.Cmd, responseChan chan *response) { marshalledJSON, err := cmd.MarshalJSON() if err != nil { responseChan <- &response{result: nil, err: err} return } log.Tracef("Sending command [%s] with id %d", cmd.Method(), cmd.Id()) c.sendMessage(marshalledJSON) } // sendCmd sends the passed command to the associated server and returns a // response channel on which the reply will be deliver at some point in the // future. It handles both websocket and HTTP POST mode depending on the // configuration of the client. func (c *Client) sendCmd(cmd btcjson.Cmd) chan *response { // Choose which marshal and send function to use depending on whether // the client running in HTTP POST mode or not. When running in HTTP // POST mode, the command is issued via an HTTP client. Otherwise, // the command is issued via the asynchronous websocket channels. responseChan := make(chan *response, 1) if c.config.HttpPostMode { c.marshalAndSendPost(cmd, responseChan) return responseChan } // Check whether the websocket connection has never been established, // in which case the handler goroutines are not running. select { case <-c.connEstablished: default: responseChan <- &response{err: ErrClientNotConnected} return responseChan } err := c.addRequest(cmd.Id().(uint64), &jsonRequest{ cmd: cmd, responseChan: responseChan, }) if err != nil { responseChan <- &response{err: err} return responseChan } c.marshalAndSend(cmd, responseChan) return responseChan } // sendCmdAndWait sends the passed command to the associated server, waits // for the reply, and returns the result from it. It will return the error // field in the reply if there is one. func (c *Client) sendCmdAndWait(cmd btcjson.Cmd) (interface{}, error) { // Marshal the command to JSON-RPC, send it to the connected server, and // wait for a response on the returned channel. return receiveFuture(c.sendCmd(cmd)) } // Disconnected returns whether or not the server is disconnected. If a // websocket client was created but never connected, this also returns false. func (c *Client) Disconnected() bool { c.mtx.Lock() defer c.mtx.Unlock() select { case <-c.connEstablished: return c.disconnected default: return false } } // doDisconnect disconnects the websocket associated with the client if it // hasn't already been disconnected. It will return false if the disconnect is // not needed or the client is running in HTTP POST mode. // // This function is safe for concurrent access. func (c *Client) doDisconnect() bool { if c.config.HttpPostMode { return false } c.mtx.Lock() defer c.mtx.Unlock() // Nothing to do if already disconnected. if c.disconnected { return false } log.Tracef("Disconnecting RPC client %s", c.config.Host) close(c.disconnect) if c.wsConn != nil { c.wsConn.Close() } c.disconnected = true return true } // doShutdown closes the shutdown channel and logs the shutdown unless shutdown // is already in progress. It will return false if the shutdown is not needed. // // This function is safe for concurrent access. func (c *Client) doShutdown() bool { // Ignore the shutdown request if the client is already in the process // of shutting down or already shutdown. select { case <-c.shutdown: return false default: } log.Tracef("Shutting down RPC client %s", c.config.Host) close(c.shutdown) return true } // Disconnect disconnects the current websocket associated with the client. The // connection will automatically be re-established unless the client was // created with the DisableAutoReconnect flag. // // This function has no effect when the client is running in HTTP POST mode. func (c *Client) Disconnect() { // Nothing to do if already disconnected or running in HTTP POST mode. if !c.doDisconnect() { return } c.requestLock.Lock() defer c.requestLock.Unlock() // When operating without auto reconnect, send errors to any pending // requests and shutdown the client. if c.config.DisableAutoReconnect { for e := c.requestList.Front(); e != nil; e = e.Next() { req := e.Value.(*jsonRequest) req.responseChan <- &response{ result: nil, err: ErrClientDisconnect, } } c.removeAllRequests() c.doShutdown() } } // Shutdown shuts down the client by disconnecting any connections associated // with the client and, when automatic reconnect is enabled, preventing future // attempts to reconnect. It also stops all goroutines. func (c *Client) Shutdown() { // Do the shutdown under the request lock to prevent clients from // adding new requests while the client shutdown process is initiated. c.requestLock.Lock() defer c.requestLock.Unlock() // Ignore the shutdown request if the client is already in the process // of shutting down or already shutdown. if !c.doShutdown() { return } // Send the ErrClientShutdown error to any pending requests. for e := c.requestList.Front(); e != nil; e = e.Next() { req := e.Value.(*jsonRequest) req.responseChan <- &response{ result: nil, err: ErrClientShutdown, } } c.removeAllRequests() // Disconnect the client if needed. c.doDisconnect() } // start begins processing input and output messages. func (c *Client) start() { log.Tracef("Starting RPC client %s", c.config.Host) // Start the I/O processing handlers depending on whether the client is // in HTTP POST mode or the default websocket mode. if c.config.HttpPostMode { c.wg.Add(1) go c.sendPostHandler() } else { c.wg.Add(3) go func() { if c.ntfnHandlers != nil { if c.ntfnHandlers.OnClientConnected != nil { c.ntfnHandlers.OnClientConnected() } } c.wg.Done() }() go c.wsInHandler() go c.wsOutHandler() } } // WaitForShutdown blocks until the client goroutines are stopped and the // connection is closed. func (c *Client) WaitForShutdown() { c.wg.Wait() } // ConnConfig describes the connection configuration parameters for the client. // This type ConnConfig struct { // Host is the IP address and port of the RPC server you want to connect // to. Host string // Endpoint is the websocket endpoint on the RPC server. This is // typically "ws". Endpoint string // User is the username to use to authenticate to the RPC server. User string // Pass is the passphrase to use to authenticate to the RPC server. Pass string // DisableTLS specifies whether transport layer security should be // disabled. It is recommended to always use TLS if the RPC server // supports it as otherwise your username and password is sent across // the wire in cleartext. DisableTLS bool // Certificates are the bytes for a PEM-encoded certificate chain used // for the TLS connection. It has no effect if the DisableTLS parameter // is true. Certificates []byte // Proxy specifies to connect through a SOCKS 5 proxy server. It may // be an empty string if a proxy is not required. Proxy string // ProxyUser is an optional username to use for the proxy server if it // requires authentication. It has no effect if the Proxy parameter // is not set. ProxyUser string // ProxyPass is an optional password to use for the proxy server if it // requires authentication. It has no effect if the Proxy parameter // is not set. ProxyPass string // DisableAutoReconnect specifies the client should not automatically // try to reconnect to the server when it has been disconnected. DisableAutoReconnect bool // DisableConnectOnNew specifies that a websocket client connection // should not be tried when creating the client with New. Instead, the // client is created and returned unconnected, and Connect must be // called manually. DisableConnectOnNew bool // HttpPostMode instructs the client to run using multiple independent // connections issuing HTTP POST requests instead of using the default // of websockets. Websockets are generally preferred as some of the // features of the client such notifications only work with websockets, // however, not all servers support the websocket extensions, so this // flag can be set to true to use basic HTTP POST requests instead. HttpPostMode bool // EnableBCInfoHacks is an option provided to enable compatiblity hacks // when connecting to blockchain.info RPC server EnableBCInfoHacks bool } // newHTTPClient returns a new http client that is configured according to the // proxy and TLS settings in the associated connection configuration. func newHTTPClient(config *ConnConfig) (*http.Client, error) { // Set proxy function if there is a proxy configured. var proxyFunc func(*http.Request) (*url.URL, error) if config.Proxy != "" { proxyURL, err := url.Parse(config.Proxy) if err != nil { return nil, err } proxyFunc = http.ProxyURL(proxyURL) } // Configure TLS if needed. var tlsConfig *tls.Config if !config.DisableTLS { if len(config.Certificates) > 0 { pool := x509.NewCertPool() pool.AppendCertsFromPEM(config.Certificates) tlsConfig = &tls.Config{ RootCAs: pool, } } } client := http.Client{ Transport: &http.Transport{ Proxy: proxyFunc, TLSClientConfig: tlsConfig, }, } return &client, nil } // dial opens a websocket connection using the passed connection configuration // details. func dial(config *ConnConfig) (*websocket.Conn, error) { // Setup TLS if not disabled. var tlsConfig *tls.Config var scheme = "ws" if !config.DisableTLS { tlsConfig = &tls.Config{ MinVersion: tls.VersionTLS12, } if len(config.Certificates) > 0 { pool := x509.NewCertPool() pool.AppendCertsFromPEM(config.Certificates) tlsConfig.RootCAs = pool } scheme = "wss" } // Create a websocket dialer that will be used to make the connection. // It is modified by the proxy setting below as needed. dialer := websocket.Dialer{TLSClientConfig: tlsConfig} // Setup the proxy if one is configured. if config.Proxy != "" { proxy := &socks.Proxy{ Addr: config.Proxy, Username: config.ProxyUser, Password: config.ProxyPass, } dialer.NetDial = proxy.Dial } // The RPC server requires basic authorization, so create a custom // request header with the Authorization header set. login := config.User + ":" + config.Pass auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) requestHeader := make(http.Header) requestHeader.Add("Authorization", auth) // Dial the connection. url := fmt.Sprintf("%s://%s/%s", scheme, config.Host, config.Endpoint) wsConn, resp, err := dialer.Dial(url, requestHeader) if err != nil { if err != websocket.ErrBadHandshake || resp == nil { return nil, err } // Detect HTTP authentication error status codes. if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { return nil, ErrInvalidAuth } // The connection was authenticated and the status response was // ok, but the websocket handshake still failed, so the endpoint // is invalid in some way. if resp.StatusCode == http.StatusOK { return nil, ErrInvalidEndpoint } // Return the status text from the server if none of the special // cases above apply. return nil, errors.New(resp.Status) } return wsConn, nil } // New creates a new RPC client based on the provided connection configuration // details. The notification handlers parameter may be nil if you are not // interested in receiving notifications and will be ignored when if the // configuration is set to run in HTTP POST mode. func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error) { // Either open a websocket connection or create an HTTP client depending // on the HTTP POST mode. Also, set the notification handlers to nil // when running in HTTP POST mode. var wsConn *websocket.Conn var httpClient *http.Client connEstablished := make(chan struct{}) var start bool if config.HttpPostMode { ntfnHandlers = nil start = true var err error httpClient, err = newHTTPClient(config) if err != nil { return nil, err } } else { if !config.DisableConnectOnNew { var err error wsConn, err = dial(config) if err != nil { return nil, err } start = true } } log.Infof("Established connection to RPC server %s", config.Host) client := &Client{ config: config, wsConn: wsConn, httpClient: httpClient, requestMap: make(map[uint64]*list.Element), requestList: list.New(), ntfnHandlers: ntfnHandlers, ntfnState: newNotificationState(), sendChan: make(chan []byte, sendBufferSize), sendPostChan: make(chan *sendPostDetails, sendPostBufferSize), connEstablished: connEstablished, disconnect: make(chan struct{}), shutdown: make(chan struct{}), } if start { close(connEstablished) client.start() if !client.config.HttpPostMode && !client.config.DisableAutoReconnect { client.wg.Add(1) go client.wsReconnectHandler() } } return client, nil } // Connect establishes the initial websocket connection. This is necessary when // a client was created after setting the DisableConnectOnNew field of the // Config struct. // // Up to tries number of connections (each after an increasing backoff) will // be tried if the connection can not be established. The special value of 0 // indicates an unlimited number of connection attempts. // // This method will error if the client is not configured for websockets, if the // connection has already been established, or if none of the connection // attempts were successful. func (c *Client) Connect(tries int) error { c.mtx.Lock() defer c.mtx.Unlock() if c.config.HttpPostMode { return ErrNotWebsocketClient } if c.wsConn != nil { return ErrClientAlreadyConnected } // Begin connection attempts. Increase the backoff after each failed // attempt, up to a maximum of one minute. var err error var backoff time.Duration for i := 0; tries == 0 || i < tries; i++ { var wsConn *websocket.Conn wsConn, err = dial(c.config) if err != nil { backoff = connectionRetryInterval * time.Duration(i+1) if backoff > time.Minute { backoff = time.Minute } time.Sleep(backoff) continue } // Connection was established. Set the websocket connection // member of the client and start the goroutines necessary // to run the client. c.wsConn = wsConn close(c.connEstablished) c.start() if !c.config.DisableAutoReconnect { c.wg.Add(1) go c.wsReconnectHandler() } return nil } // All connection attempts failed, so return the last error. return err }