github.com/cerc-io/go-ethereum@v1.9.7/cmd/devp2p/dns_cloudflare.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 "fmt" 21 "strings" 22 23 "github.com/cloudflare/cloudflare-go" 24 "github.com/ethereum/go-ethereum/log" 25 "github.com/ethereum/go-ethereum/p2p/dnsdisc" 26 "gopkg.in/urfave/cli.v1" 27 ) 28 29 var ( 30 cloudflareTokenFlag = cli.StringFlag{ 31 Name: "token", 32 Usage: "CloudFlare API token", 33 EnvVar: "CLOUDFLARE_API_TOKEN", 34 } 35 cloudflareZoneIDFlag = cli.StringFlag{ 36 Name: "zoneid", 37 Usage: "CloudFlare Zone ID (optional)", 38 } 39 ) 40 41 type cloudflareClient struct { 42 *cloudflare.API 43 zoneID string 44 } 45 46 // newCloudflareClient sets up a CloudFlare API client from command line flags. 47 func newCloudflareClient(ctx *cli.Context) *cloudflareClient { 48 token := ctx.String(cloudflareTokenFlag.Name) 49 if token == "" { 50 exit(fmt.Errorf("need cloudflare API token to proceed")) 51 } 52 api, err := cloudflare.NewWithAPIToken(token) 53 if err != nil { 54 exit(fmt.Errorf("can't create Cloudflare client: %v", err)) 55 } 56 return &cloudflareClient{ 57 API: api, 58 zoneID: ctx.String(cloudflareZoneIDFlag.Name), 59 } 60 } 61 62 // deploy uploads the given tree to CloudFlare DNS. 63 func (c *cloudflareClient) deploy(name string, t *dnsdisc.Tree) error { 64 if err := c.checkZone(name); err != nil { 65 return err 66 } 67 records := t.ToTXT(name) 68 return c.uploadRecords(name, records) 69 } 70 71 // checkZone verifies permissions on the CloudFlare DNS Zone for name. 72 func (c *cloudflareClient) checkZone(name string) error { 73 if c.zoneID == "" { 74 log.Info(fmt.Sprintf("Finding CloudFlare zone ID for %s", name)) 75 id, err := c.ZoneIDByName(name) 76 if err != nil { 77 return err 78 } 79 c.zoneID = id 80 } 81 log.Info(fmt.Sprintf("Checking Permissions on zone %s", c.zoneID)) 82 zone, err := c.ZoneDetails(c.zoneID) 83 if err != nil { 84 return err 85 } 86 if !strings.HasSuffix(name, "."+zone.Name) { 87 return fmt.Errorf("CloudFlare zone name %q does not match name %q to be deployed", zone.Name, name) 88 } 89 needPerms := map[string]bool{"#zone:edit": false, "#zone:read": false} 90 for _, perm := range zone.Permissions { 91 if _, ok := needPerms[perm]; ok { 92 needPerms[perm] = true 93 } 94 } 95 for _, ok := range needPerms { 96 if !ok { 97 return fmt.Errorf("wrong permissions on zone %s: %v", c.zoneID, needPerms) 98 } 99 } 100 return nil 101 } 102 103 // uploadRecords updates the TXT records at a particular subdomain. All non-root records 104 // will have a TTL of "infinity" and all existing records not in the new map will be 105 // nuked! 106 func (c *cloudflareClient) uploadRecords(name string, records map[string]string) error { 107 // Convert all names to lowercase. 108 lrecords := make(map[string]string, len(records)) 109 for name, r := range records { 110 lrecords[strings.ToLower(name)] = r 111 } 112 records = lrecords 113 114 log.Info(fmt.Sprintf("Retrieving existing TXT records on %s", name)) 115 entries, err := c.DNSRecords(c.zoneID, cloudflare.DNSRecord{Type: "TXT"}) 116 if err != nil { 117 return err 118 } 119 existing := make(map[string]cloudflare.DNSRecord) 120 for _, entry := range entries { 121 if !strings.HasSuffix(entry.Name, name) { 122 continue 123 } 124 existing[strings.ToLower(entry.Name)] = entry 125 } 126 127 // Iterate over the new records and inject anything missing. 128 for path, val := range records { 129 old, exists := existing[path] 130 if !exists { 131 // Entry is unknown, push a new one to Cloudflare. 132 log.Info(fmt.Sprintf("Creating %s = %q", path, val)) 133 ttl := 1 134 if path != name { 135 ttl = 2147483647 // Max TTL permitted by Cloudflare 136 } 137 _, err = c.CreateDNSRecord(c.zoneID, cloudflare.DNSRecord{Type: "TXT", Name: path, Content: val, TTL: ttl}) 138 } else if old.Content != val { 139 // Entry already exists, only change its content. 140 log.Info(fmt.Sprintf("Updating %s from %q to %q", path, old.Content, val)) 141 old.Content = val 142 err = c.UpdateDNSRecord(c.zoneID, old.ID, old) 143 } else { 144 log.Info(fmt.Sprintf("Skipping %s = %q", path, val)) 145 } 146 if err != nil { 147 return fmt.Errorf("failed to publish %s: %v", path, err) 148 } 149 } 150 151 // Iterate over the old records and delete anything stale. 152 for path, entry := range existing { 153 if _, ok := records[path]; ok { 154 continue 155 } 156 // Stale entry, nuke it. 157 log.Info(fmt.Sprintf("Deleting %s = %q", path, entry.Content)) 158 if err := c.DeleteDNSRecord(c.zoneID, entry.ID); err != nil { 159 return fmt.Errorf("failed to delete %s: %v", path, err) 160 } 161 } 162 return nil 163 }