github.com/core-coin/go-core/v2@v2.1.9/cmd/devp2p/dnscmd.go (about)

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