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