github.com/sbrajchuk/go-ethereum@v1.9.7/cmd/devp2p/dnscmd.go (about)

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