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