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