2015-12-03 20:01:46 +00:00
|
|
|
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 = envAuth()
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-01-15 04:06:25 +00:00
|
|
|
// 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)
|
2015-12-03 20:01:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-01-15 04:06:25 +00:00
|
|
|
// CleanUp removes the TXT record matching the specified parameters
|
|
|
|
func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error {
|
|
|
|
fqdn, _, _ := DNS01Record(domain, keyAuth)
|
2015-12-03 20:01:46 +00:00
|
|
|
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 {
|
2016-01-15 04:06:25 +00:00
|
|
|
return err
|
2015-12-03 20:01:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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 {
|
2016-01-15 04:06:25 +00:00
|
|
|
switch {
|
|
|
|
case ttl < 120:
|
|
|
|
return 120
|
|
|
|
case ttl > 86400:
|
|
|
|
return 86400
|
|
|
|
default:
|
|
|
|
return ttl
|
2015-12-03 20:01:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func envAuth() (email, apiKey string) {
|
|
|
|
email = os.Getenv("CLOUDFLARE_EMAIL")
|
|
|
|
apiKey = os.Getenv("CLOUDFLARE_API_KEY")
|
|
|
|
if len(email) == 0 || len(apiKey) == 0 {
|
|
|
|
return "", ""
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|