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  }