github.com/ethw3/go-ethereuma@v0.0.0-20221013053120-c14602a4c23c/cmd/devp2p/dnscmd.go (about)

     1  // Copyright 2019 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  	"crypto/ecdsa"
    21  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"time"
    26  
    27  	"github.com/ethw3/go-ethereuma/accounts/keystore"
    28  	"github.com/ethw3/go-ethereuma/common"
    29  	"github.com/ethw3/go-ethereuma/console/prompt"
    30  	"github.com/ethw3/go-ethereuma/p2p/dnsdisc"
    31  	"github.com/ethw3/go-ethereuma/p2p/enode"
    32  	"github.com/urfave/cli/v2"
    33  )
    34  
    35  var (
    36  	dnsCommand = &cli.Command{
    37  		Name:  "dns",
    38  		Usage: "DNS Discovery Commands",
    39  		Subcommands: []*cli.Command{
    40  			dnsSyncCommand,
    41  			dnsSignCommand,
    42  			dnsTXTCommand,
    43  			dnsCloudflareCommand,
    44  			dnsRoute53Command,
    45  			dnsRoute53NukeCommand,
    46  		},
    47  	}
    48  	dnsSyncCommand = &cli.Command{
    49  		Name:      "sync",
    50  		Usage:     "Download a DNS discovery tree",
    51  		ArgsUsage: "<url> [ <directory> ]",
    52  		Action:    dnsSync,
    53  		Flags:     []cli.Flag{dnsTimeoutFlag},
    54  	}
    55  	dnsSignCommand = &cli.Command{
    56  		Name:      "sign",
    57  		Usage:     "Sign a DNS discovery tree",
    58  		ArgsUsage: "<tree-directory> <key-file>",
    59  		Action:    dnsSign,
    60  		Flags:     []cli.Flag{dnsDomainFlag, dnsSeqFlag},
    61  	}
    62  	dnsTXTCommand = &cli.Command{
    63  		Name:      "to-txt",
    64  		Usage:     "Create a DNS TXT records for a discovery tree",
    65  		ArgsUsage: "<tree-directory> <output-file>",
    66  		Action:    dnsToTXT,
    67  	}
    68  	dnsCloudflareCommand = &cli.Command{
    69  		Name:      "to-cloudflare",
    70  		Usage:     "Deploy DNS TXT records to CloudFlare",
    71  		ArgsUsage: "<tree-directory>",
    72  		Action:    dnsToCloudflare,
    73  		Flags:     []cli.Flag{cloudflareTokenFlag, cloudflareZoneIDFlag},
    74  	}
    75  	dnsRoute53Command = &cli.Command{
    76  		Name:      "to-route53",
    77  		Usage:     "Deploy DNS TXT records to Amazon Route53",
    78  		ArgsUsage: "<tree-directory>",
    79  		Action:    dnsToRoute53,
    80  		Flags: []cli.Flag{
    81  			route53AccessKeyFlag,
    82  			route53AccessSecretFlag,
    83  			route53ZoneIDFlag,
    84  			route53RegionFlag,
    85  		},
    86  	}
    87  	dnsRoute53NukeCommand = &cli.Command{
    88  		Name:      "nuke-route53",
    89  		Usage:     "Deletes DNS TXT records of a subdomain on Amazon Route53",
    90  		ArgsUsage: "<domain>",
    91  		Action:    dnsNukeRoute53,
    92  		Flags: []cli.Flag{
    93  			route53AccessKeyFlag,
    94  			route53AccessSecretFlag,
    95  			route53ZoneIDFlag,
    96  			route53RegionFlag,
    97  		},
    98  	}
    99  )
   100  
   101  var (
   102  	dnsTimeoutFlag = &cli.DurationFlag{
   103  		Name:  "timeout",
   104  		Usage: "Timeout for DNS lookups",
   105  	}
   106  	dnsDomainFlag = &cli.StringFlag{
   107  		Name:  "domain",
   108  		Usage: "Domain name of the tree",
   109  	}
   110  	dnsSeqFlag = &cli.UintFlag{
   111  		Name:  "seq",
   112  		Usage: "New sequence number of the tree",
   113  	}
   114  )
   115  
   116  const (
   117  	rootTTL               = 30 * 60              // 30 min
   118  	treeNodeTTL           = 4 * 7 * 24 * 60 * 60 // 4 weeks
   119  	treeNodeTTLCloudflare = 24 * 60 * 60         // 1 day
   120  )
   121  
   122  // dnsSync performs dnsSyncCommand.
   123  func dnsSync(ctx *cli.Context) error {
   124  	var (
   125  		c      = dnsClient(ctx)
   126  		url    = ctx.Args().Get(0)
   127  		outdir = ctx.Args().Get(1)
   128  	)
   129  	domain, _, err := dnsdisc.ParseURL(url)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	if outdir == "" {
   134  		outdir = domain
   135  	}
   136  
   137  	t, err := c.SyncTree(url)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	def := treeToDefinition(url, t)
   142  	def.Meta.LastModified = time.Now()
   143  	writeTreeMetadata(outdir, def)
   144  	writeTreeNodes(outdir, def)
   145  	return nil
   146  }
   147  
   148  func dnsSign(ctx *cli.Context) error {
   149  	if ctx.NArg() < 2 {
   150  		return fmt.Errorf("need tree definition directory and key file as arguments")
   151  	}
   152  	var (
   153  		defdir  = ctx.Args().Get(0)
   154  		keyfile = ctx.Args().Get(1)
   155  		def     = loadTreeDefinition(defdir)
   156  		domain  = directoryName(defdir)
   157  	)
   158  	if def.Meta.URL != "" {
   159  		d, _, err := dnsdisc.ParseURL(def.Meta.URL)
   160  		if err != nil {
   161  			return fmt.Errorf("invalid 'url' field: %v", err)
   162  		}
   163  		domain = d
   164  	}
   165  	if ctx.IsSet(dnsDomainFlag.Name) {
   166  		domain = ctx.String(dnsDomainFlag.Name)
   167  	}
   168  	if ctx.IsSet(dnsSeqFlag.Name) {
   169  		def.Meta.Seq = ctx.Uint(dnsSeqFlag.Name)
   170  	} else {
   171  		def.Meta.Seq++ // Auto-bump sequence number if not supplied via flag.
   172  	}
   173  	t, err := dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	key := loadSigningKey(keyfile)
   179  	url, err := t.Sign(key, domain)
   180  	if err != nil {
   181  		return fmt.Errorf("can't sign: %v", err)
   182  	}
   183  
   184  	def = treeToDefinition(url, t)
   185  	def.Meta.LastModified = time.Now()
   186  	writeTreeMetadata(defdir, def)
   187  	return nil
   188  }
   189  
   190  // directoryName returns the directory name of the given path.
   191  // For example, when dir is "foo/bar", it returns "bar".
   192  // When dir is ".", and the working directory is "example/foo", it returns "foo".
   193  func directoryName(dir string) string {
   194  	abs, err := filepath.Abs(dir)
   195  	if err != nil {
   196  		exit(err)
   197  	}
   198  	return filepath.Base(abs)
   199  }
   200  
   201  // dnsToTXT performs dnsTXTCommand.
   202  func dnsToTXT(ctx *cli.Context) error {
   203  	if ctx.NArg() < 1 {
   204  		return fmt.Errorf("need tree definition directory as argument")
   205  	}
   206  	output := ctx.Args().Get(1)
   207  	if output == "" {
   208  		output = "-" // default to stdout
   209  	}
   210  	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
   211  	if err != nil {
   212  		return err
   213  	}
   214  	writeTXTJSON(output, t.ToTXT(domain))
   215  	return nil
   216  }
   217  
   218  // dnsToCloudflare performs dnsCloudflareCommand.
   219  func dnsToCloudflare(ctx *cli.Context) error {
   220  	if ctx.NArg() != 1 {
   221  		return fmt.Errorf("need tree definition directory as argument")
   222  	}
   223  	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
   224  	if err != nil {
   225  		return err
   226  	}
   227  	client := newCloudflareClient(ctx)
   228  	return client.deploy(domain, t)
   229  }
   230  
   231  // dnsToRoute53 performs dnsRoute53Command.
   232  func dnsToRoute53(ctx *cli.Context) error {
   233  	if ctx.NArg() != 1 {
   234  		return fmt.Errorf("need tree definition directory as argument")
   235  	}
   236  	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
   237  	if err != nil {
   238  		return err
   239  	}
   240  	client := newRoute53Client(ctx)
   241  	return client.deploy(domain, t)
   242  }
   243  
   244  // dnsNukeRoute53 performs dnsRoute53NukeCommand.
   245  func dnsNukeRoute53(ctx *cli.Context) error {
   246  	if ctx.NArg() != 1 {
   247  		return fmt.Errorf("need domain name as argument")
   248  	}
   249  	client := newRoute53Client(ctx)
   250  	return client.deleteDomain(ctx.Args().First())
   251  }
   252  
   253  // loadSigningKey loads a private key in Ethereum keystore format.
   254  func loadSigningKey(keyfile string) *ecdsa.PrivateKey {
   255  	keyjson, err := os.ReadFile(keyfile)
   256  	if err != nil {
   257  		exit(fmt.Errorf("failed to read the keyfile at '%s': %v", keyfile, err))
   258  	}
   259  	password, _ := prompt.Stdin.PromptPassword("Please enter the password for '" + keyfile + "': ")
   260  	key, err := keystore.DecryptKey(keyjson, password)
   261  	if err != nil {
   262  		exit(fmt.Errorf("error decrypting key: %v", err))
   263  	}
   264  	return key.PrivateKey
   265  }
   266  
   267  // dnsClient configures the DNS discovery client from command line flags.
   268  func dnsClient(ctx *cli.Context) *dnsdisc.Client {
   269  	var cfg dnsdisc.Config
   270  	if commandHasFlag(ctx, dnsTimeoutFlag) {
   271  		cfg.Timeout = ctx.Duration(dnsTimeoutFlag.Name)
   272  	}
   273  	return dnsdisc.NewClient(cfg)
   274  }
   275  
   276  // There are two file formats for DNS node trees on disk:
   277  //
   278  // The 'TXT' format is a single JSON file containing DNS TXT records
   279  // as a JSON object where the keys are names and the values are objects
   280  // containing the value of the record.
   281  //
   282  // The 'definition' format is a directory containing two files:
   283  //
   284  //      enrtree-info.json    -- contains sequence number & links to other trees
   285  //      nodes.json           -- contains the nodes as a JSON array.
   286  //
   287  // This format exists because it's convenient to edit. nodes.json can be generated
   288  // in multiple ways: it may be written by a DHT crawler or compiled by a human.
   289  
   290  type dnsDefinition struct {
   291  	Meta  dnsMetaJSON
   292  	Nodes []*enode.Node
   293  }
   294  
   295  type dnsMetaJSON struct {
   296  	URL          string    `json:"url,omitempty"`
   297  	Seq          uint      `json:"seq"`
   298  	Sig          string    `json:"signature,omitempty"`
   299  	Links        []string  `json:"links"`
   300  	LastModified time.Time `json:"lastModified"`
   301  }
   302  
   303  func treeToDefinition(url string, t *dnsdisc.Tree) *dnsDefinition {
   304  	meta := dnsMetaJSON{
   305  		URL:   url,
   306  		Seq:   t.Seq(),
   307  		Sig:   t.Signature(),
   308  		Links: t.Links(),
   309  	}
   310  	if meta.Links == nil {
   311  		meta.Links = []string{}
   312  	}
   313  	return &dnsDefinition{Meta: meta, Nodes: t.Nodes()}
   314  }
   315  
   316  // loadTreeDefinition loads a directory in 'definition' format.
   317  func loadTreeDefinition(directory string) *dnsDefinition {
   318  	metaFile, nodesFile := treeDefinitionFiles(directory)
   319  	var def dnsDefinition
   320  	err := common.LoadJSON(metaFile, &def.Meta)
   321  	if err != nil && !os.IsNotExist(err) {
   322  		exit(err)
   323  	}
   324  	if def.Meta.Links == nil {
   325  		def.Meta.Links = []string{}
   326  	}
   327  	// Check link syntax.
   328  	for _, link := range def.Meta.Links {
   329  		if _, _, err := dnsdisc.ParseURL(link); err != nil {
   330  			exit(fmt.Errorf("invalid link %q: %v", link, err))
   331  		}
   332  	}
   333  	// Check/convert nodes.
   334  	nodes := loadNodesJSON(nodesFile)
   335  	if err := nodes.verify(); err != nil {
   336  		exit(err)
   337  	}
   338  	def.Nodes = nodes.nodes()
   339  	return &def
   340  }
   341  
   342  // loadTreeDefinitionForExport loads a DNS tree and ensures it is signed.
   343  func loadTreeDefinitionForExport(dir string) (domain string, t *dnsdisc.Tree, err error) {
   344  	metaFile, _ := treeDefinitionFiles(dir)
   345  	def := loadTreeDefinition(dir)
   346  	if def.Meta.URL == "" {
   347  		return "", nil, fmt.Errorf("missing 'url' field in %v", metaFile)
   348  	}
   349  	domain, pubkey, err := dnsdisc.ParseURL(def.Meta.URL)
   350  	if err != nil {
   351  		return "", nil, fmt.Errorf("invalid 'url' field in %v: %v", metaFile, err)
   352  	}
   353  	if t, err = dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links); err != nil {
   354  		return "", nil, err
   355  	}
   356  	if err := ensureValidTreeSignature(t, pubkey, def.Meta.Sig); err != nil {
   357  		return "", nil, err
   358  	}
   359  	return domain, t, nil
   360  }
   361  
   362  // ensureValidTreeSignature checks that sig is valid for tree and assigns it as the
   363  // tree's signature if valid.
   364  func ensureValidTreeSignature(t *dnsdisc.Tree, pubkey *ecdsa.PublicKey, sig string) error {
   365  	if sig == "" {
   366  		return fmt.Errorf("missing signature, run 'devp2p dns sign' first")
   367  	}
   368  	if err := t.SetSignature(pubkey, sig); err != nil {
   369  		return fmt.Errorf("invalid signature on tree, run 'devp2p dns sign' to update it")
   370  	}
   371  	return nil
   372  }
   373  
   374  // writeTreeMetadata writes a DNS node tree metadata file to the given directory.
   375  func writeTreeMetadata(directory string, def *dnsDefinition) {
   376  	metaJSON, err := json.MarshalIndent(&def.Meta, "", jsonIndent)
   377  	if err != nil {
   378  		exit(err)
   379  	}
   380  	if err := os.Mkdir(directory, 0744); err != nil && !os.IsExist(err) {
   381  		exit(err)
   382  	}
   383  	metaFile, _ := treeDefinitionFiles(directory)
   384  	if err := os.WriteFile(metaFile, metaJSON, 0644); err != nil {
   385  		exit(err)
   386  	}
   387  }
   388  
   389  func writeTreeNodes(directory string, def *dnsDefinition) {
   390  	ns := make(nodeSet, len(def.Nodes))
   391  	ns.add(def.Nodes...)
   392  	_, nodesFile := treeDefinitionFiles(directory)
   393  	writeNodesJSON(nodesFile, ns)
   394  }
   395  
   396  func treeDefinitionFiles(directory string) (string, string) {
   397  	meta := filepath.Join(directory, "enrtree-info.json")
   398  	nodes := filepath.Join(directory, "nodes.json")
   399  	return meta, nodes
   400  }
   401  
   402  // writeTXTJSON writes TXT records in JSON format.
   403  func writeTXTJSON(file string, txt map[string]string) {
   404  	txtJSON, err := json.MarshalIndent(txt, "", jsonIndent)
   405  	if err != nil {
   406  		exit(err)
   407  	}
   408  	if file == "-" {
   409  		os.Stdout.Write(txtJSON)
   410  		fmt.Println()
   411  		return
   412  	}
   413  	if err := os.WriteFile(file, txtJSON, 0644); err != nil {
   414  		exit(err)
   415  	}
   416  }