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 }