dns_cloudflare.go 4.8 KB

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