github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/cmd/devp2p/dnscmd.go (about)

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