module_faucet.go 8.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 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"
21
	"encoding/json"
22 23 24 25 26 27 28
	"fmt"
	"html/template"
	"math/rand"
	"path/filepath"
	"strconv"
	"strings"

29
	"github.com/ethereum/go-ethereum/common"
30 31 32 33 34 35
	"github.com/ethereum/go-ethereum/log"
)

// faucetDockerfile is the Dockerfile required to build an faucet container to
// grant crypto tokens based on GitHub authentications.
var faucetDockerfile = `
36
FROM ethereum/client-go:alltools-latest
37 38 39 40 41

ADD genesis.json /genesis.json
ADD account.json /account.json
ADD account.pass /account.pass

42 43
EXPOSE 8080 30303 30303/udp

44 45
ENTRYPOINT [ \
	"faucet", "--genesis", "/genesis.json", "--network", "{{.NetworkID}}", "--bootnodes", "{{.Bootnodes}}", "--ethstats", "{{.Ethstats}}", "--ethport", "{{.EthPort}}",     \
46
	"--faucet.name", "{{.FaucetName}}", "--faucet.amount", "{{.FaucetAmount}}", "--faucet.minutes", "{{.FaucetMinutes}}", "--faucet.tiers", "{{.FaucetTiers}}",             \
47
	"--account.json", "/account.json", "--account.pass", "/account.pass"                                                                                                    \
48
	{{if .CaptchaToken}}, "--captcha.token", "{{.CaptchaToken}}", "--captcha.secret", "{{.CaptchaSecret}}"{{end}}{{if .NoAuth}}, "--noauth"{{end}}                          \
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
]`

// faucetComposefile is the docker-compose.yml file required to deploy and maintain
// a crypto faucet.
var faucetComposefile = `
version: '2'
services:
  faucet:
    build: .
    image: {{.Network}}/faucet
    ports:
      - "{{.EthPort}}:{{.EthPort}}"{{if not .VHost}}
      - "{{.ApiPort}}:8080"{{end}}
    volumes:
      - {{.Datadir}}:/root/.faucet
    environment:
      - ETH_PORT={{.EthPort}}
      - ETH_NAME={{.EthName}}
      - FAUCET_AMOUNT={{.FaucetAmount}}
      - FAUCET_MINUTES={{.FaucetMinutes}}
69
      - FAUCET_TIERS={{.FaucetTiers}}
70
      - CAPTCHA_TOKEN={{.CaptchaToken}}
71 72
      - CAPTCHA_SECRET={{.CaptchaSecret}}
      - NO_AUTH={{.NoAuth}}{{if .VHost}}
73 74
      - VIRTUAL_HOST={{.VHost}}
      - VIRTUAL_PORT=8080{{end}}
75 76 77 78 79
    logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "10"
80 81 82 83 84 85
    restart: always
`

// deployFaucet deploys a new faucet 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!
86
func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos, nocache bool) ([]byte, error) {
87 88 89 90 91 92 93 94 95
	// 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(faucetDockerfile)).Execute(dockerfile, map[string]interface{}{
		"NetworkID":     config.node.network,
		"Bootnodes":     strings.Join(bootnodes, ","),
		"Ethstats":      config.node.ethstats,
96
		"EthPort":       config.node.port,
97 98
		"CaptchaToken":  config.captchaToken,
		"CaptchaSecret": config.captchaSecret,
99 100 101
		"FaucetName":    strings.Title(network),
		"FaucetAmount":  config.amount,
		"FaucetMinutes": config.minutes,
102
		"FaucetTiers":   config.tiers,
103
		"NoAuth":        config.noauth,
104 105 106 107 108 109 110 111 112
	})
	files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes()

	composefile := new(bytes.Buffer)
	template.Must(template.New("").Parse(faucetComposefile)).Execute(composefile, map[string]interface{}{
		"Network":       network,
		"Datadir":       config.node.datadir,
		"VHost":         config.host,
		"ApiPort":       config.port,
113
		"EthPort":       config.node.port,
114
		"EthName":       config.node.ethstats[:strings.Index(config.node.ethstats, ":")],
115 116
		"CaptchaToken":  config.captchaToken,
		"CaptchaSecret": config.captchaSecret,
117 118
		"FaucetAmount":  config.amount,
		"FaucetMinutes": config.minutes,
119
		"FaucetTiers":   config.tiers,
120
		"NoAuth":        config.noauth,
121 122 123
	})
	files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes()

124
	files[filepath.Join(workdir, "genesis.json")] = config.node.genesis
125 126 127 128 129 130 131 132 133 134
	files[filepath.Join(workdir, "account.json")] = []byte(config.node.keyJSON)
	files[filepath.Join(workdir, "account.pass")] = []byte(config.node.keyPass)

	// 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 faucet service
135 136 137 138
	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))
139 140 141 142 143
}

// faucetInfos is returned from an faucet status check to allow reporting various
// configuration parameters.
type faucetInfos struct {
144 145 146 147 148
	node          *nodeInfos
	host          string
	port          int
	amount        int
	minutes       int
149
	tiers         int
150
	noauth        bool
151 152
	captchaToken  string
	captchaSecret string
153 154
}

155
// Report converts the typed struct into a plain string->string map, containing
156 157 158 159 160
// most - but not all - fields for reporting to the user.
func (info *faucetInfos) Report() map[string]string {
	report := map[string]string{
		"Website address":              info.host,
		"Website listener port":        strconv.Itoa(info.port),
161
		"Ethereum listener port":       strconv.Itoa(info.node.port),
162 163 164 165 166
		"Funding amount (base tier)":   fmt.Sprintf("%d Ethers", info.amount),
		"Funding cooldown (base tier)": fmt.Sprintf("%d mins", info.minutes),
		"Funding tiers":                strconv.Itoa(info.tiers),
		"Captha protection":            fmt.Sprintf("%v", info.captchaToken != ""),
		"Ethstats username":            info.node.ethstats,
167 168 169
	}
	if info.noauth {
		report["Debug mode (no auth)"] = "enabled"
170 171 172 173 174 175 176 177 178 179 180 181
	}
	if info.node.keyJSON != "" {
		var key struct {
			Address string `json:"address"`
		}
		if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil {
			report["Funding account"] = common.HexToAddress(key.Address).Hex()
		} else {
			log.Error("Failed to retrieve signer address", "err", err)
		}
	}
	return report
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
}

// checkFaucet does a health-check against an faucet server to verify whether
// it's running, and if yes, gathering a collection of useful infos about it.
func checkFaucet(client *sshClient, network string) (*faucetInfos, error) {
	// Inspect a possible faucet container on the host
	infos, err := inspectContainer(client, fmt.Sprintf("%s_faucet_1", network))
	if err != nil {
		return nil, err
	}
	if !infos.running {
		return nil, ErrServiceOffline
	}
	// Resolve the port from the host, or the reverse proxy
	port := infos.portmap["8080/tcp"]
	if port == 0 {
		if proxy, _ := checkNginx(client, network); proxy != nil {
			port = proxy.port
		}
	}
	if port == 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
	}
	amount, _ := strconv.Atoi(infos.envvars["FAUCET_AMOUNT"])
	minutes, _ := strconv.Atoi(infos.envvars["FAUCET_MINUTES"])
212
	tiers, _ := strconv.Atoi(infos.envvars["FAUCET_TIERS"])
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230

	// Retrieve the funding account informations
	var out []byte
	keyJSON, keyPass := "", ""
	if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.json", network)); err == nil {
		keyJSON = string(bytes.TrimSpace(out))
	}
	if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.pass", network)); err == nil {
		keyPass = string(bytes.TrimSpace(out))
	}
	// Run a sanity check to see if the port is reachable
	if err = checkPort(host, port); err != nil {
		log.Warn("Faucet service seems unreachable", "server", host, "port", port, "err", err)
	}
	// Container available, assemble and return the useful infos
	return &faucetInfos{
		node: &nodeInfos{
			datadir:  infos.volumes["/root/.faucet"],
231
			port:     infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"],
232 233 234 235
			ethstats: infos.envvars["ETH_NAME"],
			keyJSON:  keyJSON,
			keyPass:  keyPass,
		},
236 237 238 239
		host:          host,
		port:          port,
		amount:        amount,
		minutes:       minutes,
240
		tiers:         tiers,
241 242
		captchaToken:  infos.envvars["CAPTCHA_TOKEN"],
		captchaSecret: infos.envvars["CAPTCHA_SECRET"],
243
		noauth:        infos.envvars["NO_AUTH"] == "true",
244 245
	}, nil
}