Use TLS+auth for frontend connections.
This change is mostly a copy paste job from the TLS listeners and autogenerated cert code from btcd.
This commit is contained in:
parent
af1438eecd
commit
3b04e3a4bc
3 changed files with 151 additions and 11 deletions
|
@ -110,9 +110,6 @@ messages they originated from.
|
||||||
|
|
||||||
## TODO
|
## 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)
|
- Documentation (specifically the websocket API additions)
|
||||||
- Code cleanup
|
- Code cleanup
|
||||||
- Optimize
|
- Optimize
|
||||||
|
|
14
config.go
14
config.go
|
@ -36,10 +36,12 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false)
|
btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false)
|
||||||
defaultCAFile = filepath.Join(btcwalletHomeDir, defaultCAFilename)
|
defaultCAFile = filepath.Join(btcwalletHomeDir, defaultCAFilename)
|
||||||
defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename)
|
defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename)
|
||||||
defaultDataDir = btcwalletHomeDir
|
defaultDataDir = btcwalletHomeDir
|
||||||
|
defaultRPCKeyFile = filepath.Join(btcwalletHomeDir, "rpc.key")
|
||||||
|
defaultRPCCertFile = filepath.Join(btcwalletHomeDir, "rpc.cert")
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
@ -52,6 +54,8 @@ type config struct {
|
||||||
DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"`
|
DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"`
|
||||||
Username string `short:"u" long:"username" description:"Username for btcd authorization"`
|
Username string `short:"u" long:"username" description:"Username for btcd authorization"`
|
||||||
Password string `short:"P" long:"password" description:"Password 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)"`
|
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)"`
|
Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"`
|
||||||
ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
|
ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
|
||||||
|
@ -128,6 +132,8 @@ func loadConfig() (*config, []string, error) {
|
||||||
Connect: netParams(defaultBtcNet).connect,
|
Connect: netParams(defaultBtcNet).connect,
|
||||||
SvrPort: netParams(defaultBtcNet).svrPort,
|
SvrPort: netParams(defaultBtcNet).svrPort,
|
||||||
DataDir: defaultDataDir,
|
DataDir: defaultDataDir,
|
||||||
|
RPCKey: defaultRPCKeyFile,
|
||||||
|
RPCCert: defaultRPCCertFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A config file in the current directory takes precedence.
|
// A config file in the current directory takes precedence.
|
||||||
|
|
145
sockets.go
145
sockets.go
|
@ -18,10 +18,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.google.com/p/go.net/websocket"
|
"code.google.com/p/go.net/websocket"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
_ "crypto/sha512" // for cert generation
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/conformal/btcjson"
|
"github.com/conformal/btcjson"
|
||||||
|
@ -29,9 +35,12 @@ import (
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
"github.com/conformal/btcws"
|
"github.com/conformal/btcws"
|
||||||
"github.com/conformal/go-socks"
|
"github.com/conformal/go-socks"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -91,6 +100,8 @@ var (
|
||||||
// config, shutdown, etc.)
|
// config, shutdown, etc.)
|
||||||
type server struct {
|
type server struct {
|
||||||
port string
|
port string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
listeners []net.Listener
|
listeners []net.Listener
|
||||||
}
|
}
|
||||||
|
@ -98,13 +109,32 @@ type server struct {
|
||||||
// newServer returns a new instance of the server struct.
|
// newServer returns a new instance of the server struct.
|
||||||
func newServer() (*server, error) {
|
func newServer() (*server, error) {
|
||||||
s := server{
|
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.
|
// IPv4 listener.
|
||||||
var listeners []net.Listener
|
var listeners []net.Listener
|
||||||
listenAddr4 := net.JoinHostPort("127.0.0.1", s.port)
|
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 {
|
if err != nil {
|
||||||
log.Errorf("RPCS: Couldn't create listener: %v", err)
|
log.Errorf("RPCS: Couldn't create listener: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -113,7 +143,7 @@ func newServer() (*server, error) {
|
||||||
|
|
||||||
// IPv6 listener.
|
// IPv6 listener.
|
||||||
listenAddr6 := net.JoinHostPort("::1", s.port)
|
listenAddr6 := net.JoinHostPort("::1", s.port)
|
||||||
listener6, err := net.Listen("tcp6", listenAddr6)
|
listener6, err := tls.Listen("tcp6", listenAddr6, &tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("RPCS: Couldn't create listener: %v", err)
|
log.Errorf("RPCS: Couldn't create listener: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -125,6 +155,97 @@ func newServer() (*server, error) {
|
||||||
return &s, nil
|
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.
|
// handleRPCRequest processes a JSON-RPC request from a frontend.
|
||||||
func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
frontend := make(chan []byte)
|
frontend := make(chan []byte)
|
||||||
|
@ -535,6 +656,8 @@ func (s *server) Start() {
|
||||||
// Use a sync.Once to insure no extra duplicators run.
|
// Use a sync.Once to insure no extra duplicators run.
|
||||||
go duplicateOnce.Do(frontendListenerDuplicator)
|
go duplicateOnce.Do(frontendListenerDuplicator)
|
||||||
|
|
||||||
|
log.Trace("Starting RPC server")
|
||||||
|
|
||||||
// TODO(jrick): We need some sort of authentication before websocket
|
// TODO(jrick): We need some sort of authentication before websocket
|
||||||
// connections are allowed, and perhaps TLS on the server as well.
|
// connections are allowed, and perhaps TLS on the server as well.
|
||||||
serveMux := http.NewServeMux()
|
serveMux := http.NewServeMux()
|
||||||
|
@ -542,7 +665,21 @@ func (s *server) Start() {
|
||||||
serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
s.handleRPCRequest(w, r)
|
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 {
|
for _, listener := range s.listeners {
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func(listener net.Listener) {
|
go func(listener net.Listener) {
|
||||||
|
|
Loading…
Reference in a new issue