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