lego/acme/dns_challenge_cloudflare.go

162 lines
3.7 KiB
Go
Raw Normal View History

package acme
import (
"fmt"
"os"
"strings"
"github.com/crackcomm/cloudflare"
"golang.org/x/net/context"
)
// DNSProviderCloudFlare is an implementation of the DNSProvider interface
type DNSProviderCloudFlare struct {
client *cloudflare.Client
ctx context.Context
}
// NewDNSProviderCloudFlare returns a DNSProviderCloudFlare instance with a configured cloudflare client.
// Authentication is either done using the passed credentials or - when empty - using the environment
// variables CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY.
func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProviderCloudFlare, error) {
if cloudflareEmail == "" || cloudflareKey == "" {
cloudflareEmail, cloudflareKey = cloudflareEnvAuth()
if cloudflareEmail == "" || cloudflareKey == "" {
return nil, fmt.Errorf("CloudFlare credentials missing")
}
}
c := &DNSProviderCloudFlare{
client: cloudflare.New(&cloudflare.Options{cloudflareEmail, cloudflareKey}),
ctx: context.Background(),
}
return c, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge
func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
}
record := newTxtRecord(zoneID, fqdn, value, ttl)
err = c.client.Records.Create(c.ctx, record)
if err != nil {
return fmt.Errorf("CloudFlare API call failed: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := DNS01Record(domain, keyAuth)
records, err := c.findTxtRecords(fqdn)
if err != nil {
return err
}
for _, rec := range records {
err := c.client.Records.Delete(c.ctx, rec.ZoneID, rec.ID)
if err != nil {
return err
}
}
return nil
}
func (c *DNSProviderCloudFlare) findTxtRecords(fqdn string) ([]*cloudflare.Record, error) {
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return nil, err
}
var records []*cloudflare.Record
result, err := c.client.Records.List(c.ctx, zoneID)
if err != nil {
return records, fmt.Errorf("CloudFlare API call has failed: %v", err)
}
name := unFqdn(fqdn)
for _, rec := range result {
if rec.Name == name && rec.Type == "TXT" {
records = append(records, rec)
}
}
return records, nil
}
func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) {
zones, err := c.client.Zones.List(c.ctx)
if err != nil {
return "", fmt.Errorf("CloudFlare API call failed: %v", err)
}
var hostedZone cloudflare.Zone
for _, zone := range zones {
name := toFqdn(zone.Name)
if strings.HasSuffix(fqdn, name) {
if len(zone.Name) > len(hostedZone.Name) {
hostedZone = *zone
}
}
}
if hostedZone.ID == "" {
return "", fmt.Errorf("No matching CloudFlare zone found for domain %s", fqdn)
}
return hostedZone.ID, nil
}
func newTxtRecord(zoneID, fqdn, value string, ttl int) *cloudflare.Record {
name := unFqdn(fqdn)
return &cloudflare.Record{
Type: "TXT",
Name: name,
Content: value,
TTL: sanitizeTTL(ttl),
ZoneID: zoneID,
}
}
func toFqdn(name string) string {
n := len(name)
if n == 0 || name[n-1] == '.' {
return name
}
return name + "."
}
func unFqdn(name string) string {
n := len(name)
if n != 0 && name[n-1] == '.' {
return name[:n-1]
}
return name
}
// TTL must be between 120 and 86400 seconds
func sanitizeTTL(ttl int) int {
switch {
case ttl < 120:
return 120
case ttl > 86400:
return 86400
default:
return ttl
}
}
func cloudflareEnvAuth() (email, apiKey string) {
email = os.Getenv("CLOUDFLARE_EMAIL")
apiKey = os.Getenv("CLOUDFLARE_API_KEY")
if len(email) == 0 || len(apiKey) == 0 {
return "", ""
}
return
}