1008 lines
29 KiB
Go
1008 lines
29 KiB
Go
/*
|
|
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"code.google.com/p/go.net/websocket"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
_ "crypto/sha512" // for cert generation
|
|
"crypto/subtle"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/conformal/btcjson"
|
|
"github.com/conformal/btcwallet/wallet"
|
|
"github.com/conformal/btcwire"
|
|
"github.com/conformal/btcws"
|
|
"github.com/conformal/go-socks"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
// ErrConnRefused represents an error where a connection to another
|
|
// process cannot be established.
|
|
ErrConnRefused = errors.New("connection refused")
|
|
|
|
// ErrConnLost represents an error where a connection to another
|
|
// process cannot be established.
|
|
ErrConnLost = errors.New("connection lost")
|
|
|
|
// Channel for updates and boolean with the most recent update of
|
|
// whether the connection to btcd is active or not.
|
|
btcdConnected = struct {
|
|
b bool
|
|
c chan bool
|
|
}{
|
|
c: make(chan bool),
|
|
}
|
|
|
|
// Channel to send messages btcwallet does not understand and requests
|
|
// from btcwallet to btcd.
|
|
btcdMsgs = make(chan []byte)
|
|
|
|
// Adds a frontend listener channel
|
|
addFrontendListener = make(chan (chan []byte))
|
|
|
|
// Removes a frontend listener channel
|
|
deleteFrontendListener = make(chan (chan []byte))
|
|
|
|
// Messages sent to this channel are sent to each connected frontend.
|
|
frontendNotificationMaster = make(chan []byte, 100)
|
|
|
|
// replyHandlers maps between a unique number (passed as part of
|
|
// the JSON Id field) and a function to handle a reply or notification
|
|
// from btcd. As requests are received, this map is checked for a
|
|
// handler function to route the reply to. If the function returns
|
|
// true, the handler is removed from the map.
|
|
replyHandlers = struct {
|
|
sync.Mutex
|
|
m map[uint64]func(interface{}, *btcjson.Error) bool
|
|
}{
|
|
m: make(map[uint64]func(interface{}, *btcjson.Error) bool),
|
|
}
|
|
|
|
// replyRouter maps unique uint64 ids to reply channels, so btcd
|
|
// replies can be routed to the correct frontend.
|
|
replyRouter = struct {
|
|
sync.Mutex
|
|
m map[uint64]chan []byte
|
|
}{
|
|
m: make(map[uint64]chan []byte),
|
|
}
|
|
)
|
|
|
|
// server holds the items the RPC server may need to access (auth,
|
|
// config, shutdown, etc.)
|
|
type server struct {
|
|
wg sync.WaitGroup
|
|
listeners []net.Listener
|
|
authsha [sha256.Size]byte
|
|
}
|
|
|
|
// parseListeners splits the list of listen addresses passed in addrs into
|
|
// IPv4 and IPv6 slices and returns them. This allows easy creation of the
|
|
// listeners on the correct interface "tcp4" and "tcp6". It also properly
|
|
// detects addresses which apply to "all interfaces" and adds the address to
|
|
// both slices.
|
|
func parseListeners(addrs []string) ([]string, []string, error) {
|
|
ipv4ListenAddrs := make([]string, 0, len(addrs)*2)
|
|
ipv6ListenAddrs := make([]string, 0, len(addrs)*2)
|
|
for _, addr := range addrs {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
// Shouldn't happen due to already being normalized.
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Empty host or host of * on plan9 is both IPv4 and IPv6.
|
|
if host == "" || (host == "*" && runtime.GOOS == "plan9") {
|
|
ipv4ListenAddrs = append(ipv4ListenAddrs, addr)
|
|
ipv6ListenAddrs = append(ipv6ListenAddrs, addr)
|
|
continue
|
|
}
|
|
|
|
// Parse the IP.
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return nil, nil, fmt.Errorf("'%s' is not a valid IP "+
|
|
"address", host)
|
|
}
|
|
|
|
// To4 returns nil when the IP is not an IPv4 address, so use
|
|
// this determine the address type.
|
|
if ip.To4() == nil {
|
|
ipv6ListenAddrs = append(ipv6ListenAddrs, addr)
|
|
} else {
|
|
ipv4ListenAddrs = append(ipv4ListenAddrs, addr)
|
|
}
|
|
}
|
|
return ipv4ListenAddrs, ipv6ListenAddrs, nil
|
|
}
|
|
|
|
// newServer returns a new instance of the server struct.
|
|
func newServer(listenAddrs []string) (*server, error) {
|
|
login := cfg.Username + ":" + cfg.Password
|
|
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
|
|
s := server{
|
|
authsha: sha256.Sum256([]byte(auth)),
|
|
}
|
|
|
|
// 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},
|
|
}
|
|
|
|
ipv4ListenAddrs, ipv6ListenAddrs, err := parseListeners(listenAddrs)
|
|
listeners := make([]net.Listener, 0,
|
|
len(ipv6ListenAddrs)+len(ipv4ListenAddrs))
|
|
for _, addr := range ipv4ListenAddrs {
|
|
listener, err := tls.Listen("tcp4", addr, &tlsConfig)
|
|
if err != nil {
|
|
log.Warnf("RPCS: Can't listen on %s: %v", addr,
|
|
err)
|
|
continue
|
|
}
|
|
listeners = append(listeners, listener)
|
|
}
|
|
|
|
for _, addr := range ipv6ListenAddrs {
|
|
listener, err := tls.Listen("tcp6", addr, &tlsConfig)
|
|
if err != nil {
|
|
log.Warnf("RPCS: Can't listen on %s: %v", addr,
|
|
err)
|
|
continue
|
|
}
|
|
listeners = append(listeners, listener)
|
|
}
|
|
if len(listeners) == 0 {
|
|
return nil, errors.New("RPCS: No valid listen address")
|
|
}
|
|
|
|
s.listeners = listeners
|
|
|
|
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)
|
|
|
|
body, err := btcjson.GetRaw(r.Body)
|
|
if err != nil {
|
|
log.Errorf("RPCS: Error getting JSON message: %v", err)
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
if _, err := w.Write(<-frontend); err != nil {
|
|
log.Warnf("RPCS: could not respond to RPC request: %v",
|
|
err)
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
ProcessRequest(frontend, body, false)
|
|
<-done
|
|
}
|
|
|
|
// frontendListenerDuplicator listens for new wallet listener channels
|
|
// and duplicates messages sent to frontendNotificationMaster to all
|
|
// connected listeners.
|
|
func frontendListenerDuplicator() {
|
|
// frontendListeners is a map holding each currently connected frontend
|
|
// listener as the key. The value is ignored, as this is only used as
|
|
// a set.
|
|
frontendListeners := make(map[chan []byte]bool)
|
|
|
|
// Don't want to add or delete a wallet listener while iterating
|
|
// through each to propigate to every attached wallet. Use a mutex to
|
|
// prevent this.
|
|
var mtx sync.Mutex
|
|
|
|
// Check for listener channels to add or remove from set.
|
|
go func() {
|
|
for {
|
|
select {
|
|
case c := <-addFrontendListener:
|
|
mtx.Lock()
|
|
frontendListeners[c] = true
|
|
mtx.Unlock()
|
|
|
|
// TODO(jrick): these notifications belong somewhere better.
|
|
// Probably want to copy AddWalletListener from btcd, and
|
|
// place these notifications in that function.
|
|
NotifyBtcdConnected(frontendNotificationMaster,
|
|
btcdConnected.b)
|
|
if bs, err := GetCurBlock(); err == nil {
|
|
NotifyNewBlockChainHeight(c, bs)
|
|
NotifyBalances(c)
|
|
}
|
|
|
|
case c := <-deleteFrontendListener:
|
|
mtx.Lock()
|
|
delete(frontendListeners, c)
|
|
mtx.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Duplicate all messages sent across frontendNotificationMaster, as
|
|
// well as internal btcwallet notifications, to each listening wallet.
|
|
for {
|
|
var ntfn []byte
|
|
|
|
select {
|
|
case conn := <-btcdConnected.c:
|
|
NotifyBtcdConnected(frontendNotificationMaster, conn)
|
|
continue
|
|
|
|
case ntfn = <-frontendNotificationMaster:
|
|
}
|
|
|
|
mtx.Lock()
|
|
for c := range frontendListeners {
|
|
c <- ntfn
|
|
}
|
|
mtx.Unlock()
|
|
}
|
|
}
|
|
|
|
// NotifyBtcdConnected notifies all frontends of a new btcd connection.
|
|
func NotifyBtcdConnected(reply chan []byte, conn bool) {
|
|
btcdConnected.b = conn
|
|
|
|
ntfn := btcws.NewBtcdConnectedNtfn(conn)
|
|
mntfn, _ := ntfn.MarshalJSON()
|
|
frontendNotificationMaster <- mntfn
|
|
}
|
|
|
|
// frontendSendRecv is the handler function for websocket connections from
|
|
// a btcwallet instance. It reads requests and sends responses to a
|
|
// frontend, as well as notififying wallets of chain updates. There can
|
|
// possibly be many of these running, one for each currently connected
|
|
// frontend.
|
|
func frontendSendRecv(ws *websocket.Conn) {
|
|
// Add frontend notification channel to set so this handler receives
|
|
// updates.
|
|
frontendNotification := make(chan []byte)
|
|
addFrontendListener <- frontendNotification
|
|
defer func() {
|
|
deleteFrontendListener <- frontendNotification
|
|
}()
|
|
|
|
// jsonMsgs receives JSON messages from the currently connected frontend.
|
|
jsonMsgs := make(chan []byte)
|
|
|
|
// Receive messages from websocket and send across jsonMsgs until
|
|
// connection is lost
|
|
go func() {
|
|
for {
|
|
var m []byte
|
|
if err := websocket.Message.Receive(ws, &m); err != nil {
|
|
close(jsonMsgs)
|
|
return
|
|
}
|
|
jsonMsgs <- m
|
|
}
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case m, ok := <-jsonMsgs:
|
|
if !ok {
|
|
// frontend disconnected.
|
|
return
|
|
}
|
|
// Handle request here.
|
|
go ProcessRequest(frontendNotification, m, true)
|
|
case ntfn, _ := <-frontendNotification:
|
|
if err := websocket.Message.Send(ws, ntfn); err != nil {
|
|
// Frontend disconnected.
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BtcdHandler listens for replies and notifications from btcd over a
|
|
// websocket and sends messages that btcwallet does not understand to
|
|
// btcd. Unlike FrontendHandler, exactly one BtcdHandler goroutine runs.
|
|
// BtcdHandler spawns goroutines to perform these tasks, and closes the
|
|
// done channel once they are finished.
|
|
func BtcdHandler(ws *websocket.Conn, done chan struct{}) {
|
|
// Listen for replies/notifications from btcd, and decide how to handle them.
|
|
replies := make(chan []byte)
|
|
go func() {
|
|
for {
|
|
var m []byte
|
|
if err := websocket.Message.Receive(ws, &m); err != nil {
|
|
log.Debugf("cannot recevie btcd message: %v", err)
|
|
close(replies)
|
|
return
|
|
}
|
|
replies <- m
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
defer close(done)
|
|
for {
|
|
select {
|
|
case rply, ok := <-replies:
|
|
if !ok {
|
|
// btcd disconnected
|
|
return
|
|
}
|
|
// Handle message here.
|
|
go ProcessBtcdNotificationReply(rply)
|
|
|
|
case r := <-btcdMsgs:
|
|
if err := websocket.Message.Send(ws, r); err != nil {
|
|
// btcd disconnected.
|
|
log.Errorf("Unable to send message to btcd: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
type notificationHandler func(btcjson.Cmd, []byte)
|
|
|
|
var notificationHandlers = map[string]notificationHandler{
|
|
btcws.BlockConnectedNtfnMethod: NtfnBlockConnected,
|
|
btcws.BlockDisconnectedNtfnMethod: NtfnBlockDisconnected,
|
|
btcws.TxMinedNtfnMethod: NtfnTxMined,
|
|
}
|
|
|
|
// ProcessBtcdNotificationReply unmarshalls the JSON notification or
|
|
// reply received from btcd and decides how to handle it. Replies are
|
|
// routed back to the frontend who sent the message, and wallet
|
|
// notifications are processed by btcwallet, and frontend notifications
|
|
// are sent to every connected frontend.
|
|
func ProcessBtcdNotificationReply(b []byte) {
|
|
// Idea: instead of reading btcd messages from just one websocket
|
|
// connection, maybe use two so the same connection isn't used
|
|
// for both notifications and responses? Should make handling
|
|
// must faster as unnecessary unmarshal attempts could be avoided.
|
|
|
|
// Check for notifications first.
|
|
if req, err := btcjson.ParseMarshaledCmd(b); err == nil {
|
|
// btcd should not be sending Requests except for
|
|
// notifications. Check for a nil id.
|
|
if req.Id() != nil {
|
|
// Invalid response
|
|
log.Warnf("btcd sent a non-notification JSON-RPC Request (ID: %v)",
|
|
req.Id())
|
|
return
|
|
}
|
|
|
|
// Message is a btcd notification. Check the method and dispatch
|
|
// correct handler, or if no handler, pass up to each wallet.
|
|
if ntfnHandler, ok := notificationHandlers[req.Method()]; ok {
|
|
ntfnHandler(req, b)
|
|
} else {
|
|
// No handler; send to all wallets.
|
|
frontendNotificationMaster <- b
|
|
}
|
|
return
|
|
}
|
|
|
|
// b is not a Request notification, so it must be a Response.
|
|
// Attempt to parse it as one and handle.
|
|
var r btcjson.Reply
|
|
if err := json.Unmarshal(b, &r); err != nil {
|
|
log.Warn("Unable to process btcd message as notification or response")
|
|
return
|
|
}
|
|
|
|
// Check for a valid ID.
|
|
//
|
|
// TODO(jrick): Remove this terrible ID overloading. Each
|
|
// passed-through request should be given a new unique ID number
|
|
// (reading from the NewJSONID channel) and a reply route with the
|
|
// frontend's incoming ID should be set.
|
|
if r.Id == nil {
|
|
// Responses with no IDs cannot be handled.
|
|
log.Warn("Unable to process btcd response without ID")
|
|
return
|
|
}
|
|
idStr, ok := (*r.Id).(string)
|
|
if !ok {
|
|
// btcd's responses to btcwallet should (currently, see TODO above)
|
|
// only ever be sending string IDs. If ID is not a string, log the
|
|
// error and drop the message.
|
|
log.Error("Incorrect btcd notification id type.")
|
|
return
|
|
}
|
|
|
|
var routeID uint64
|
|
var origID string
|
|
n, _ := fmt.Sscanf(idStr, "btcwallet(%d)-%s", &routeID, &origID)
|
|
if n == 1 {
|
|
// Request originated from btcwallet. Run and remove correct
|
|
// handler.
|
|
replyHandlers.Lock()
|
|
f := replyHandlers.m[routeID]
|
|
replyHandlers.Unlock()
|
|
if f != nil {
|
|
go func() {
|
|
if f(r.Result, r.Error) {
|
|
replyHandlers.Lock()
|
|
delete(replyHandlers.m, routeID)
|
|
replyHandlers.Unlock()
|
|
}
|
|
}()
|
|
}
|
|
} else if n == 2 {
|
|
// Attempt to route btcd reply to correct frontend.
|
|
replyRouter.Lock()
|
|
c := replyRouter.m[routeID]
|
|
if c != nil {
|
|
delete(replyRouter.m, routeID)
|
|
} else {
|
|
// Can't route to a frontend, drop reply.
|
|
replyRouter.Unlock()
|
|
log.Info("Unable to route btcd reply to frontend. Dropping.")
|
|
return
|
|
}
|
|
replyRouter.Unlock()
|
|
|
|
// Convert string back to number if possible.
|
|
var origIDNum float64
|
|
n, _ := fmt.Sscanf(origID, "%f", &origIDNum)
|
|
var id interface{}
|
|
if n == 1 {
|
|
id = origIDNum
|
|
} else {
|
|
id = origID
|
|
}
|
|
r.Id = &id
|
|
|
|
b, err := json.Marshal(r)
|
|
if err != nil {
|
|
log.Error("Error marshalling btcd reply. Dropping.")
|
|
return
|
|
}
|
|
c <- b
|
|
}
|
|
}
|
|
|
|
// NotifyNewBlockChainHeight notifies all frontends of a new
|
|
// blockchain height. This sends the same notification as
|
|
// btcd, so this can probably be removed.
|
|
func NotifyNewBlockChainHeight(reply chan []byte, bs wallet.BlockStamp) {
|
|
ntfn := btcws.NewBlockConnectedNtfn(bs.Hash.String(), bs.Height)
|
|
mntfn, _ := ntfn.MarshalJSON()
|
|
reply <- mntfn
|
|
}
|
|
|
|
// NtfnBlockConnected handles btcd notifications resulting from newly
|
|
// connected blocks to the main blockchain.
|
|
//
|
|
// TODO(jrick): Send block time with notification. This will be used
|
|
// to mark wallet files with a possibly-better earliest block height,
|
|
// and will greatly reduce rescan times for wallets created with an
|
|
// out of sync btcd.
|
|
func NtfnBlockConnected(n btcjson.Cmd, marshaled []byte) {
|
|
bcn, ok := n.(*btcws.BlockConnectedNtfn)
|
|
if !ok {
|
|
log.Errorf("%v handler: unexpected type", n.Method())
|
|
return
|
|
}
|
|
hash, err := btcwire.NewShaHashFromStr(bcn.Hash)
|
|
if err != nil {
|
|
log.Errorf("%v handler: invalid hash string", n.Method())
|
|
return
|
|
}
|
|
|
|
// Update the blockstamp for the newly-connected block.
|
|
bs := &wallet.BlockStamp{
|
|
Height: bcn.Height,
|
|
Hash: *hash,
|
|
}
|
|
curBlock.Lock()
|
|
curBlock.BlockStamp = *bs
|
|
curBlock.Unlock()
|
|
|
|
// btcd notifies btcwallet about transactions first, and then sends
|
|
// the new block notification. New balance notifications for txs
|
|
// in blocks are therefore sent here after all tx notifications
|
|
// have arrived and finished being processed by the handlers.
|
|
workers := NotifyBalanceRequest{
|
|
block: *hash,
|
|
wg: make(chan *sync.WaitGroup),
|
|
}
|
|
NotifyBalanceSyncerChans.access <- workers
|
|
if wg := <-workers.wg; wg != nil {
|
|
wg.Wait()
|
|
NotifyBalanceSyncerChans.remove <- *hash
|
|
}
|
|
accountstore.BlockNotify(bs)
|
|
|
|
// Pass notification to frontends too.
|
|
frontendNotificationMaster <- marshaled
|
|
}
|
|
|
|
// NtfnBlockDisconnected handles btcd notifications resulting from
|
|
// blocks disconnected from the main chain in the event of a chain
|
|
// switch and notifies frontends of the new blockchain height.
|
|
func NtfnBlockDisconnected(n btcjson.Cmd, marshaled []byte) {
|
|
bdn, ok := n.(*btcws.BlockDisconnectedNtfn)
|
|
if !ok {
|
|
log.Errorf("%v handler: unexpected type", n.Method())
|
|
return
|
|
}
|
|
hash, err := btcwire.NewShaHashFromStr(bdn.Hash)
|
|
if err != nil {
|
|
log.Errorf("%v handler: invalid hash string", n.Method())
|
|
return
|
|
}
|
|
|
|
// Rollback Utxo and Tx data stores.
|
|
go func() {
|
|
accountstore.Rollback(bdn.Height, hash)
|
|
}()
|
|
|
|
// Pass notification to frontends too.
|
|
frontendNotificationMaster <- marshaled
|
|
}
|
|
|
|
// NtfnTxMined handles btcd notifications resulting from newly
|
|
// mined transactions that originated from this wallet.
|
|
func NtfnTxMined(n btcjson.Cmd, marshaled []byte) {
|
|
tmn, ok := n.(*btcws.TxMinedNtfn)
|
|
if !ok {
|
|
log.Errorf("%v handler: unexpected type", n.Method())
|
|
return
|
|
}
|
|
|
|
txid, err := btcwire.NewShaHashFromStr(tmn.TxID)
|
|
if err != nil {
|
|
log.Errorf("%v handler: invalid hash string", n.Method())
|
|
return
|
|
}
|
|
blockhash, err := btcwire.NewShaHashFromStr(tmn.BlockHash)
|
|
if err != nil {
|
|
log.Errorf("%v handler: invalid block hash string", n.Method())
|
|
return
|
|
}
|
|
|
|
err = accountstore.RecordMinedTx(txid, blockhash,
|
|
tmn.BlockHeight, tmn.Index, tmn.BlockTime)
|
|
if err != nil {
|
|
log.Errorf("%v handler: %v", n.Method(), err)
|
|
return
|
|
}
|
|
|
|
// Remove mined transaction from pool.
|
|
UnminedTxs.Lock()
|
|
delete(UnminedTxs.m, TXID(*txid))
|
|
UnminedTxs.Unlock()
|
|
}
|
|
|
|
var duplicateOnce sync.Once
|
|
|
|
// Start starts a HTTP server to provide standard RPC and extension
|
|
// websocket connections for any number of btcwallet frontends.
|
|
func (s *server) Start() {
|
|
// We'll need to duplicate replies to frontends to each frontend.
|
|
// Replies are sent to frontendReplyMaster, and duplicated to each valid
|
|
// channel in frontendReplySet. This runs a goroutine to duplicate
|
|
// requests for each channel in the set.
|
|
//
|
|
// Use a sync.Once to insure no extra duplicators run.
|
|
go duplicateOnce.Do(frontendListenerDuplicator)
|
|
|
|
log.Trace("Starting RPC server")
|
|
|
|
serveMux := http.NewServeMux()
|
|
httpServer := &http.Server{Handler: serveMux}
|
|
serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.checkAuth(r); err != nil {
|
|
http.Error(w, "401 Unauthorized.", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
s.handleRPCRequest(w, r)
|
|
})
|
|
serveMux.HandleFunc("/frontend", func(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.checkAuth(r); err != nil {
|
|
http.Error(w, "401 Unauthorized.", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
websocket.Handler(frontendSendRecv).ServeHTTP(w, r)
|
|
})
|
|
for _, listener := range s.listeners {
|
|
s.wg.Add(1)
|
|
go func(listener net.Listener) {
|
|
log.Infof("RPCS: RPC server listening on %s", listener.Addr())
|
|
httpServer.Serve(listener)
|
|
log.Tracef("RPCS: RPC listener done for %s", listener.Addr())
|
|
s.wg.Done()
|
|
}(listener)
|
|
}
|
|
}
|
|
|
|
// checkAuth checks the HTTP Basic authentication supplied by a frontend
|
|
// in the HTTP request r. If the frontend's supplied authentication does
|
|
// not match the username and password expected, a non-nil error is
|
|
// returned.
|
|
//
|
|
// This check is time-constant.
|
|
func (s *server) checkAuth(r *http.Request) error {
|
|
authhdr := r.Header["Authorization"]
|
|
if len(authhdr) <= 0 {
|
|
log.Infof("Frontend did not supply authentication.")
|
|
return errors.New("auth failure")
|
|
}
|
|
|
|
authsha := sha256.Sum256([]byte(authhdr[0]))
|
|
cmp := subtle.ConstantTimeCompare(authsha[:], s.authsha[:])
|
|
if cmp != 1 {
|
|
log.Infof("Frontend did not supply correct authentication.")
|
|
return errors.New("auth failure")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BtcdConnect connects to a running btcd instance over a websocket
|
|
// for sending and receiving chain-related messages, failing if the
|
|
// connection cannot be established or is lost.
|
|
func BtcdConnect(certificates []byte, reply chan error) {
|
|
url := fmt.Sprintf("wss://%s/wallet", cfg.Connect)
|
|
config, err := websocket.NewConfig(url, "https://localhost/")
|
|
if err != nil {
|
|
reply <- ErrConnRefused
|
|
return
|
|
}
|
|
|
|
pool := x509.NewCertPool()
|
|
pool.AppendCertsFromPEM(certificates)
|
|
config.TlsConfig = &tls.Config{
|
|
RootCAs: pool,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Attempt to connect to running btcd instance. Bail if it fails.
|
|
var btcdws *websocket.Conn
|
|
var cerr error
|
|
if cfg.Proxy != "" {
|
|
proxy := &socks.Proxy{
|
|
Addr: cfg.Proxy,
|
|
Username: cfg.ProxyUser,
|
|
Password: cfg.ProxyPass,
|
|
}
|
|
conn, err := proxy.Dial("tcp", cfg.Connect)
|
|
if err != nil {
|
|
log.Warnf("Error connecting to proxy: %v", err)
|
|
reply <- ErrConnRefused
|
|
return
|
|
}
|
|
|
|
tlsConn := tls.Client(conn, config.TlsConfig)
|
|
btcdws, cerr = websocket.NewClient(config, tlsConn)
|
|
} else {
|
|
btcdws, cerr = websocket.DialConfig(config)
|
|
}
|
|
if cerr != nil {
|
|
log.Errorf("%s", cerr)
|
|
reply <- ErrConnRefused
|
|
return
|
|
}
|
|
reply <- nil
|
|
|
|
// Remove all reply handlers (if any exist from an old connection).
|
|
replyHandlers.Lock()
|
|
for k := range replyHandlers.m {
|
|
delete(replyHandlers.m, k)
|
|
}
|
|
replyHandlers.Unlock()
|
|
|
|
done := make(chan struct{})
|
|
BtcdHandler(btcdws, done)
|
|
|
|
if err := BtcdHandshake(btcdws); err != nil {
|
|
log.Errorf("%v", err)
|
|
reply <- ErrConnRefused
|
|
return
|
|
}
|
|
|
|
// done is closed when BtcdHandler's goroutines are finished.
|
|
<-done
|
|
reply <- ErrConnLost
|
|
}
|
|
|
|
// resendUnminedTxs resends any transactions in the unmined
|
|
// transaction pool to btcd using the 'sendrawtransaction' RPC
|
|
// command.
|
|
func resendUnminedTxs() {
|
|
for _, createdTx := range UnminedTxs.m {
|
|
n := <-NewJSONID
|
|
var id interface{} = fmt.Sprintf("btcwallet(%v)", n)
|
|
m, err := btcjson.CreateMessageWithId("sendrawtransaction", id, string(createdTx.rawTx))
|
|
if err != nil {
|
|
log.Errorf("cannot create resend request: %v", err)
|
|
continue
|
|
}
|
|
replyHandlers.Lock()
|
|
replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool {
|
|
// Do nothing, just remove the handler.
|
|
return true
|
|
}
|
|
replyHandlers.Unlock()
|
|
btcdMsgs <- m
|
|
}
|
|
}
|
|
|
|
// BtcdHandshake first checks that the websocket connection between
|
|
// btcwallet and btcd is valid, that is, that there are no mismatching
|
|
// settings between the two processes (such as running on different
|
|
// Bitcoin networks). If the sanity checks pass, all wallets are set to
|
|
// be tracked against chain notifications from this btcd connection.
|
|
//
|
|
// TODO(jrick): Track and Rescan commands should be replaced with a
|
|
// single TrackSince function (or similar) which requests address
|
|
// notifications and performs the rescan since some block height.
|
|
func BtcdHandshake(ws *websocket.Conn) error {
|
|
n := <-NewJSONID
|
|
var cmd btcjson.Cmd
|
|
cmd = btcws.NewGetCurrentNetCmd(fmt.Sprintf("btcwallet(%v)", n))
|
|
mcmd, err := cmd.MarshalJSON()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot complete btcd handshake: %v", err)
|
|
}
|
|
|
|
correctNetwork := make(chan bool)
|
|
|
|
replyHandlers.Lock()
|
|
replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool {
|
|
fnet, ok := result.(float64)
|
|
if !ok {
|
|
log.Error("btcd handshake: result is not a number")
|
|
correctNetwork <- false
|
|
return true
|
|
}
|
|
|
|
correctNetwork <- btcwire.BitcoinNet(fnet) == cfg.Net()
|
|
|
|
// No additional replies expected, remove handler.
|
|
return true
|
|
}
|
|
replyHandlers.Unlock()
|
|
|
|
btcdMsgs <- mcmd
|
|
|
|
if !<-correctNetwork {
|
|
return errors.New("btcd and btcwallet running on different Bitcoin networks")
|
|
}
|
|
|
|
// Get current best block. If this is before than the oldest
|
|
// saved block hash, assume that this btcd instance is not yet
|
|
// synced up to a previous btcd that was last used with this
|
|
// wallet.
|
|
bs, err := GetCurBlock()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot get best block: %v", err)
|
|
}
|
|
NotifyNewBlockChainHeight(frontendNotificationMaster, bs)
|
|
NotifyBalances(frontendNotificationMaster)
|
|
|
|
// Get default account. Only the default account is used to
|
|
// track recently-seen blocks.
|
|
a, err := accountstore.Account("")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// TODO(jrick): if height is less than the earliest-saved block
|
|
// height, should probably wait for btcd to catch up.
|
|
|
|
// Check that there was not any reorgs done since last connection.
|
|
// If so, rollback and rescan to catch up.
|
|
it := a.Wallet.NewIterateRecentBlocks()
|
|
for cont := it != nil; cont; cont = it.Prev() {
|
|
bs := it.BlockStamp()
|
|
log.Debugf("Checking for previous saved block with height %v hash %v",
|
|
bs.Height, bs.Hash)
|
|
|
|
n = <-NewJSONID
|
|
// NewGetBlockCmd can't fail, so don't check error.
|
|
// TODO(jrick): probably want to remove the error return value.
|
|
cmd, _ = btcjson.NewGetBlockCmd(fmt.Sprintf("btcwallet(%v)", n),
|
|
bs.Hash.String())
|
|
mcmd, _ = cmd.MarshalJSON()
|
|
|
|
blockMissing := make(chan bool)
|
|
|
|
replyHandlers.Lock()
|
|
replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) bool {
|
|
blockMissing <- err != nil && err.Code == btcjson.ErrBlockNotFound.Code
|
|
|
|
// No additional replies expected, remove handler.
|
|
return true
|
|
}
|
|
replyHandlers.Unlock()
|
|
|
|
btcdMsgs <- mcmd
|
|
|
|
if <-blockMissing {
|
|
continue
|
|
}
|
|
|
|
log.Debug("Found matching block.")
|
|
|
|
// If we had to go back to any previous blocks (it.Next
|
|
// returns true), then rollback the next and all child blocks.
|
|
// This rollback is done here instead of in the blockMissing
|
|
// check above for each removed block because Rollback will
|
|
// try to write new tx and utxo files on each rollback.
|
|
if it.Next() {
|
|
bs := it.BlockStamp()
|
|
accountstore.Rollback(bs.Height, &bs.Hash)
|
|
}
|
|
|
|
// Set default account to be marked in sync with the current
|
|
// blockstamp. This invalidates the iterator.
|
|
a.Wallet.SetSyncedWith(bs)
|
|
|
|
// Begin tracking wallets against this btcd instance.
|
|
accountstore.Track()
|
|
accountstore.RescanActiveAddresses()
|
|
|
|
// (Re)send any unmined transactions to btcd in case of a btcd restart.
|
|
resendUnminedTxs()
|
|
|
|
// Get current blockchain height and best block hash.
|
|
return nil
|
|
}
|
|
|
|
log.Warnf("None of the previous saved blocks in btcd chain. Must perform full rescan.")
|
|
|
|
// Iterator was invalid (wallet has never been synced) or there was a
|
|
// huge chain fork + reorg (more than 20 blocks). Since we don't know
|
|
// what block (if any) this wallet is synced to, roll back everything
|
|
// and start a new rescan since the earliest block wallet must know
|
|
// about.
|
|
a.fullRescan = true
|
|
accountstore.Track()
|
|
accountstore.RescanActiveAddresses()
|
|
resendUnminedTxs()
|
|
return nil
|
|
}
|