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