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 }