diff --git a/README.md b/README.md index 02650fe..aa6e07b 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,6 @@ messages they originated from. ## TODO -- Require authentication before wallet functionality can be accessed -- Serve frontend websocket connections over TLS -- Rescan the blockchain for missed transactions - Documentation (specifically the websocket API additions) - Code cleanup - Optimize diff --git a/config.go b/config.go index 6473261..dfd916f 100644 --- a/config.go +++ b/config.go @@ -36,10 +36,12 @@ const ( ) var ( - btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false) - defaultCAFile = filepath.Join(btcwalletHomeDir, defaultCAFilename) - defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename) - defaultDataDir = btcwalletHomeDir + btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false) + defaultCAFile = filepath.Join(btcwalletHomeDir, defaultCAFilename) + defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename) + defaultDataDir = btcwalletHomeDir + defaultRPCKeyFile = filepath.Join(btcwalletHomeDir, "rpc.key") + defaultRPCCertFile = filepath.Join(btcwalletHomeDir, "rpc.cert") ) type config struct { @@ -52,6 +54,8 @@ type config struct { DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"` Username string `short:"u" long:"username" description:"Username for btcd authorization"` Password string `short:"P" long:"password" description:"Password for btcd authorization"` + RPCCert string `long:"rpccert" description:"File containing the certificate file"` + RPCKey string `long:"rpckey" description:"File containing the certificate key"` MainNet bool `long:"mainnet" description:"*DISABLED* Use the main Bitcoin network (default testnet3)"` Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` ProxyUser string `long:"proxyuser" description:"Username for proxy server"` @@ -128,6 +132,8 @@ func loadConfig() (*config, []string, error) { Connect: netParams(defaultBtcNet).connect, SvrPort: netParams(defaultBtcNet).svrPort, DataDir: defaultDataDir, + RPCKey: defaultRPCKeyFile, + RPCCert: defaultRPCCertFile, } // A config file in the current directory takes precedence. diff --git a/sockets.go b/sockets.go index 65d6e5f..459b6e2 100644 --- a/sockets.go +++ b/sockets.go @@ -18,10 +18,16 @@ package main import ( "code.google.com/p/go.net/websocket" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + _ "crypto/sha512" // for cert generation "crypto/tls" "crypto/x509" + "crypto/x509/pkix" "encoding/base64" "encoding/json" + "encoding/pem" "errors" "fmt" "github.com/conformal/btcjson" @@ -29,9 +35,12 @@ import ( "github.com/conformal/btcwire" "github.com/conformal/btcws" "github.com/conformal/go-socks" + "math/big" "net" "net/http" + "os" "sync" + "time" ) var ( @@ -91,6 +100,8 @@ var ( // config, shutdown, etc.) type server struct { port string + username string + password string wg sync.WaitGroup listeners []net.Listener } @@ -98,13 +109,32 @@ type server struct { // newServer returns a new instance of the server struct. func newServer() (*server, error) { s := server{ - port: cfg.SvrPort, + port: cfg.SvrPort, + username: cfg.Username, + password: cfg.Password, + } + + // Check for existence of cert file and key file + if !fileExists(cfg.RPCKey) && !fileExists(cfg.RPCCert) { + // if both files do not exist, we generate them. + err := genKey(cfg.RPCKey, cfg.RPCCert) + if err != nil { + return nil, err + } + } + keypair, err := tls.LoadX509KeyPair(cfg.RPCCert, cfg.RPCKey) + if err != nil { + return nil, err + } + + tlsConfig := tls.Config{ + Certificates: []tls.Certificate{keypair}, } // IPv4 listener. var listeners []net.Listener listenAddr4 := net.JoinHostPort("127.0.0.1", s.port) - listener4, err := net.Listen("tcp4", listenAddr4) + listener4, err := tls.Listen("tcp4", listenAddr4, &tlsConfig) if err != nil { log.Errorf("RPCS: Couldn't create listener: %v", err) return nil, err @@ -113,7 +143,7 @@ func newServer() (*server, error) { // IPv6 listener. listenAddr6 := net.JoinHostPort("::1", s.port) - listener6, err := net.Listen("tcp6", listenAddr6) + listener6, err := tls.Listen("tcp6", listenAddr6, &tlsConfig) if err != nil { log.Errorf("RPCS: Couldn't create listener: %v", err) return nil, err @@ -125,6 +155,97 @@ func newServer() (*server, error) { return &s, nil } +// genkey generates a key/cert pair to the paths provided. +// TODO(oga) wrap errors with fmt.Errorf for more context? +func genKey(key, cert string) error { + log.Infof("Generating TLS certificates...") + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return err + } + + notBefore := time.Now() + notAfter := notBefore.Add(10 * 365 * 24 * time.Hour) + + // end of ASN.1 time + endOfTime := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC) + if notAfter.After(endOfTime) { + notAfter = endOfTime + } + + template := x509.Certificate{ + SerialNumber: new(big.Int).SetInt64(0), + Subject: pkix.Name{ + Organization: []string{"btcwallet autogenerated cert"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IsCA: true, // so can sign self. + BasicConstraintsValid: true, + } + + host, err := os.Hostname() + if err != nil { + return err + } + template.DNSNames = append(template.DNSNames, host, "localhost") + + needLocalhost := true + addrs, err := net.InterfaceAddrs() + if err != nil { + return err + } + for _, a := range addrs { + ip, _, err := net.ParseCIDR(a.String()) + if err == nil { + if ip.String() == "127.0.0.1" { + needLocalhost = false + } + template.IPAddresses = append(template.IPAddresses, ip) + } + } + if needLocalhost { + localHost := net.ParseIP("127.0.0.1") + template.IPAddresses = append(template.IPAddresses, localHost) + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, + &template, &priv.PublicKey, priv) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create certificate: %v\n", err) + os.Exit(-1) + } + + certOut, err := os.Create(cert) + if err != nil { + return err + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + + keyOut, err := os.OpenFile(key, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + 0600) + if err != nil { + os.Remove(cert) + return err + } + keybytes, err := x509.MarshalECPrivateKey(priv) + if err != nil { + os.Remove(key) + os.Remove(cert) + return err + } + pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes}) + keyOut.Close() + + log.Info("Done generating TLS certificates") + + return nil +} + // handleRPCRequest processes a JSON-RPC request from a frontend. func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) { frontend := make(chan []byte) @@ -535,6 +656,8 @@ func (s *server) Start() { // Use a sync.Once to insure no extra duplicators run. go duplicateOnce.Do(frontendListenerDuplicator) + log.Trace("Starting RPC server") + // TODO(jrick): We need some sort of authentication before websocket // connections are allowed, and perhaps TLS on the server as well. serveMux := http.NewServeMux() @@ -542,7 +665,21 @@ func (s *server) Start() { serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { s.handleRPCRequest(w, r) }) - serveMux.Handle("/frontend", websocket.Handler(frontendSendRecv)) + wsServer := websocket.Server{ + Handler: websocket.Handler(func(ws *websocket.Conn) { + frontendSendRecv(ws) + }), + Handshake: func(_ *websocket.Config, r *http.Request) error { + login := s.username + ":" + s.password + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) + authhdr := r.Header["Authorization"] + if len(authhdr) <= 0 || authhdr[0] != auth { + return errors.New("auth failure") + } + return nil + }, + } + serveMux.Handle("/frontend", wsServer) for _, listener := range s.listeners { s.wg.Add(1) go func(listener net.Listener) {