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