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