lego/acme/dns_challenge_cloudflare.go
Matthew Holt 971541dc0a Use http client with timeout of 10s
This will prevent indefinitely-hanging requests in case some service or middle box is malfunctioning.

Fix vet errors and lint warnings

Add vet to CI check

Only get issuer certificate if it would be used

No need to make a GET request if the OCSP server is not specified in leaf certificate

Fix CI tests

Make tests verbose
2016-02-14 14:33:54 -07:00

145 lines
3.5 KiB
Go

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{Email: cloudflareEmail, Key: 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,
}
}
// 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
}