diff --git a/cmd.go b/cmd.go index b506f66..689ab53 100644 --- a/cmd.go +++ b/cmd.go @@ -316,14 +316,15 @@ func main() { go DirtyAccountSyncer() go func() { + s, err := newServer() + if err != nil { + log.Errorf("Unable to create HTTP server: %v", err) + os.Exit(1) + } + // Start HTTP server to listen and send messages to frontend and btcd // backend. Try reconnection if connection failed. - for { - if err := FrontendListenAndServe(); err != nil { - log.Info("Unable to start frontend HTTP server: %v", err) - os.Exit(1) - } - } + s.Start() }() // Begin generating new IDs for JSON calls. diff --git a/cmdmgr.go b/cmdmgr.go index 1ea9f16..d6e841e 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -37,7 +37,7 @@ var ( type cmdHandler func(chan []byte, btcjson.Cmd) -var handlers = map[string]cmdHandler{ +var rpcHandlers = map[string]cmdHandler{ // Standard bitcoind methods "getaddressesbyaccount": GetAddressesByAccount, "getbalance": GetBalance, @@ -49,16 +49,20 @@ var handlers = map[string]cmdHandler{ "walletlock": WalletLock, "walletpassphrase": WalletPassphrase, - // btcwallet extensions + // Extensions not exclusive to websocket connections. "createencryptedwallet": CreateEncryptedWallet, - "getbalances": GetBalances, - "walletislocked": WalletIsLocked, +} + +// Extensions exclusive to websocket connections. +var wsHandlers = map[string]cmdHandler{ + "getbalances": GetBalances, + "walletislocked": WalletIsLocked, } // ProcessFrontendMsg checks the message sent from a frontend. If the // message method is one that must be handled by btcwallet, the request // is processed here. Otherwise, the message is sent to btcd. -func ProcessFrontendMsg(frontend chan []byte, msg []byte) { +func ProcessFrontendMsg(frontend chan []byte, msg []byte, ws bool) { // Parse marshaled command and check cmd, err := btcjson.ParseMarshaledCmd(msg) if err != nil { @@ -71,13 +75,14 @@ func ProcessFrontendMsg(frontend chan []byte, msg []byte) { // btcwallet cannot handle this command, so defer handling // to btcd. - fmt.Printf("deferring %v with error %v\n", string(msg), err) DeferToBTCD(frontend, msg) return } // Check for a handler to reply to cmd. If none exist, defer to btcd. - if f, ok := handlers[cmd.Method()]; ok { + if f, ok := rpcHandlers[cmd.Method()]; ok { + f(frontend, cmd) + } else if f, ok := wsHandlers[cmd.Method()]; ws && ok { f(frontend, cmd) } else { // btcwallet does not have a handler for the command. Pass diff --git a/sockets.go b/sockets.go index cca99a6..c0323b1 100644 --- a/sockets.go +++ b/sockets.go @@ -84,6 +84,59 @@ var ( } ) +// server holds the items the RPC server may need to access (auth, +// config, shutdown, etc.) +type server struct { + port string + wg sync.WaitGroup + listeners []net.Listener +} + +// newServer returns a new instance of the server struct. +func newServer() (*server, error) { + s := server{ + port: cfg.SvrPort, + } + + // IPv4 listener. + var listeners []net.Listener + listenAddr4 := net.JoinHostPort("127.0.0.1", s.port) + listener4, err := net.Listen("tcp4", listenAddr4) + if err != nil { + log.Errorf("RPCS: Couldn't create listener: %v", err) + return nil, err + } + listeners = append(listeners, listener4) + + // IPv6 listener. + listenAddr6 := net.JoinHostPort("::1", s.port) + listener6, err := net.Listen("tcp6", listenAddr6) + if err != nil { + log.Errorf("RPCS: Couldn't create listener: %v", err) + return nil, err + } + listeners = append(listeners, listener6) + + s.listeners = listeners + + return &s, 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) + } + + ProcessFrontendMsg(frontend, body, false) + if _, err := w.Write(<-frontend); err != nil { + log.Warnf("RPCS: could not respond to RPC request: %v", err) + } +} + // frontendListenerDuplicator listens for new wallet listener channels // and duplicates messages sent to frontendNotificationMaster to all // connected listeners. @@ -158,12 +211,12 @@ func NotifyBtcdConnected(reply chan []byte, conn bool) { frontendNotificationMaster <- ntfn } -// frontendReqsNotifications is the handler function for websocket -// connections from a btcwallet instance. It reads messages from wallet and -// sends back replies, as well as notififying wallets of chain updates. -// There can possibly be many of these running, one for each currently -// connected frontend. -func frontendReqsNotifications(ws *websocket.Conn) { +// 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) @@ -195,8 +248,8 @@ func frontendReqsNotifications(ws *websocket.Conn) { // frontend disconnected. return } - // Handle JSON message here. - go ProcessFrontendMsg(frontendNotification, m) + // Handle request here. + go ProcessFrontendMsg(frontendNotification, m, true) case ntfn, _ := <-frontendNotification: if err := websocket.Message.Send(ws, ntfn); err != nil { // Frontend disconnected. @@ -484,9 +537,9 @@ func NtfnTxMined(n btcws.Notification) { var duplicateOnce sync.Once -// FrontendListenAndServe starts a HTTP server to provide websocket -// connections for any number of btcwallet frontends. -func FrontendListenAndServe() error { +// 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 @@ -497,8 +550,21 @@ func FrontendListenAndServe() error { // TODO(jrick): We need some sort of authentication before websocket // connections are allowed, and perhaps TLS on the server as well. - http.Handle("/frontend", websocket.Handler(frontendReqsNotifications)) - return http.ListenAndServe(net.JoinHostPort("", cfg.SvrPort), nil) + serveMux := http.NewServeMux() + httpServer := &http.Server{Handler: serveMux} + serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + s.handleRPCRequest(w, r) + }) + serveMux.Handle("/frontend", websocket.Handler(frontendSendRecv)) + 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) + } } // BtcdConnect connects to a running btcd instance over a websocket