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