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