Ver Fonte

cmd/puppeth: add support for deploying web wallets

Péter Szilágyi há 8 anos atrás
pai
commit
b5cf603895

+ 1 - 1
cmd/puppeth/module_explorer.go

@@ -183,7 +183,7 @@ func (info *explorerInfos) Report() map[string]string {
 	return report
 }
 
-// checkExplorer does a health-check against an boot or seal node server to verify
+// checkExplorer does a health-check against an block explorer server to verify
 // whether it's running, and if yes, whether it's responsive.
 func checkExplorer(client *sshClient, network string) (*explorerInfos, error) {
 	// Inspect a possible block explorer container on the host

+ 249 - 0
cmd/puppeth/module_wallet.go

@@ -0,0 +1,249 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"html/template"
+	"math/rand"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/ethereum/go-ethereum/log"
+)
+
+// walletDockerfile is the Dockerfile required to run a web wallet.
+var walletDockerfile = `
+FROM ethereum/client-go:latest
+
+RUN \
+	apk add --update git python make g++ libnotify nodejs-npm && \
+	npm install -g gulp-cli
+
+RUN \
+  git clone --depth=1 https://github.com/kvhnuke/etherwallet.git && \
+	(cd etherwallet && npm install)
+WORKDIR etherwallet
+
+RUN \
+	echo '"use strict";'                                                  > app/scripts/nodes.js && \
+	echo 'var nodes = function() {}'                                     >> app/scripts/nodes.js && \
+	echo 'nodes.customNode = require("./nodeHelpers/customNode");'       >> app/scripts/nodes.js && \
+	echo 'nodes.nodeTypes = {'                                           >> app/scripts/nodes.js && \
+  echo '	{{.Network}}: "{{.Denom}} ETH",'                             >> app/scripts/nodes.js && \
+	echo '	Custom: "CUSTOM ETH"'                                        >> app/scripts/nodes.js && \
+	echo '};'                                                            >> app/scripts/nodes.js && \
+	echo 'nodes.ensNodeTypes = [];'                                      >> app/scripts/nodes.js && \
+	echo 'nodes.customNodeObj = {'                                       >> app/scripts/nodes.js && \
+  echo '	"name": "CUS",'                                              >> app/scripts/nodes.js && \
+  echo '	"type": nodes.nodeTypes.Custom,'                             >> app/scripts/nodes.js && \
+  echo '	"eip155": false,'                                            >> app/scripts/nodes.js && \
+  echo '	"chainId": "",'                                              >> app/scripts/nodes.js && \
+	echo '	"tokenList": [],'                                            >> app/scripts/nodes.js && \
+	echo '	"abiList": [],'                                              >> app/scripts/nodes.js && \
+	echo '	"service": "Custom",'                                        >> app/scripts/nodes.js && \
+  echo '	"lib": null'                                                 >> app/scripts/nodes.js && \
+  echo '}'                                                             >> app/scripts/nodes.js && \
+	echo 'nodes.nodeList = {'                                            >> app/scripts/nodes.js && \
+  echo '	"eth_mew": {'                                                >> app/scripts/nodes.js && \
+  echo '		"name": "{{.Network}}",'                                   >> app/scripts/nodes.js && \
+  echo '		"type": nodes.nodeTypes.{{.Network}},'                     >> app/scripts/nodes.js && \
+  echo '		"eip155": true,'                                           >> app/scripts/nodes.js && \
+  echo '		"chainId": {{.NetworkID}},'                                >> app/scripts/nodes.js && \
+	echo '		"tokenList": [],'                                          >> app/scripts/nodes.js && \
+	echo '		"abiList": [],'                                            >> app/scripts/nodes.js && \
+	echo '		"service": "Go Ethereum",'                                 >> app/scripts/nodes.js && \
+  echo '		"lib": new nodes.customNode("http://{{.Host}}:{{.RPCPort}}", "")' >> app/scripts/nodes.js && \
+  echo '	}'                                                           >> app/scripts/nodes.js && \
+	echo '};'                                                            >> app/scripts/nodes.js && \
+	echo 'nodes.ethPrice = require("./nodeHelpers/ethPrice");'           >> app/scripts/nodes.js && \
+	echo 'module.exports = nodes;'                                       >> app/scripts/nodes.js
+
+RUN rm -rf dist && gulp prep && npm run dist
+
+RUN \
+	npm install connect serve-static && \
+	\
+	echo 'var connect = require("connect");'                                       > server.js && \
+	echo 'var serveStatic = require("serve-static");'                             >> server.js && \
+	echo 'connect().use(serveStatic("/etherwallet/dist")).listen(80, function(){' >> server.js && \
+	echo '    console.log("Server running on 80...");'                            >> server.js && \
+	echo '});'                                                                    >> server.js
+
+ADD genesis.json /genesis.json
+
+RUN \
+  echo 'node server.js &'                     > wallet.sh && \
+	echo 'geth --cache 512 init /genesis.json' >> wallet.sh && \
+	echo $'geth --networkid {{.NetworkID}} --port {{.NodePort}} --bootnodes {{.Bootnodes}} --ethstats \'{{.Ethstats}}\' --cache=512 --rpc --rpcaddr=0.0.0.0 --rpccorsdomain "*"' >> wallet.sh
+
+EXPOSE 80 8545
+
+ENTRYPOINT ["/bin/sh", "wallet.sh"]
+`
+
+// walletComposefile is the docker-compose.yml file required to deploy and
+// maintain a web wallet.
+var walletComposefile = `
+version: '2'
+services:
+  wallet:
+    build: .
+    image: {{.Network}}/wallet
+    ports:
+      - "{{.NodePort}}:{{.NodePort}}"
+      - "{{.NodePort}}:{{.NodePort}}/udp"
+      - "{{.RPCPort}}:8545"{{if not .VHost}}
+      - "{{.WebPort}}:80"{{end}}
+    volumes:
+      - {{.Datadir}}:/root/.ethereum
+    environment:
+      - NODE_PORT={{.NodePort}}/tcp
+      - STATS={{.Ethstats}}{{if .VHost}}
+      - VIRTUAL_HOST={{.VHost}}
+      - VIRTUAL_PORT=80{{end}}
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "1m"
+        max-file: "10"
+    restart: always
+`
+
+// deployWallet deploys a new web wallet container to a remote machine via SSH,
+// docker and docker-compose. If an instance with the specified network name
+// already exists there, it will be overwritten!
+func deployWallet(client *sshClient, network string, bootnodes []string, config *walletInfos, nocache bool) ([]byte, error) {
+	// Generate the content to upload to the server
+	workdir := fmt.Sprintf("%d", rand.Int63())
+	files := make(map[string][]byte)
+
+	dockerfile := new(bytes.Buffer)
+	template.Must(template.New("").Parse(walletDockerfile)).Execute(dockerfile, map[string]interface{}{
+		"Network":   strings.ToTitle(network),
+		"Denom":     strings.ToUpper(network),
+		"NetworkID": config.network,
+		"NodePort":  config.nodePort,
+		"RPCPort":   config.rpcPort,
+		"Bootnodes": strings.Join(bootnodes, ","),
+		"Ethstats":  config.ethstats,
+		"Host":      client.address,
+	})
+	files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()
+
+	composefile := new(bytes.Buffer)
+	template.Must(template.New("").Parse(walletComposefile)).Execute(composefile, map[string]interface{}{
+		"Datadir":  config.datadir,
+		"Network":  network,
+		"NodePort": config.nodePort,
+		"RPCPort":  config.rpcPort,
+		"VHost":    config.webHost,
+		"WebPort":  config.webPort,
+		"Ethstats": config.ethstats[:strings.Index(config.ethstats, ":")],
+	})
+	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()
+
+	files[filepath.Join(workdir, "genesis.json")] = []byte(config.genesis)
+
+	// Upload the deployment files to the remote server (and clean up afterwards)
+	if out, err := client.Upload(files); err != nil {
+		return out, err
+	}
+	defer client.Run("rm -rf " + workdir)
+
+	// Build and deploy the boot or seal node service
+	if nocache {
+		return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate", workdir, network, network))
+	}
+	return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network))
+}
+
+// walletInfos is returned from a web wallet status check to allow reporting
+// various configuration parameters.
+type walletInfos struct {
+	genesis  []byte
+	network  int64
+	datadir  string
+	ethstats string
+	nodePort int
+	rpcPort  int
+	webHost  string
+	webPort  int
+}
+
+// Report converts the typed struct into a plain string->string map, cotnaining
+// most - but not all - fields for reporting to the user.
+func (info *walletInfos) Report() map[string]string {
+	report := map[string]string{
+		"Data directory":         info.datadir,
+		"Ethstats username":      info.ethstats,
+		"Node listener port ":    strconv.Itoa(info.nodePort),
+		"RPC listener port ":     strconv.Itoa(info.rpcPort),
+		"Website address ":       info.webHost,
+		"Website listener port ": strconv.Itoa(info.webPort),
+	}
+	return report
+}
+
+// checkWallet does a health-check against web wallet server to verify whether
+// it's running, and if yes, whether it's responsive.
+func checkWallet(client *sshClient, network string) (*walletInfos, error) {
+	// Inspect a possible web wallet container on the host
+	infos, err := inspectContainer(client, fmt.Sprintf("%s_wallet_1", network))
+	if err != nil {
+		return nil, err
+	}
+	if !infos.running {
+		return nil, ErrServiceOffline
+	}
+	// Resolve the port from the host, or the reverse proxy
+	webPort := infos.portmap["80/tcp"]
+	if webPort == 0 {
+		if proxy, _ := checkNginx(client, network); proxy != nil {
+			webPort = proxy.port
+		}
+	}
+	if webPort == 0 {
+		return nil, ErrNotExposed
+	}
+	// Resolve the host from the reverse-proxy and the config values
+	host := infos.envvars["VIRTUAL_HOST"]
+	if host == "" {
+		host = client.server
+	}
+	// Run a sanity check to see if the devp2p and RPC ports are reachable
+	nodePort := infos.portmap[infos.envvars["NODE_PORT"]]
+	if err = checkPort(client.server, nodePort); err != nil {
+		log.Warn(fmt.Sprintf("Wallet devp2p port seems unreachable"), "server", client.server, "port", nodePort, "err", err)
+	}
+	rpcPort := infos.portmap["8545/tcp"]
+	if err = checkPort(client.server, rpcPort); err != nil {
+		log.Warn(fmt.Sprintf("Wallet RPC port seems unreachable"), "server", client.server, "port", rpcPort, "err", err)
+	}
+	// Assemble and return the useful infos
+	stats := &walletInfos{
+		datadir:  infos.volumes["/root/.ethereum"],
+		nodePort: nodePort,
+		rpcPort:  rpcPort,
+		webHost:  host,
+		webPort:  webPort,
+		ethstats: infos.envvars["STATS"],
+	}
+	return stats, nil
+}

+ 8 - 0
cmd/puppeth/wizard_netstats.go

@@ -145,6 +145,14 @@ func (w *wizard) gatherStats(server string, pubkey []byte, client *sshClient) *s
 	} else {
 		stat.services["explorer"] = infos.Report()
 	}
+	logger.Debug("Checking for wallet availability")
+	if infos, err := checkWallet(client, w.network); err != nil {
+		if err != ErrServiceUnknown {
+			stat.services["wallet"] = map[string]string{"offline": err.Error()}
+		}
+	} else {
+		stat.services["wallet"] = infos.Report()
+	}
 	logger.Debug("Checking for faucet availability")
 	if infos, err := checkFaucet(client, w.network); err != nil {
 		if err != ErrServiceUnknown {

+ 1 - 0
cmd/puppeth/wizard_network.go

@@ -189,6 +189,7 @@ func (w *wizard) deployComponent() {
 	case "4":
 		w.deployExplorer()
 	case "5":
+		w.deployWallet()
 	case "6":
 		w.deployFaucet()
 	case "7":

+ 107 - 0
cmd/puppeth/wizard_wallet.go

@@ -0,0 +1,107 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/ethereum/go-ethereum/log"
+)
+
+// deployWallet creates a new web wallet based on some user input.
+func (w *wizard) deployWallet() {
+	// Do some sanity check before the user wastes time on input
+	if w.conf.genesis == nil {
+		log.Error("No genesis block configured")
+		return
+	}
+	if w.conf.ethstats == "" {
+		log.Error("No ethstats server configured")
+		return
+	}
+	// Select the server to interact with
+	server := w.selectServer()
+	if server == "" {
+		return
+	}
+	client := w.servers[server]
+
+	// Retrieve any active node configurations from the server
+	infos, err := checkWallet(client, w.network)
+	if err != nil {
+		infos = &walletInfos{nodePort: 30303, rpcPort: 8545, webPort: 80, webHost: client.server}
+	}
+	infos.genesis, _ = json.MarshalIndent(w.conf.genesis, "", "  ")
+	infos.network = w.conf.genesis.Config.ChainId.Int64()
+
+	// Figure out which port to listen on
+	fmt.Println()
+	fmt.Printf("Which port should the wallet listen on? (default = %d)\n", infos.webPort)
+	infos.webPort = w.readDefaultInt(infos.webPort)
+
+	// Figure which virtual-host to deploy ethstats on
+	if infos.webHost, err = w.ensureVirtualHost(client, infos.webPort, infos.webHost); err != nil {
+		log.Error("Failed to decide on wallet host", "err", err)
+		return
+	}
+	// Figure out where the user wants to store the persistent data
+	fmt.Println()
+	if infos.datadir == "" {
+		fmt.Printf("Where should data be stored on the remote machine?\n")
+		infos.datadir = w.readString()
+	} else {
+		fmt.Printf("Where should data be stored on the remote machine? (default = %s)\n", infos.datadir)
+		infos.datadir = w.readDefaultString(infos.datadir)
+	}
+	// Figure out which port to listen on
+	fmt.Println()
+	fmt.Printf("Which TCP/UDP port should the backing node listen on? (default = %d)\n", infos.nodePort)
+	infos.nodePort = w.readDefaultInt(infos.nodePort)
+
+	fmt.Println()
+	fmt.Printf("Which TCP/UDP port should the backing RPC API listen on? (default = %d)\n", infos.rpcPort)
+	infos.rpcPort = w.readDefaultInt(infos.rpcPort)
+
+	// Set a proper name to report on the stats page
+	fmt.Println()
+	if infos.ethstats == "" {
+		fmt.Printf("What should the wallet be called on the stats page?\n")
+		infos.ethstats = w.readString() + ":" + w.conf.ethstats
+	} else {
+		fmt.Printf("What should the wallet be called on the stats page? (default = %s)\n", infos.ethstats)
+		infos.ethstats = w.readDefaultString(infos.ethstats) + ":" + w.conf.ethstats
+	}
+	// Try to deploy the wallet on the host
+	fmt.Println()
+	fmt.Printf("Should the wallet be built from scratch (y/n)? (default = no)\n")
+	nocache := w.readDefaultString("n") != "n"
+
+	if out, err := deployWallet(client, w.network, w.conf.bootFull, infos, nocache); err != nil {
+		log.Error("Failed to deploy wallet container", "err", err)
+		if len(out) > 0 {
+			fmt.Printf("%s\n", out)
+		}
+		return
+	}
+	// All ok, run a network scan to pick any changes up
+	log.Info("Waiting for node to finish booting")
+	time.Sleep(3 * time.Second)
+
+	w.networkStats()
+}