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  }