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
|
||||
|
||||
- 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
|
||||
|
|
14
config.go
14
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.
|
||||
|
|
145
sockets.go
145
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) {
|
||||
|
|
Loading…
Reference in a new issue