Enable TLS support for btcd websocket connections.

This adds an additional config option, -cafile, to specify the root
certificates checked when verifying a btcd TLC connection.  btcd will
now automatically generate certs in
~/.btcd/data/{main,test}net/rpc.cert, and this file should be copied
to ~/.btcwallet/cert.pem.

The -btcdport option is also gone now, and replaced with -connect (or
-c), to specify both the hostname/ip and port of the server running
btcd.
This commit is contained in:
Josh Rickmar 2013-11-19 12:21:54 -05:00
parent 474106a757
commit 5dbf69d23e
6 changed files with 54 additions and 15 deletions

View file

@ -96,7 +96,7 @@ messages they originated from.
## TODO ## TODO
- Require authentication before wallet functionality can be accessed - Require authentication before wallet functionality can be accessed
- Serve websocket connections over TLS - Serve frontend websocket connections over TLS
- Rescan the blockchain for missed transactions - Rescan the blockchain for missed transactions
- Documentation (specifically the websocket API additions) - Documentation (specifically the websocket API additions)
- Code cleanup - Code cleanup

10
cmd.go
View file

@ -25,6 +25,7 @@ import (
"github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -312,6 +313,13 @@ func main() {
log.Errorf("cannot open wallet: %v", err) log.Errorf("cannot open wallet: %v", err)
} }
// Read CA file to verify a btcd TLS connection.
cafile, err := ioutil.ReadFile(cfg.CAFile)
if err != nil {
log.Errorf("cannot open CA file: %v", err)
os.Exit(1)
}
// Start account disk syncer goroutine. // Start account disk syncer goroutine.
go DirtyAccountSyncer() go DirtyAccountSyncer()
@ -334,7 +342,7 @@ func main() {
replies := make(chan error) replies := make(chan error)
done := make(chan int) done := make(chan int)
go func() { go func() {
BtcdConnect(replies) BtcdConnect(cafile, replies)
close(done) close(done)
}() }()
selectLoop: selectLoop:

View file

@ -21,12 +21,14 @@ import (
"github.com/conformal/btcutil" "github.com/conformal/btcutil"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/go-flags" "github.com/conformal/go-flags"
"net"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
) )
const ( const (
defaultCAFilename = "cert.pem"
defaultConfigFilename = "btcwallet.conf" defaultConfigFilename = "btcwallet.conf"
defaultDataDirname = "data" defaultDataDirname = "data"
defaultBtcNet = btcwire.TestNet3 defaultBtcNet = btcwire.TestNet3
@ -35,13 +37,15 @@ const (
var ( var (
btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false) btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false)
defaultCAFile = filepath.Join(btcwalletHomeDir, defaultCAFilename)
defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename) defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename)
defaultDataDir = btcwalletHomeDir defaultDataDir = btcwalletHomeDir
) )
type config struct { type config struct {
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
BtcdPort string `short:"b" long:"btcdport" description:"Port to connect to btcd on (default: 18334, mainnet: 18332)"` CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"`
Connect string `short:"c" long:"connect" description:"Server and port of btcd instance to connect to"`
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"`
SvrPort string `short:"p" long:"serverport" description:"Port to serve frontend websocket connections on (default: 18332, mainnet: 8332)"` SvrPort string `short:"p" long:"serverport" description:"Port to serve frontend websocket connections on (default: 18332, mainnet: 8332)"`
@ -56,8 +60,8 @@ type config struct {
// object are the default so options specified by the user on the command line // object are the default so options specified by the user on the command line
// are not overridden. // are not overridden.
func updateConfigWithActiveParams(cfg *config) { func updateConfigWithActiveParams(cfg *config) {
if cfg.BtcdPort == netParams(defaultBtcNet).btcdPort { if cfg.Connect == netParams(defaultBtcNet).connect {
cfg.BtcdPort = activeNetParams.btcdPort cfg.Connect = activeNetParams.connect
} }
if cfg.SvrPort == netParams(defaultBtcNet).svrPort { if cfg.SvrPort == netParams(defaultBtcNet).svrPort {
@ -75,6 +79,16 @@ func fileExists(name string) bool {
return true return true
} }
// normalizeAddress returns addr with the passed default port appended if
// there is not already a port specified.
func normalizeAddress(addr, defaultPort string) string {
_, _, err := net.SplitHostPort(addr)
if err != nil {
return net.JoinHostPort(addr, defaultPort)
}
return addr
}
// loadConfig initializes and parses the config using a config file and command // loadConfig initializes and parses the config using a config file and command
// line options. // line options.
// //
@ -91,8 +105,9 @@ func loadConfig() (*config, []string, error) {
// Default config. // Default config.
cfg := config{ cfg := config{
DebugLevel: defaultLogLevel, DebugLevel: defaultLogLevel,
CAFile: defaultCAFile,
ConfigFile: defaultConfigFile, ConfigFile: defaultConfigFile,
BtcdPort: netParams(defaultBtcNet).btcdPort, Connect: netParams(defaultBtcNet).connect,
SvrPort: netParams(defaultBtcNet).svrPort, SvrPort: netParams(defaultBtcNet).svrPort,
DataDir: defaultDataDir, DataDir: defaultDataDir,
} }
@ -161,6 +176,9 @@ func loadConfig() (*config, []string, error) {
return nil, nil, err return nil, nil, err
} }
// Add default port to connect flag if missing.
cfg.Connect = normalizeAddress(cfg.Connect, activeNetParams.btcdPort)
return &cfg, remainingArgs, nil return &cfg, remainingArgs, nil
} }

View file

@ -25,6 +25,7 @@ var activeNetParams = netParams(defaultBtcNet)
// params is used to group parameters for various networks such as the main // params is used to group parameters for various networks such as the main
// network and test networks. // network and test networks.
type params struct { type params struct {
connect string
btcdPort string btcdPort string
svrPort string svrPort string
} }
@ -32,6 +33,7 @@ type params struct {
// mainNetParams contains parameters specific running btcwallet and // mainNetParams contains parameters specific running btcwallet and
// btcd on the main network (btcwire.MainNet). // btcd on the main network (btcwire.MainNet).
var mainNetParams = params{ var mainNetParams = params{
connect: "localhost:8334",
btcdPort: "8334", btcdPort: "8334",
svrPort: "8332", svrPort: "8332",
} }
@ -39,6 +41,7 @@ var mainNetParams = params{
// testNet3Params contains parameters specific running btcwallet and // testNet3Params contains parameters specific running btcwallet and
// btcd on the test network (version 3) (btcwire.TestNet3). // btcd on the test network (version 3) (btcwire.TestNet3).
var testNet3Params = params{ var testNet3Params = params{
connect: "localhost:18334",
btcdPort: "18334", btcdPort: "18334",
svrPort: "18332", svrPort: "18332",
} }

View file

@ -4,8 +4,8 @@
; Network settings ; Network settings
; ------------------------------------------------------------------------------ ; ------------------------------------------------------------------------------
; The port used for btcd websocket connections. ; The server and port used for btcd websocket connections.
; btcdport=18334 ; connect=localhost:18334
; Username and password to authenticate to btcd RPC/websocket HTTP server. ; Username and password to authenticate to btcd RPC/websocket HTTP server.
; username= ; username=

View file

@ -18,6 +18,8 @@ package main
import ( import (
"code.google.com/p/go.net/websocket" "code.google.com/p/go.net/websocket"
"crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
@ -577,22 +579,30 @@ func (s *server) Start() {
// BtcdConnect connects to a running btcd instance over a websocket // BtcdConnect connects to a running btcd instance over a websocket
// for sending and receiving chain-related messages, failing if the // for sending and receiving chain-related messages, failing if the
// connection cannot be established or is lost. // connection cannot be established or is lost.
func BtcdConnect(reply chan error) { func BtcdConnect(certificates []byte, reply chan error) {
// btcd requires basic authorization, so we use a custom config with url := fmt.Sprintf("wss://%s/wallet", cfg.Connect)
// the Authorization header set. config, err := websocket.NewConfig(url, "https://localhost/")
server := fmt.Sprintf("ws://%s/wallet", net.JoinHostPort("localhost", cfg.BtcdPort))
login := cfg.Username + ":" + cfg.Password
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
config, err := websocket.NewConfig(server, "http://localhost/")
if err != nil { if err != nil {
reply <- ErrConnRefused reply <- ErrConnRefused
return return
} }
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(certificates)
config.TlsConfig = &tls.Config{
RootCAs: pool,
}
// btcd requires basic authorization, so we use a custom config with
// the Authorization header set.
login := cfg.Username + ":" + cfg.Password
auth := "Basic" + base64.StdEncoding.EncodeToString([]byte(login))
config.Header.Add("Authorization", auth) config.Header.Add("Authorization", auth)
// Attempt to connect to running btcd instance. Bail if it fails. // Attempt to connect to running btcd instance. Bail if it fails.
btcdws, err := websocket.DialConfig(config) btcdws, err := websocket.DialConfig(config)
if err != nil { if err != nil {
log.Errorf("%s", err)
reply <- ErrConnRefused reply <- ErrConnRefused
return return
} }