cmd/puppeth: add support for deploying web wallets
This commit is contained in:
		| @@ -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
									
								
								cmd/puppeth/module_wallet.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								cmd/puppeth/module_wallet.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -189,6 +189,7 @@ func (w *wizard) deployComponent() { | ||||
| 	case "4": | ||||
| 		w.deployExplorer() | ||||
| 	case "5": | ||||
| 		w.deployWallet() | ||||
| 	case "6": | ||||
| 		w.deployFaucet() | ||||
| 	case "7": | ||||
|   | ||||
							
								
								
									
										107
									
								
								cmd/puppeth/wizard_wallet.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								cmd/puppeth/wizard_wallet.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| } | ||||
		Reference in New Issue
	
	Block a user