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