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