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 .
This commit is contained in:
Dave Collins 2014-05-09 19:26:56 -05:00
parent 4921282646
commit 8700eeaeb6

View file

@ -6,7 +6,6 @@ package btcrpcclient
import ( import (
"bytes" "bytes"
"code.google.com/p/go.net/websocket"
"container/list" "container/list"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
@ -16,6 +15,7 @@ import (
"fmt" "fmt"
"github.com/conformal/btcjson" "github.com/conformal/btcjson"
"github.com/conformal/go-socks" "github.com/conformal/go-socks"
"github.com/gorilla/websocket"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -30,6 +30,11 @@ var (
// incorrect. // incorrect.
ErrInvalidAuth = errors.New("authentication failure") 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 // ErrClientDisconnect is an error to describe the condition where the
// client has been disconnected from the RPC server. When the // client has been disconnected from the RPC server. When the
// DisableAutoReconnect option is not set, any outstanding futures // 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 // (including pass through for standard RPC commands), sends the appropriate
// response. It also detects commands which are marked as long-running and // response. It also detects commands which are marked as long-running and
// sends them off to the asyncHander for processing. // 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. // 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 // Commands that have an ID associated with them are not
// notifications. Since this is a client, it should not // notifications. Since this is a client, it should not
// be receiving non-notifications. // be receiving non-notifications.
@ -271,8 +276,8 @@ out:
default: default:
} }
var msg string _, msg, err := c.wsConn.ReadMessage()
if err := websocket.Message.Receive(c.wsConn, &msg); err != nil { if err != nil {
// Log the error if it's not due to disconnecting. // Log the error if it's not due to disconnecting.
if _, ok := err.(*net.OpError); !ok { if _, ok := err.(*net.OpError); !ok {
log.Errorf("Websocket receive error from "+ log.Errorf("Websocket receive error from "+
@ -299,7 +304,7 @@ out:
// disconnected closed. // disconnected closed.
select { select {
case msg := <-c.sendChan: case msg := <-c.sendChan:
err := websocket.Message.Send(c.wsConn, string(msg)) err := c.wsConn.WriteMessage(websocket.TextMessage, msg)
if err != nil { if err != nil {
c.Disconnect() c.Disconnect()
break out break out
@ -832,63 +837,62 @@ func newHttpClient(config *ConnConfig) (*http.Client, error) {
// dial opens a websocket connection using the passed connection configuration // dial opens a websocket connection using the passed connection configuration
// details. // details.
func dial(config *ConnConfig) (*websocket.Conn, error) { func dial(config *ConnConfig) (*websocket.Conn, error) {
// Connect to websocket. // Setup TLS if not disabled.
url := fmt.Sprintf("wss://%s/%s", config.Host, config.Endpoint) var tlsConfig *tls.Config
wsConfig, err := websocket.NewConfig(url, "https://localhost/") var scheme = "ws"
if err != nil { if !config.DisableTLS {
return nil, err pool := x509.NewCertPool()
pool.AppendCertsFromPEM(config.Certificates)
tlsConfig = &tls.Config{
RootCAs: pool,
MinVersion: tls.VersionTLS12,
}
scheme = "wss"
} }
pool := x509.NewCertPool() // Create a websocket dialer that will be used to make the connection.
pool.AppendCertsFromPEM(config.Certificates) // It is modified by the proxy setting below as needed.
wsConfig.TlsConfig = &tls.Config{ dialer := websocket.Dialer{TLSClientConfig: tlsConfig}
RootCAs: pool,
MinVersion: tls.VersionTLS12,
}
// The wallet requires basic authorization, so use a custom config with // Setup the proxy if one is configured.
// 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.
if config.Proxy != "" { if config.Proxy != "" {
proxy := &socks.Proxy{ proxy := &socks.Proxy{
Addr: config.Proxy, Addr: config.Proxy,
Username: config.ProxyUser, Username: config.ProxyUser,
Password: config.ProxyPass, Password: config.ProxyPass,
} }
conn, err := proxy.Dial("tcp", config.Host) dialer.NetDial = proxy.Dial
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
} }
// No proxy was specified, so attempt to connect to running wallet // The RPC server requires basic authorization, so create a custom
// instance directly. // request header with the Authorization header set.
ws, err := websocket.DialConfig(wsConfig) 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 != nil {
// XXX(davec): This is not really accurate, but unfortunately if err == websocket.ErrBadHandshake {
// the current websocket package does not expose the status // Detect HTTP authentication error status codes.
// code, so it's impossible to tell for sure. if resp != nil &&
if dialError, ok := err.(*websocket.DialError); ok { (resp.StatusCode == http.StatusUnauthorized ||
if dialError.Err == websocket.ErrBadStatus { resp.StatusCode == http.StatusForbidden) {
return nil, ErrInvalidAuth 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 nil, err
} }
return ws, nil return wsConn, nil
} }
// New create a new RPC client based on the provided connection configuration // New create a new RPC client based on the provided connection configuration