Implement alternative auth for websockets.

The previous websocket code required HTTP auth headers to be sent in order
to use the websocket.  While this makes sense for most applications, some
use cases such as javascript-based websockets from browsers do no have the
ability to send headers.

This commit modifies the authentication logic to allow an alternative
authentication mechanism when HTTP auth headers can't be sent.  In
particular, it introduces a new JSON-RPC command named authenticate which
accepts the username and passphrase for the RPC server.  The command is
only required if the connetion has not already been authenticated via HTTP
auth headers and it must be the first command to be received.  Sending any
other command will immediately disconnect the websocket.

ok from @owainga and @jrick.

This closes #77.
This commit is contained in:
Dave Collins 2014-01-22 14:10:04 -06:00
parent 9cb5190ac2
commit 0d40bf901d
2 changed files with 126 additions and 34 deletions

View file

@ -166,7 +166,7 @@ func (s *rpcServer) Start() {
ReadTimeout: time.Second * rpcAuthTimeoutSeconds,
}
rpcServeMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if err := s.checkAuth(r); err != nil {
if _, err := s.checkAuth(r, true); err != nil {
jsonAuthFail(w, r, s)
return
}
@ -175,12 +175,15 @@ func (s *rpcServer) Start() {
})
rpcServeMux.HandleFunc("/wallet", func(w http.ResponseWriter, r *http.Request) {
if err := s.checkAuth(r); err != nil {
authenticated, err := s.checkAuth(r, false)
if err != nil {
http.Error(w, "401 Unauthorized.", http.StatusUnauthorized)
return
}
wsServer := websocket.Server{
Handler: websocket.Handler(s.walletReqsNotifications),
Handler: websocket.Handler(func(ws *websocket.Conn) {
s.walletReqsNotifications(ws, authenticated)
}),
}
wsServer.ServeHTTP(w, r)
})
@ -202,20 +205,24 @@ func (s *rpcServer) Start() {
// returned.
//
// This check is time-constant.
func (s *rpcServer) checkAuth(r *http.Request) error {
func (s *rpcServer) checkAuth(r *http.Request, require bool) (bool, error) {
authhdr := r.Header["Authorization"]
if len(authhdr) <= 0 {
rpcsLog.Warnf("Auth failure.")
return errors.New("auth failure")
if require {
rpcsLog.Warnf("Auth failure.")
return false, errors.New("auth failure")
}
return false, nil
}
authsha := sha256.Sum256([]byte(authhdr[0]))
cmp := subtle.ConstantTimeCompare(authsha[:], s.authsha[:])
if cmp != 1 {
rpcsLog.Warnf("Auth failure.")
return errors.New("auth failure")
return false, errors.New("auth failure")
}
return nil
return true, nil
}
// Stop is used by server.go to stop the rpc listener.

View file

@ -8,8 +8,12 @@ import (
"bytes"
"code.google.com/p/go.net/websocket"
"container/list"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/conformal/btcdb"
"github.com/conformal/btcjson"
@ -23,6 +27,8 @@ import (
var timeZeroVal time.Time
var ErrBadAuth = errors.New("invalid credentials")
type ntfnChan chan btcjson.Cmd
type handlerChans struct {
@ -230,6 +236,15 @@ func newWebsocketContext() *wsContext {
// requestContexts holds all requests for a single wallet connection.
type requestContexts struct {
// disconnecting indicates the websocket is in the process of
// disconnecting. This is used to prevent trying to handle any more
// commands in the interim.
disconnecting bool
// authenticated specifies whether a client has been authenticated
// and therefore is allowed to communicated over the websocket.
authenticated bool
// blockUpdates specifies whether a client has requested notifications
// for whenever blocks are connected or disconnected from the main
// chain.
@ -512,15 +527,41 @@ func handleWalletSendRawTransaction(s *rpcServer, icmd btcjson.Cmd, c handlerCha
return result, nil
}
// websocketAuthenticate checks the authenticate command for valid credentials.
// An error is returned if the credentials are invalid or if the connection is
// already authenticated.
//
// This function MUST be called with the websocket lock held.
func websocketAuthenticate(icmd btcjson.Cmd, rc *requestContexts, authSha []byte) error {
cmd, ok := icmd.(*btcws.AuthenticateCmd)
if !ok {
return fmt.Errorf("%s", btcjson.ErrInternal.Message)
}
// Already authenticated?
if rc.authenticated {
rpcsLog.Warnf("Already authenticated")
return ErrBadAuth
}
// Check credentials.
login := cmd.Username + ":" + cmd.Passphrase
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
calcualtedAuthSha := sha256.Sum256([]byte(auth))
cmp := subtle.ConstantTimeCompare(calcualtedAuthSha[:], authSha)
if cmp != 1 {
rpcsLog.Warnf("Auth failure.")
return ErrBadAuth
}
rc.authenticated = true
return nil
}
// AddWalletListener adds a channel to listen for new messages from a
// wallet.
func (s *rpcServer) AddWalletListener(n ntfnChan) {
func (s *rpcServer) AddWalletListener(n ntfnChan, rc *requestContexts) {
s.ws.Lock()
rc := &requestContexts{
txRequests: make(map[string]struct{}),
spentRequests: make(map[btcwire.OutPoint]struct{}),
minedTxRequests: make(map[btcwire.ShaHash]struct{}),
}
s.ws.connections[n] = rc
s.ws.Unlock()
}
@ -547,7 +588,7 @@ func (s *rpcServer) RemoveWalletListener(n ntfnChan) {
// walletReqsNotifications is the handler function for websocket
// connections from a btcwallet instance. It reads messages from wallet and
// sends back replies, as well as notififying wallets of chain updates.
func (s *rpcServer) walletReqsNotifications(ws *websocket.Conn) {
func (s *rpcServer) walletReqsNotifications(ws *websocket.Conn, authenticated bool) {
// Clear the read deadline that was set before the websocket hijacked
// the connection.
ws.SetReadDeadline(timeZeroVal)
@ -555,7 +596,13 @@ func (s *rpcServer) walletReqsNotifications(ws *websocket.Conn) {
// Add wallet notification channel so this handler receives btcd chain
// notifications.
n := make(ntfnChan)
s.AddWalletListener(n)
rc := &requestContexts{
authenticated: authenticated,
txRequests: make(map[string]struct{}),
spentRequests: make(map[btcwire.OutPoint]struct{}),
minedTxRequests: make(map[btcwire.ShaHash]struct{}),
}
s.AddWalletListener(n, rc)
defer s.RemoveWalletListener(n)
// Channel for responses.
@ -622,10 +669,18 @@ func (s *rpcServer) walletReqsNotifications(ws *websocket.Conn) {
}
case m := <-msgs:
// Spawn new goroutine to handle request. Responses and
// notifications are read by channels in this for-select
// loop.
go s.websocketJSONHandler(r, hc, m)
// This function internally spawns a new goroutine to
// the handle request after validating authentication.
// Responses and notifications are read by channels in
// this for-select loop.
if !rc.disconnecting {
err := s.websocketJSONHandler(r, hc, m)
if err == ErrBadAuth {
rc.disconnecting = true
close(disconnected)
ws.Close()
}
}
case response := <-r:
// Marshal and send response.
@ -670,10 +725,7 @@ func (s *rpcServer) walletReqsNotifications(ws *websocket.Conn) {
// websocketJSONHandler parses and handles a marshalled json message,
// sending the marshalled reply to a wallet notification channel.
func (s *rpcServer) websocketJSONHandler(r chan *btcjson.Reply, c handlerChans, msg string) {
s.wg.Add(1)
defer s.wg.Done()
func (s *rpcServer) websocketJSONHandler(r chan *btcjson.Reply, c handlerChans, msg string) error {
var resp *btcjson.Reply
cmd, jsonErr := parseCmd([]byte(msg))
@ -685,24 +737,57 @@ func (s *rpcServer) websocketJSONHandler(r chan *btcjson.Reply, c handlerChans,
// should be ignored.
id := cmd.Id()
if id == nil {
return
return nil
}
resp.Id = &id
}
resp.Error = jsonErr
} else {
resp = respondToAnyCmd(cmd, s, c)
// The first command must be the "authenticate" command if the
// connection is not already authenticated.
s.ws.Lock()
rc := s.ws.connections[c.n]
if _, ok := cmd.(*btcws.AuthenticateMsg); ok {
// Validate the provided credentials.
err := websocketAuthenticate(cmd, rc, s.authsha[:])
if err != nil {
s.ws.Unlock()
return err
}
// Generate an empty response to send for the successful
// authentication.
id := cmd.Id()
resp = &btcjson.Reply{
Id: &id,
Result: nil,
Error: nil,
}
} else if !rc.authenticated {
rpcsLog.Warnf("Unauthenticated websocket message " +
"received")
s.ws.Unlock()
return ErrBadAuth
}
s.ws.Unlock()
}
// Once response has been processed, only send if the client
// is still connected.
select {
case <-c.disconnected:
return
// Find and run handler in new goroutine.
go func() {
if resp == nil {
resp = respondToAnyCmd(cmd, s, c)
}
select {
case <-c.disconnected:
return
default:
r <- resp
}
default:
r <- resp
}
}()
return nil
}
// NotifyBlockConnected creates and marshalls a JSON message to notify