github.com/zhiqiangxu/go-ethereum@v1.9.16-0.20210824055606-be91cfdebc48/cmd/puppeth/module_faucet.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "html/template" 24 "math/rand" 25 "path/filepath" 26 "strconv" 27 "strings" 28 29 "github.com/zhiqiangxu/go-ethereum/common" 30 "github.com/zhiqiangxu/go-ethereum/log" 31 ) 32 33 // faucetDockerfile is the Dockerfile required to build a faucet container to 34 // grant crypto tokens based on GitHub authentications. 35 var faucetDockerfile = ` 36 FROM ethereum/client-go:alltools-latest 37 38 ADD genesis.json /genesis.json 39 ADD account.json /account.json 40 ADD account.pass /account.pass 41 42 EXPOSE 8080 30303 30303/udp 43 44 ENTRYPOINT [ \ 45 "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 // faucetComposefile is the docker-compose.yml file required to deploy and maintain 52 // a crypto faucet. 53 var faucetComposefile = ` 54 version: '2' 55 services: 56 faucet: 57 build: . 58 image: {{.Network}}/faucet 59 container_name: {{.Network}}_faucet_1 60 ports: 61 - "{{.EthPort}}:{{.EthPort}}" 62 - "{{.EthPort}}:{{.EthPort}}/udp"{{if not .VHost}} 63 - "{{.ApiPort}}:8080"{{end}} 64 volumes: 65 - {{.Datadir}}:/root/.faucet 66 environment: 67 - ETH_PORT={{.EthPort}} 68 - ETH_NAME={{.EthName}} 69 - FAUCET_AMOUNT={{.FaucetAmount}} 70 - FAUCET_MINUTES={{.FaucetMinutes}} 71 - FAUCET_TIERS={{.FaucetTiers}} 72 - CAPTCHA_TOKEN={{.CaptchaToken}} 73 - CAPTCHA_SECRET={{.CaptchaSecret}} 74 - NO_AUTH={{.NoAuth}}{{if .VHost}} 75 - VIRTUAL_HOST={{.VHost}} 76 - VIRTUAL_PORT=8080{{end}} 77 logging: 78 driver: "json-file" 79 options: 80 max-size: "1m" 81 max-file: "10" 82 restart: always 83 ` 84 85 // deployFaucet deploys a new faucet container to a remote machine via SSH, 86 // docker and docker-compose. If an instance with the specified network name 87 // already exists there, it will be overwritten! 88 func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos, nocache bool) ([]byte, error) { 89 // Generate the content to upload to the server 90 workdir := fmt.Sprintf("%d", rand.Int63()) 91 files := make(map[string][]byte) 92 93 dockerfile := new(bytes.Buffer) 94 template.Must(template.New("").Parse(faucetDockerfile)).Execute(dockerfile, map[string]interface{}{ 95 "NetworkID": config.node.network, 96 "Bootnodes": strings.Join(bootnodes, ","), 97 "Ethstats": config.node.ethstats, 98 "EthPort": config.node.port, 99 "CaptchaToken": config.captchaToken, 100 "CaptchaSecret": config.captchaSecret, 101 "FaucetName": strings.Title(network), 102 "FaucetAmount": config.amount, 103 "FaucetMinutes": config.minutes, 104 "FaucetTiers": config.tiers, 105 "NoAuth": config.noauth, 106 }) 107 files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() 108 109 composefile := new(bytes.Buffer) 110 template.Must(template.New("").Parse(faucetComposefile)).Execute(composefile, map[string]interface{}{ 111 "Network": network, 112 "Datadir": config.node.datadir, 113 "VHost": config.host, 114 "ApiPort": config.port, 115 "EthPort": config.node.port, 116 "EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")], 117 "CaptchaToken": config.captchaToken, 118 "CaptchaSecret": config.captchaSecret, 119 "FaucetAmount": config.amount, 120 "FaucetMinutes": config.minutes, 121 "FaucetTiers": config.tiers, 122 "NoAuth": config.noauth, 123 }) 124 files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() 125 126 files[filepath.Join(workdir, "genesis.json")] = config.node.genesis 127 files[filepath.Join(workdir, "account.json")] = []byte(config.node.keyJSON) 128 files[filepath.Join(workdir, "account.pass")] = []byte(config.node.keyPass) 129 130 // Upload the deployment files to the remote server (and clean up afterwards) 131 if out, err := client.Upload(files); err != nil { 132 return out, err 133 } 134 defer client.Run("rm -rf " + workdir) 135 136 // Build and deploy the faucet service 137 if nocache { 138 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s build --pull --no-cache && docker-compose -p %s up -d --force-recreate --timeout 60", workdir, network, network)) 139 } 140 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate --timeout 60", workdir, network)) 141 } 142 143 // faucetInfos is returned from a faucet status check to allow reporting various 144 // configuration parameters. 145 type faucetInfos struct { 146 node *nodeInfos 147 host string 148 port int 149 amount int 150 minutes int 151 tiers int 152 noauth bool 153 captchaToken string 154 captchaSecret string 155 } 156 157 // Report converts the typed struct into a plain string->string map, containing 158 // most - but not all - fields for reporting to the user. 159 func (info *faucetInfos) Report() map[string]string { 160 report := map[string]string{ 161 "Website address": info.host, 162 "Website listener port": strconv.Itoa(info.port), 163 "Ethereum listener port": strconv.Itoa(info.node.port), 164 "Funding amount (base tier)": fmt.Sprintf("%d Ethers", info.amount), 165 "Funding cooldown (base tier)": fmt.Sprintf("%d mins", info.minutes), 166 "Funding tiers": strconv.Itoa(info.tiers), 167 "Captha protection": fmt.Sprintf("%v", info.captchaToken != ""), 168 "Ethstats username": info.node.ethstats, 169 } 170 if info.noauth { 171 report["Debug mode (no auth)"] = "enabled" 172 } 173 if info.node.keyJSON != "" { 174 var key struct { 175 Address string `json:"address"` 176 } 177 if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil { 178 report["Funding account"] = common.HexToAddress(key.Address).Hex() 179 } else { 180 log.Error("Failed to retrieve signer address", "err", err) 181 } 182 } 183 return report 184 } 185 186 // checkFaucet does a health-check against a faucet server to verify whether 187 // it's running, and if yes, gathering a collection of useful infos about it. 188 func checkFaucet(client *sshClient, network string) (*faucetInfos, error) { 189 // Inspect a possible faucet container on the host 190 infos, err := inspectContainer(client, fmt.Sprintf("%s_faucet_1", network)) 191 if err != nil { 192 return nil, err 193 } 194 if !infos.running { 195 return nil, ErrServiceOffline 196 } 197 // Resolve the port from the host, or the reverse proxy 198 port := infos.portmap["8080/tcp"] 199 if port == 0 { 200 if proxy, _ := checkNginx(client, network); proxy != nil { 201 port = proxy.port 202 } 203 } 204 if port == 0 { 205 return nil, ErrNotExposed 206 } 207 // Resolve the host from the reverse-proxy and the config values 208 host := infos.envvars["VIRTUAL_HOST"] 209 if host == "" { 210 host = client.server 211 } 212 amount, _ := strconv.Atoi(infos.envvars["FAUCET_AMOUNT"]) 213 minutes, _ := strconv.Atoi(infos.envvars["FAUCET_MINUTES"]) 214 tiers, _ := strconv.Atoi(infos.envvars["FAUCET_TIERS"]) 215 216 // Retrieve the funding account information 217 var out []byte 218 keyJSON, keyPass := "", "" 219 if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.json", network)); err == nil { 220 keyJSON = string(bytes.TrimSpace(out)) 221 } 222 if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.pass", network)); err == nil { 223 keyPass = string(bytes.TrimSpace(out)) 224 } 225 // Run a sanity check to see if the port is reachable 226 if err = checkPort(host, port); err != nil { 227 log.Warn("Faucet service seems unreachable", "server", host, "port", port, "err", err) 228 } 229 // Container available, assemble and return the useful infos 230 return &faucetInfos{ 231 node: &nodeInfos{ 232 datadir: infos.volumes["/root/.faucet"], 233 port: infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"], 234 ethstats: infos.envvars["ETH_NAME"], 235 keyJSON: keyJSON, 236 keyPass: keyPass, 237 }, 238 host: host, 239 port: port, 240 amount: amount, 241 minutes: minutes, 242 tiers: tiers, 243 captchaToken: infos.envvars["CAPTCHA_TOKEN"], 244 captchaSecret: infos.envvars["CAPTCHA_SECRET"], 245 noauth: infos.envvars["NO_AUTH"] == "true", 246 }, nil 247 }