github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/cmd/puppeth/module_faucet.go (about) 1 // Copyright 2017 The Spectrum Authors 2 // This file is part of Spectrum. 3 // 4 // Spectrum 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 // Spectrum 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 Spectrum. 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/SmartMeshFoundation/Spectrum/common" 30 "github.com/SmartMeshFoundation/Spectrum/log" 31 ) 32 33 // faucetDockerfile is the Dockerfile required to build an 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 ports: 60 - "{{.EthPort}}:{{.EthPort}}"{{if not .VHost}} 61 - "{{.ApiPort}}:8080"{{end}} 62 volumes: 63 - {{.Datadir}}:/root/.faucet 64 environment: 65 - ETH_PORT={{.EthPort}} 66 - ETH_NAME={{.EthName}} 67 - FAUCET_AMOUNT={{.FaucetAmount}} 68 - FAUCET_MINUTES={{.FaucetMinutes}} 69 - FAUCET_TIERS={{.FaucetTiers}} 70 - CAPTCHA_TOKEN={{.CaptchaToken}} 71 - CAPTCHA_SECRET={{.CaptchaSecret}} 72 - NO_AUTH={{.NoAuth}}{{if .VHost}} 73 - VIRTUAL_HOST={{.VHost}} 74 - VIRTUAL_PORT=8080{{end}} 75 logging: 76 driver: "json-file" 77 options: 78 max-size: "1m" 79 max-file: "10" 80 restart: always 81 ` 82 83 // deployFaucet deploys a new faucet container to a remote machine via SSH, 84 // docker and docker-compose. If an instance with the specified network name 85 // already exists there, it will be overwritten! 86 func deployFaucet(client *sshClient, network string, bootnodes []string, config *faucetInfos, nocache bool) ([]byte, error) { 87 // Generate the content to upload to the server 88 workdir := fmt.Sprintf("%d", rand.Int63()) 89 files := make(map[string][]byte) 90 91 dockerfile := new(bytes.Buffer) 92 template.Must(template.New("").Parse(faucetDockerfile)).Execute(dockerfile, map[string]interface{}{ 93 "NetworkID": config.node.network, 94 "Bootnodes": strings.Join(bootnodes, ","), 95 "Ethstats": config.node.ethstats, 96 "EthPort": config.node.portFull, 97 "CaptchaToken": config.captchaToken, 98 "CaptchaSecret": config.captchaSecret, 99 "FaucetName": strings.Title(network), 100 "FaucetAmount": config.amount, 101 "FaucetMinutes": config.minutes, 102 "FaucetTiers": config.tiers, 103 "NoAuth": config.noauth, 104 }) 105 files[filepath.Join(workdir, "Dockerfile")] = dockerfile.Bytes() 106 107 composefile := new(bytes.Buffer) 108 template.Must(template.New("").Parse(faucetComposefile)).Execute(composefile, map[string]interface{}{ 109 "Network": network, 110 "Datadir": config.node.datadir, 111 "VHost": config.host, 112 "ApiPort": config.port, 113 "EthPort": config.node.portFull, 114 "EthName": config.node.ethstats[:strings.Index(config.node.ethstats, ":")], 115 "CaptchaToken": config.captchaToken, 116 "CaptchaSecret": config.captchaSecret, 117 "FaucetAmount": config.amount, 118 "FaucetMinutes": config.minutes, 119 "FaucetTiers": config.tiers, 120 "NoAuth": config.noauth, 121 }) 122 files[filepath.Join(workdir, "docker-compose.yaml")] = composefile.Bytes() 123 124 files[filepath.Join(workdir, "genesis.json")] = config.node.genesis 125 files[filepath.Join(workdir, "account.json")] = []byte(config.node.keyJSON) 126 files[filepath.Join(workdir, "account.pass")] = []byte(config.node.keyPass) 127 128 // Upload the deployment files to the remote server (and clean up afterwards) 129 if out, err := client.Upload(files); err != nil { 130 return out, err 131 } 132 defer client.Run("rm -rf " + workdir) 133 134 // Build and deploy the faucet service 135 if nocache { 136 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)) 137 } 138 return nil, client.Stream(fmt.Sprintf("cd %s && docker-compose -p %s up -d --build --force-recreate", workdir, network)) 139 } 140 141 // faucetInfos is returned from an faucet status check to allow reporting various 142 // configuration parameters. 143 type faucetInfos struct { 144 node *nodeInfos 145 host string 146 port int 147 amount int 148 minutes int 149 tiers int 150 noauth bool 151 captchaToken string 152 captchaSecret string 153 } 154 155 // Report converts the typed struct into a plain string->string map, containing 156 // most - but not all - fields for reporting to the user. 157 func (info *faucetInfos) Report() map[string]string { 158 report := map[string]string{ 159 "Website address": info.host, 160 "Website listener port": strconv.Itoa(info.port), 161 "Ethereum listener port": strconv.Itoa(info.node.portFull), 162 "Funding amount (base tier)": fmt.Sprintf("%d Ethers", info.amount), 163 "Funding cooldown (base tier)": fmt.Sprintf("%d mins", info.minutes), 164 "Funding tiers": strconv.Itoa(info.tiers), 165 "Captha protection": fmt.Sprintf("%v", info.captchaToken != ""), 166 "Ethstats username": info.node.ethstats, 167 } 168 if info.noauth { 169 report["Debug mode (no auth)"] = "enabled" 170 } 171 if info.node.keyJSON != "" { 172 var key struct { 173 Address string `json:"address"` 174 } 175 if err := json.Unmarshal([]byte(info.node.keyJSON), &key); err == nil { 176 report["Funding account"] = common.HexToAddress(key.Address).Hex() 177 } else { 178 log.Error("Failed to retrieve signer address", "err", err) 179 } 180 } 181 return report 182 } 183 184 // checkFaucet does a health-check against an faucet server to verify whether 185 // it's running, and if yes, gathering a collection of useful infos about it. 186 func checkFaucet(client *sshClient, network string) (*faucetInfos, error) { 187 // Inspect a possible faucet container on the host 188 infos, err := inspectContainer(client, fmt.Sprintf("%s_faucet_1", network)) 189 if err != nil { 190 return nil, err 191 } 192 if !infos.running { 193 return nil, ErrServiceOffline 194 } 195 // Resolve the port from the host, or the reverse proxy 196 port := infos.portmap["8080/tcp"] 197 if port == 0 { 198 if proxy, _ := checkNginx(client, network); proxy != nil { 199 port = proxy.port 200 } 201 } 202 if port == 0 { 203 return nil, ErrNotExposed 204 } 205 // Resolve the host from the reverse-proxy and the config values 206 host := infos.envvars["VIRTUAL_HOST"] 207 if host == "" { 208 host = client.server 209 } 210 amount, _ := strconv.Atoi(infos.envvars["FAUCET_AMOUNT"]) 211 minutes, _ := strconv.Atoi(infos.envvars["FAUCET_MINUTES"]) 212 tiers, _ := strconv.Atoi(infos.envvars["FAUCET_TIERS"]) 213 214 // Retrieve the funding account informations 215 var out []byte 216 keyJSON, keyPass := "", "" 217 if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.json", network)); err == nil { 218 keyJSON = string(bytes.TrimSpace(out)) 219 } 220 if out, err = client.Run(fmt.Sprintf("docker exec %s_faucet_1 cat /account.pass", network)); err == nil { 221 keyPass = string(bytes.TrimSpace(out)) 222 } 223 // Run a sanity check to see if the port is reachable 224 if err = checkPort(host, port); err != nil { 225 log.Warn("Faucet service seems unreachable", "server", host, "port", port, "err", err) 226 } 227 // Container available, assemble and return the useful infos 228 return &faucetInfos{ 229 node: &nodeInfos{ 230 datadir: infos.volumes["/root/.faucet"], 231 portFull: infos.portmap[infos.envvars["ETH_PORT"]+"/tcp"], 232 ethstats: infos.envvars["ETH_NAME"], 233 keyJSON: keyJSON, 234 keyPass: keyPass, 235 }, 236 host: host, 237 port: port, 238 amount: amount, 239 minutes: minutes, 240 tiers: tiers, 241 captchaToken: infos.envvars["CAPTCHA_TOKEN"], 242 captchaSecret: infos.envvars["CAPTCHA_SECRET"], 243 noauth: infos.envvars["NO_AUTH"] == "true", 244 }, nil 245 }