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