github.com/core-coin/go-core/v2@v2.1.9/cmd/devp2p/dns_cloudflare.go (about)

     1  // Copyright 2019 by the Authors
     2  // This file is part of go-core.
     3  //
     4  // go-core 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-core 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-core. 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  	"gopkg.in/urfave/cli.v1"
    25  
    26  	"github.com/core-coin/go-core/v2/log"
    27  	"github.com/core-coin/go-core/v2/p2p/dnsdisc"
    28  )
    29  
    30  var (
    31  	cloudflareTokenFlag = cli.StringFlag{
    32  		Name:   "token",
    33  		Usage:  "CloudFlare API token",
    34  		EnvVar: "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(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(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 = treeNodeTTL // Max TTL permitted by Cloudflare
   137  			}
   138  			_, err = c.CreateDNSRecord(c.zoneID, cloudflare.DNSRecord{Type: "TXT", Name: path, Content: val, TTL: ttl})
   139  		} else if old.Content != val {
   140  			// Entry already exists, only change its content.
   141  			log.Info(fmt.Sprintf("Updating %s from %q to %q", path, old.Content, val))
   142  			old.Content = val
   143  			err = c.UpdateDNSRecord(c.zoneID, old.ID, old)
   144  		} else {
   145  			log.Info(fmt.Sprintf("Skipping %s = %q", path, val))
   146  		}
   147  		if err != nil {
   148  			return fmt.Errorf("failed to publish %s: %v", path, err)
   149  		}
   150  	}
   151  
   152  	// Iterate over the old records and delete anything stale.
   153  	for path, entry := range existing {
   154  		if _, ok := records[path]; ok {
   155  			continue
   156  		}
   157  		// Stale entry, nuke it.
   158  		log.Info(fmt.Sprintf("Deleting %s = %q", path, entry.Content))
   159  		if err := c.DeleteDNSRecord(c.zoneID, entry.ID); err != nil {
   160  			return fmt.Errorf("failed to delete %s: %v", path, err)
   161  		}
   162  	}
   163  	return nil
   164  }