dns_cloudflare.go 5.0 KB

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