From 8700eeaeb6d95554be8b4edb9d8bb0a514b4ca36 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 9 May 2014 19:26:56 -0500 Subject: [PATCH] Convert to use gorilla websockets package. Also, since the new package exposes more connection related error information, add a new ErrInvalidEndpoint which is returned if the specified enpoint does not appear to be a valid websocket provider and only return the ErrInvalidAuth error when HTTP authorization failure status codes are detected. Closes #1. --- infrastructure.go | 96 ++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/infrastructure.go b/infrastructure.go index db404286a..af0af9d84 100644 --- a/infrastructure.go +++ b/infrastructure.go @@ -6,7 +6,6 @@ package btcrpcclient import ( "bytes" - "code.google.com/p/go.net/websocket" "container/list" "crypto/tls" "crypto/x509" @@ -16,6 +15,7 @@ import ( "fmt" "github.com/conformal/btcjson" "github.com/conformal/go-socks" + "github.com/gorilla/websocket" "net" "net/http" "net/url" @@ -30,6 +30,11 @@ var ( // 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") + // 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 @@ -193,9 +198,9 @@ func (c *Client) removeAllRequests() { // (including pass through for standard RPC commands), sends the appropriate // response. It also detects commands which are marked as long-running and // sends them off to the asyncHander for processing. -func (c *Client) handleMessage(msg string) { +func (c *Client) handleMessage(msg []byte) { // Attempt to unmarshal the message as a known JSON-RPC command. - if cmd, err := btcjson.ParseMarshaledCmd([]byte(msg)); err == nil { + if cmd, err := btcjson.ParseMarshaledCmd(msg); err == nil { // Commands that have an ID associated with them are not // notifications. Since this is a client, it should not // be receiving non-notifications. @@ -271,8 +276,8 @@ out: default: } - var msg string - if err := websocket.Message.Receive(c.wsConn, &msg); err != nil { + _, 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 "+ @@ -299,7 +304,7 @@ out: // disconnected closed. select { case msg := <-c.sendChan: - err := websocket.Message.Send(c.wsConn, string(msg)) + err := c.wsConn.WriteMessage(websocket.TextMessage, msg) if err != nil { c.Disconnect() break out @@ -832,63 +837,62 @@ func newHttpClient(config *ConnConfig) (*http.Client, error) { // dial opens a websocket connection using the passed connection configuration // details. func dial(config *ConnConfig) (*websocket.Conn, error) { - // Connect to websocket. - url := fmt.Sprintf("wss://%s/%s", config.Host, config.Endpoint) - wsConfig, err := websocket.NewConfig(url, "https://localhost/") - if err != nil { - return nil, err + // Setup TLS if not disabled. + var tlsConfig *tls.Config + var scheme = "ws" + if !config.DisableTLS { + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(config.Certificates) + tlsConfig = &tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + } + scheme = "wss" } - pool := x509.NewCertPool() - pool.AppendCertsFromPEM(config.Certificates) - wsConfig.TlsConfig = &tls.Config{ - RootCAs: pool, - MinVersion: tls.VersionTLS12, - } + // 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} - // The wallet requires basic authorization, so use a custom config with - // with the Authorization header set. - login := config.User + ":" + config.Pass - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) - wsConfig.Header.Add("Authorization", auth) - - // Attempt to connect to running wallet instance using a proxy if one - // is configured. + // Setup the proxy if one is configured. if config.Proxy != "" { proxy := &socks.Proxy{ Addr: config.Proxy, Username: config.ProxyUser, Password: config.ProxyPass, } - conn, err := proxy.Dial("tcp", config.Host) - if err != nil { - return nil, err - } - - tlsConn := tls.Client(conn, wsConfig.TlsConfig) - ws, err := websocket.NewClient(wsConfig, tlsConn) - if err != nil { - return nil, err - } - - return ws, nil + dialer.NetDial = proxy.Dial } - // No proxy was specified, so attempt to connect to running wallet - // instance directly. - ws, err := websocket.DialConfig(wsConfig) + // 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 { - // XXX(davec): This is not really accurate, but unfortunately - // the current websocket package does not expose the status - // code, so it's impossible to tell for sure. - if dialError, ok := err.(*websocket.DialError); ok { - if dialError.Err == websocket.ErrBadStatus { + if err == websocket.ErrBadHandshake { + // Detect HTTP authentication error status codes. + if resp != nil && + (resp.StatusCode == http.StatusUnauthorized || + resp.StatusCode == http.StatusForbidden) { + return nil, ErrInvalidAuth } + + // The connection was authenticated, but the websocket + // handshake still failed, so the endpoint is invalid + // in some way. + return nil, ErrInvalidEndpoint } + return nil, err } - return ws, nil + return wsConn, nil } // New create a new RPC client based on the provided connection configuration