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