Allow to configure TTL, interval and timeout (#634)

* feat: add GetOrDefaultXXX methods.
* refactor: configuration (alidns).
* refactor: configuration (azure).
* refactor: configuration (auroradns).
* refactor: configuration (bluecat).
* refactor: configuration (cloudflare).
* refactor: configuration (digitalocean).
* refactor: configuration (dnsimple).
* refactor: configuration (dnmadeeasy).
* refactor: configuration (dnspod).
* refactor: configuration (duckdns).
* refactor: configuration (dyn).
* refactor: configuration (exoscale).
* refactor: configuration (fastdns).
* refactor: configuration (gandi).
* refactor: configuration (gandiv5).
* refactor: configuration (gcloud).
* refactor: configuration (glesys).
* refactor: configuration (godaddy).
* refactor: configuration (iij).
* refactor: configuration (lightsail).
* refactor: configuration (linode).
* refactor: configuration (namecheap).
* refactor: configuration (namedotcom).
* refactor: configuration (netcup).
* refactor: configuration (nifcloud).
* refactor: configuration (ns1).
* refactor: configuration (otc).
* refactor: configuration (ovh).
* refactor: configuration (pdns).
* refactor: configuration (rackspace).
* refactor: configuration (rfc2136).
* refactor: configuration (route53).
* refactor: configuration (sakuracloud).
* refactor: configuration (vegadns).
* refactor: configuration (vultr).
This commit is contained in:
Ludovic Fernandez 2018-09-15 19:07:24 +02:00 committed by GitHub
parent ad34a85dad
commit bba134ce87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 4800 additions and 2521 deletions

2
Gopkg.lock generated
View file

@ -522,6 +522,8 @@
"github.com/OpenDNS/vegadns2client",
"github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v1",
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid",
"github.com/aliyun/alibaba-cloud-sdk-go/sdk",
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials",
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests",
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns",
"github.com/aws/aws-sdk-go/aws",

View file

@ -24,6 +24,14 @@ var (
const defaultResolvConf = "/etc/resolv.conf"
const (
// DefaultPropagationTimeout default propagation timeout
DefaultPropagationTimeout = 60 * time.Second
// DefaultPollingInterval default polling interval
DefaultPollingInterval = 2 * time.Second
)
var defaultNameservers = []string{
"google-public-dns-a.google.com:53",
"google-public-dns-b.google.com:53",
@ -112,7 +120,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
case ChallengeProviderTimeout:
timeout, interval = provider.Timeout()
default:
timeout, interval = 60*time.Second, 2*time.Second
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
}
err = WaitFor(timeout, interval, func() (bool, error) {

View file

@ -5,6 +5,7 @@ import (
"os"
"strconv"
"strings"
"time"
)
// Get environment variables
@ -37,3 +38,36 @@ func GetOrDefaultInt(envVar string, defaultValue int) int {
return v
}
// GetOrDefaultSecond returns the given environment variable value as an time.Duration (second).
// Returns the default if the envvar cannot be coopered to an int, or is not found.
func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration {
v := GetOrDefaultInt(envVar, -1)
if v < 0 {
return defaultValue
}
return time.Duration(v) * time.Second
}
// GetOrDefaultString returns the given environment variable value as a string.
// Returns the default if the envvar cannot be find.
func GetOrDefaultString(envVar string, defaultValue string) string {
v := os.Getenv(envVar)
if len(v) == 0 {
return defaultValue
}
return v
}
// GetOrDefaultBool returns the given environment variable value as a boolean.
// Returns the default if the envvar cannot be coopered to a boolean, or is not found.
func GetOrDefaultBool(envVar string, defaultValue bool) bool {
v, err := strconv.ParseBool(os.Getenv(envVar))
if err != nil {
return defaultValue
}
return v
}

View file

@ -3,12 +3,13 @@ package env
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_GetOrDefaultInt(t *testing.T) {
func TestGetOrDefaultInt(t *testing.T) {
testCases := []struct {
desc string
envValue string
@ -54,3 +55,124 @@ func Test_GetOrDefaultInt(t *testing.T) {
})
}
}
func TestGetOrDefaultSecond(t *testing.T) {
testCases := []struct {
desc string
envValue string
defaultValue time.Duration
expected time.Duration
}{
{
desc: "valid value",
envValue: "100",
defaultValue: 2 * time.Second,
expected: 100 * time.Second,
},
{
desc: "invalid content, use default value",
envValue: "abc123",
defaultValue: 2 * time.Second,
expected: 2 * time.Second,
},
{
desc: "invalid content, negative value",
envValue: "-111",
defaultValue: 2 * time.Second,
expected: 2 * time.Second,
},
{
desc: "float: invalid type, use default value",
envValue: "1.11",
defaultValue: 2 * time.Second,
expected: 2 * time.Second,
},
}
var key = "LEGO_ENV_TC"
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer os.Unsetenv(key)
err := os.Setenv(key, test.envValue)
require.NoError(t, err)
result := GetOrDefaultSecond(key, test.defaultValue)
assert.Equal(t, test.expected, result)
})
}
}
func TestGetOrDefaultString(t *testing.T) {
testCases := []struct {
desc string
envValue string
defaultValue string
expected string
}{
{
desc: "missing env var",
defaultValue: "foo",
expected: "foo",
},
{
desc: "with env var",
envValue: "bar",
defaultValue: "foo",
expected: "bar",
},
}
var key = "LEGO_ENV_TC"
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer os.Unsetenv(key)
err := os.Setenv(key, test.envValue)
require.NoError(t, err)
actual := GetOrDefaultString(key, test.defaultValue)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetOrDefaultBool(t *testing.T) {
testCases := []struct {
desc string
envValue string
defaultValue bool
expected bool
}{
{
desc: "missing env var",
defaultValue: true,
expected: true,
},
{
desc: "with env var",
envValue: "true",
defaultValue: false,
expected: true,
},
{
desc: "invalid value",
envValue: "foo",
defaultValue: false,
expected: false,
},
}
var key = "LEGO_ENV_TC"
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer os.Unsetenv(key)
err := os.Setenv(key, test.envValue)
require.NoError(t, err)
actual := GetOrDefaultBool(key, test.defaultValue)
assert.Equal(t, test.expected, actual)
})
}
}

View file

@ -3,10 +3,14 @@
package alidns
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/xenolf/lego/acme"
@ -15,8 +19,30 @@ import (
const defaultRegionID = "cn-hangzhou"
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
SecretKey string
RegionID string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("ALICLOUD_TTL", 600),
PropagationTimeout: env.GetOrDefaultSecond("ALICLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("ALICLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond("ALICLOUD_HTTP_TIMEOUT", 10*time.Second),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
client *alidns.Client
}
@ -25,48 +51,74 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("ALICLOUD_ACCESS_KEY", "ALICLOUD_SECRET_KEY")
if err != nil {
return nil, fmt.Errorf("AliDNS: %v", err)
return nil, fmt.Errorf("alicloud: %v", err)
}
regionID := os.Getenv("ALICLOUD_REGION_ID")
config := NewDefaultConfig()
config.APIKey = values["ALICLOUD_ACCESS_KEY"]
config.SecretKey = values["ALICLOUD_SECRET_KEY"]
config.RegionID = os.Getenv("ALICLOUD_REGION_ID")
return NewDNSProviderCredentials(values["ALICLOUD_ACCESS_KEY"], values["ALICLOUD_SECRET_KEY"], regionID)
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a DNSProvider instance configured for alidns.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for alidns.
// Deprecated
func NewDNSProviderCredentials(apiKey, secretKey, regionID string) (*DNSProvider, error) {
if apiKey == "" || secretKey == "" {
return nil, fmt.Errorf("AliDNS: credentials missing")
config := NewDefaultConfig()
config.APIKey = apiKey
config.SecretKey = secretKey
config.RegionID = regionID
return NewDNSProviderConfig(config)
}
if len(regionID) == 0 {
regionID = defaultRegionID
// NewDNSProviderConfig return a DNSProvider instance configured for alidns.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("alicloud: the configuration of the DNS provider is nil")
}
client, err := alidns.NewClientWithAccessKey(regionID, apiKey, secretKey)
if config.APIKey == "" || config.SecretKey == "" {
return nil, fmt.Errorf("alicloud: credentials missing")
}
if len(config.RegionID) == 0 {
config.RegionID = defaultRegionID
}
conf := sdk.NewConfig().WithTimeout(config.HTTPTimeout)
credential := credentials.NewAccessKeyCredential(config.APIKey, config.SecretKey)
client, err := alidns.NewClientWithOptions(config.RegionID, conf, credential)
if err != nil {
return nil, fmt.Errorf("AliDNS: credentials failed: %v", err)
return nil, fmt.Errorf("alicloud: credentials failed: %v", err)
}
return &DNSProvider{
client: client,
}, nil
return &DNSProvider{config: config, client: client}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
_, zoneName, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("alicloud: %v", err)
}
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, ttl)
recordAttributes := d.newTxtRecord(zoneName, fqdn, value)
_, err = d.client.AddDomainRecord(recordAttributes)
if err != nil {
return fmt.Errorf("AliDNS: API call failed: %v", err)
return fmt.Errorf("alicloud: API call failed: %v", err)
}
return nil
}
@ -77,12 +129,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
records, err := d.findTxtRecords(domain, fqdn)
if err != nil {
return err
return fmt.Errorf("alicloud: %v", err)
}
_, _, err = d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("alicloud: %v", err)
}
for _, rec := range records {
@ -90,7 +142,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
request.RecordId = rec.RecordId
_, err = d.client.DeleteDomainRecord(request)
if err != nil {
return err
return fmt.Errorf("alicloud: %v", err)
}
}
return nil
@ -100,7 +152,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, string, error) {
request := alidns.CreateDescribeDomainsRequest()
zones, err := d.client.DescribeDomains(request)
if err != nil {
return "", "", fmt.Errorf("AliDNS: API call failed: %v", err)
return "", "", fmt.Errorf("API call failed: %v", err)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
@ -116,18 +168,18 @@ func (d *DNSProvider) getHostedZone(domain string) (string, string, error) {
}
if hostedZone.DomainId == "" {
return "", "", fmt.Errorf("AliDNS: zone %s not found in AliDNS for domain %s", authZone, domain)
return "", "", fmt.Errorf("zone %s not found in AliDNS for domain %s", authZone, domain)
}
return fmt.Sprintf("%v", hostedZone.DomainId), hostedZone.DomainName, nil
}
func (d *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *alidns.AddDomainRecordRequest {
func (d *DNSProvider) newTxtRecord(zone, fqdn, value string) *alidns.AddDomainRecordRequest {
request := alidns.CreateAddDomainRecordRequest()
request.Type = "TXT"
request.DomainName = zone
request.RR = d.extractRecordName(fqdn, zone)
request.Value = value
request.TTL = requests.NewInteger(600)
request.TTL = requests.NewInteger(d.config.TTL)
return request
}
@ -145,7 +197,7 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]alidns.Record, erro
result, err := d.client.DescribeDomainRecords(request)
if err != nil {
return records, fmt.Errorf("AliDNS: API call has failed: %v", err)
return records, fmt.Errorf("API call has failed: %v", err)
}
recordName := d.extractRecordName(fqdn, zoneName)

View file

@ -35,7 +35,11 @@ func TestNewDNSProviderValid(t *testing.T) {
os.Setenv("ALICLOUD_ACCESS_KEY", "")
os.Setenv("ALICLOUD_SECRET_KEY", "")
_, err := NewDNSProviderCredentials("123", "123", "")
config := NewDefaultConfig()
config.APIKey = "123"
config.SecretKey = "123"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
@ -54,7 +58,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("ALICLOUD_SECRET_KEY", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "AliDNS: some credentials information are missing: ALICLOUD_ACCESS_KEY,ALICLOUD_SECRET_KEY")
assert.EqualError(t, err, "alicloud: some credentials information are missing: ALICLOUD_ACCESS_KEY,ALICLOUD_SECRET_KEY")
}
func TestCloudXNSPresent(t *testing.T) {
@ -62,7 +66,11 @@ func TestCloudXNSPresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(alidnsAPIKey, alidnsSecretKey, "")
config := NewDefaultConfig()
config.APIKey = alidnsAPIKey
config.SecretKey = alidnsSecretKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(alidnsDomain, "", "123d==")
@ -75,7 +83,12 @@ func TestLivednspodCleanUp(t *testing.T) {
}
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderCredentials(alidnsAPIKey, alidnsSecretKey, "")
config := NewDefaultConfig()
config.APIKey = alidnsAPIKey
config.SecretKey = alidnsSecretKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(alidnsDomain, "", "123d==")
assert.NoError(t, err)

View file

@ -1,9 +1,11 @@
package auroradns
import (
"errors"
"fmt"
"os"
"sync"
"time"
"github.com/edeckers/auroradnsclient"
"github.com/edeckers/auroradnsclient/records"
@ -12,68 +14,97 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
const defaultBaseURL = "https://api.auroradns.eu"
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
UserID string
Key string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("AURORA_TTL", 300),
PropagationTimeout: env.GetOrDefaultSecond("AURORA_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("AURORA_POLLING_INTERVAL", acme.DefaultPollingInterval),
}
}
// DNSProvider describes a provider for AuroraDNS
type DNSProvider struct {
recordIDs map[string]string
recordIDsMu sync.Mutex
config *Config
client *auroradnsclient.AuroraDNSClient
}
// NewDNSProvider returns a DNSProvider instance configured for AuroraDNS.
// Credentials must be passed in the environment variables: AURORA_USER_ID
// and AURORA_KEY.
// Credentials must be passed in the environment variables:
// AURORA_USER_ID and AURORA_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("AURORA_USER_ID", "AURORA_KEY")
if err != nil {
return nil, fmt.Errorf("AuroraDNS: %v", err)
return nil, fmt.Errorf("aurora: %v", err)
}
endpoint := os.Getenv("AURORA_ENDPOINT")
config := NewDefaultConfig()
config.BaseURL = os.Getenv("AURORA_ENDPOINT")
config.UserID = values["AURORA_USER_ID"]
config.Key = values["AURORA_KEY"]
return NewDNSProviderCredentials(endpoint, values["AURORA_USER_ID"], values["AURORA_KEY"])
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for AuroraDNS.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for AuroraDNS.
// Deprecated
func NewDNSProviderCredentials(baseURL string, userID string, key string) (*DNSProvider, error) {
if baseURL == "" {
baseURL = "https://api.auroradns.eu"
config := NewDefaultConfig()
config.BaseURL = baseURL
config.UserID = userID
config.Key = key
return NewDNSProviderConfig(config)
}
client, err := auroradnsclient.NewAuroraDNSClient(baseURL, userID, key)
// NewDNSProviderConfig return a DNSProvider instance configured for AuroraDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("aurora: the configuration of the DNS provider is nil")
}
if config.UserID == "" || config.Key == "" {
return nil, errors.New("aurora: some credentials information are missing")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
client, err := auroradnsclient.NewAuroraDNSClient(config.BaseURL, config.UserID, config.Key)
if err != nil {
return nil, err
return nil, fmt.Errorf("aurora: %v", err)
}
return &DNSProvider{
config: config,
client: client,
recordIDs: make(map[string]string),
}, nil
}
func (d *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRecord, error) {
zs, err := d.client.GetZones()
if err != nil {
return zones.ZoneRecord{}, err
}
for _, element := range zs {
if element.Name == name {
return element, nil
}
}
return zones.ZoneRecord{}, fmt.Errorf("could not find Zone record")
}
// Present creates a record with a secret
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
return fmt.Errorf("aurora: could not determine zone for domain: '%s'. %s", domain, err)
}
// 1. Aurora will happily create the TXT record when it is provided a fqdn,
@ -89,7 +120,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
zoneRecord, err := d.getZoneInformationByName(authZone)
if err != nil {
return fmt.Errorf("could not create record: %v", err)
return fmt.Errorf("aurora: could not create record: %v", err)
}
reqData :=
@ -97,12 +128,12 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
RecordType: "TXT",
Name: subdomain,
Content: value,
TTL: 300,
TTL: d.config.TTL,
}
respData, err := d.client.CreateRecord(zoneRecord.ID, reqData)
if err != nil {
return fmt.Errorf("could not create record: %v", err)
return fmt.Errorf("aurora: could not create record: %v", err)
}
d.recordIDsMu.Lock()
@ -147,3 +178,24 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRecord, error) {
zs, err := d.client.GetZones()
if err != nil {
return zones.ZoneRecord{}, err
}
for _, element := range zs {
if element.Name == name {
return element, nil
}
}
return zones.ZoneRecord{}, fmt.Errorf("could not find Zone record")
}

View file

@ -48,11 +48,16 @@ func TestAuroraDNSPresent(t *testing.T) {
defer mock.Close()
auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserID, fakeAuroraDNSKey)
require.NoError(t, err)
require.NotNil(t, auroraProvider)
config := NewDefaultConfig()
config.UserID = fakeAuroraDNSUserID
config.Key = fakeAuroraDNSKey
config.BaseURL = mock.URL
err = auroraProvider.Present("example.com", "", "foobar")
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
require.NotNil(t, provider)
err = provider.Present("example.com", "", "foobar")
require.NoError(t, err, "fail to create TXT record")
assert.True(t, requestReceived, "Expected request to be received by mock backend, but it wasn't")
@ -93,14 +98,19 @@ func TestAuroraDNSCleanUp(t *testing.T) {
}))
defer mock.Close()
auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserID, fakeAuroraDNSKey)
require.NoError(t, err)
require.NotNil(t, auroraProvider)
config := NewDefaultConfig()
config.UserID = fakeAuroraDNSUserID
config.Key = fakeAuroraDNSKey
config.BaseURL = mock.URL
err = auroraProvider.Present("example.com", "", "foobar")
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
require.NotNil(t, provider)
err = provider.Present("example.com", "", "foobar")
require.NoError(t, err, "fail to create TXT record")
err = auroraProvider.CleanUp("example.com", "", "foobar")
err = provider.CleanUp("example.com", "", "foobar")
require.NoError(t, err, "fail to remove TXT record")
assert.True(t, requestReceived, "Expected request to be received by mock backend, but it wasn't")

View file

@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
@ -19,14 +20,31 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
ClientID string
ClientSecret string
SubscriptionID string
TenantID string
ResourceGroup string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("AZURE_TTL", 60),
PropagationTimeout: env.GetOrDefaultSecond("AZURE_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("AZURE_POLLING_INTERVAL", 2*time.Second),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
clientID string
clientSecret string
subscriptionID string
tenantID string
resourceGroup string
context context.Context
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for azure.
@ -35,54 +53,66 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_RESOURCE_GROUP")
if err != nil {
return nil, fmt.Errorf("Azure: %v", err)
return nil, fmt.Errorf("azure: %v", err)
}
return NewDNSProviderCredentials(
values["AZURE_CLIENT_ID"],
values["AZURE_CLIENT_SECRET"],
values["AZURE_SUBSCRIPTION_ID"],
values["AZURE_TENANT_ID"],
values["AZURE_RESOURCE_GROUP"],
)
config := NewDefaultConfig()
config.ClientID = values["AZURE_CLIENT_ID"]
config.ClientSecret = values["AZURE_CLIENT_SECRET"]
config.SubscriptionID = values["AZURE_SUBSCRIPTION_ID"]
config.TenantID = values["AZURE_TENANT_ID"]
config.ResourceGroup = values["AZURE_RESOURCE_GROUP"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for azure.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for azure.
// Deprecated
func NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroup string) (*DNSProvider, error) {
if clientID == "" || clientSecret == "" || subscriptionID == "" || tenantID == "" || resourceGroup == "" {
return nil, errors.New("Azure: some credentials information are missing")
config := NewDefaultConfig()
config.ClientID = clientID
config.ClientSecret = clientSecret
config.SubscriptionID = subscriptionID
config.TenantID = tenantID
config.ResourceGroup = resourceGroup
return NewDNSProviderConfig(config)
}
return &DNSProvider{
clientID: clientID,
clientSecret: clientSecret,
subscriptionID: subscriptionID,
tenantID: tenantID,
resourceGroup: resourceGroup,
// TODO: A timeout can be added here for cancellation purposes.
context: context.Background(),
}, nil
// NewDNSProviderConfig return a DNSProvider instance configured for Azure.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("azure: the configuration of the DNS provider is nil")
}
if config.ClientID == "" || config.ClientSecret == "" || config.SubscriptionID == "" || config.TenantID == "" || config.ResourceGroup == "" {
return nil, errors.New("azure: some credentials information are missing")
}
return &DNSProvider{config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 120 * time.Second, 2 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
ctx := context.Background()
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZoneID(fqdn)
zone, err := d.getHostedZoneID(ctx, fqdn)
if err != nil {
return err
return fmt.Errorf("azure: %v", err)
}
rsc := dns.NewRecordSetsClient(d.subscriptionID)
spt, err := d.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
rsc := dns.NewRecordSetsClient(d.config.SubscriptionID)
spt, err := d.newServicePrincipalToken(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return err
return fmt.Errorf("azure: %v", err)
}
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
@ -91,59 +121,55 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
rec := dns.RecordSet{
Name: &relative,
RecordSetProperties: &dns.RecordSetProperties{
TTL: to.Int64Ptr(60),
TTL: to.Int64Ptr(int64(d.config.TTL)),
TxtRecords: &[]dns.TxtRecord{{Value: &[]string{value}}},
},
}
_, err = rsc.CreateOrUpdate(d.context, d.resourceGroup, zone, relative, dns.TXT, rec, "", "")
return err
}
// Returns the relative record to the domain
func toRelativeRecord(domain, zone string) string {
return acme.UnFqdn(strings.TrimSuffix(domain, zone))
_, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, rec, "", "")
return fmt.Errorf("azure: %v", err)
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
ctx := context.Background()
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZoneID(fqdn)
zone, err := d.getHostedZoneID(ctx, fqdn)
if err != nil {
return err
return fmt.Errorf("azure: %v", err)
}
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
rsc := dns.NewRecordSetsClient(d.subscriptionID)
spt, err := d.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
rsc := dns.NewRecordSetsClient(d.config.SubscriptionID)
spt, err := d.newServicePrincipalToken(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return err
return fmt.Errorf("azure: %v", err)
}
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
_, err = rsc.Delete(d.context, d.resourceGroup, zone, relative, dns.TXT, "")
return err
_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, "")
return fmt.Errorf("azure: %v", err)
}
// Checks that azure has a zone for this domain name.
func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
// Now we want to to Azure and get the zone.
spt, err := d.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
spt, err := d.newServicePrincipalToken(azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return "", err
}
dc := dns.NewZonesClient(d.subscriptionID)
dc := dns.NewZonesClient(d.config.SubscriptionID)
dc.Authorizer = autorest.NewBearerAuthorizer(spt)
zone, err := dc.Get(d.context, d.resourceGroup, acme.UnFqdn(authZone))
zone, err := dc.Get(ctx, d.config.ResourceGroup, acme.UnFqdn(authZone))
if err != nil {
return "", err
}
@ -154,10 +180,15 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
// NewServicePrincipalTokenFromCredentials creates a new ServicePrincipalToken using values of the
// passed credentials map.
func (d *DNSProvider) newServicePrincipalTokenFromCredentials(scope string) (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, d.tenantID)
func (d *DNSProvider) newServicePrincipalToken(scope string) (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, d.config.TenantID)
if err != nil {
return nil, err
}
return adal.NewServicePrincipalToken(*oauthConfig, d.clientID, d.clientSecret, scope)
return adal.NewServicePrincipalToken(*oauthConfig, d.config.ClientID, d.config.ClientSecret, scope)
}
// Returns the relative record to the domain
func toRelativeRecord(domain, zone string) string {
return acme.UnFqdn(strings.TrimSuffix(domain, zone))
}

View file

@ -43,7 +43,14 @@ func TestNewDNSProviderValid(t *testing.T) {
defer restoreEnv()
os.Setenv("AZURE_CLIENT_ID", "")
_, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azureSubscriptionID, azureTenantID, azureResourceGroup)
config := NewDefaultConfig()
config.ClientID = azureClientID
config.ClientSecret = azureClientSecret
config.SubscriptionID = azureSubscriptionID
config.TenantID = azureTenantID
config.ResourceGroup = azureResourceGroup
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
@ -64,7 +71,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("AZURE_SUBSCRIPTION_ID", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "Azure: some credentials information are missing: AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,AZURE_RESOURCE_GROUP")
assert.EqualError(t, err, "azure: some credentials information are missing: AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,AZURE_RESOURCE_GROUP")
}
func TestLiveAzurePresent(t *testing.T) {
@ -72,7 +79,14 @@ func TestLiveAzurePresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azureSubscriptionID, azureTenantID, azureResourceGroup)
config := NewDefaultConfig()
config.ClientID = azureClientID
config.ClientSecret = azureClientSecret
config.SubscriptionID = azureSubscriptionID
config.TenantID = azureTenantID
config.ResourceGroup = azureResourceGroup
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(azureDomain, "", "123d==")
@ -84,7 +98,15 @@ func TestLiveAzureCleanUp(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azureSubscriptionID, azureTenantID, azureResourceGroup)
config := NewDefaultConfig()
config.ClientID = azureClientID
config.ClientSecret = azureClientSecret
config.SubscriptionID = azureSubscriptionID
config.TenantID = azureTenantID
config.ResourceGroup = azureResourceGroup
provider, err := NewDNSProviderConfig(config)
time.Sleep(time.Second * 1)
assert.NoError(t, err)

View file

@ -5,6 +5,7 @@ package bluecat
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@ -17,91 +18,221 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
const bluecatURLTemplate = "%s/Services/REST/v1"
const configType = "Configuration"
const viewType = "View"
const txtType = "TXTRecord"
const zoneType = "Zone"
type entityResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Properties string `json:"properties"`
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
UserName string
Password string
ConfigName string
DNSView string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("BLUECAT_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("BLUECAT_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("BLUECAT_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("BLUECAT_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// Bluecat's Address Manager REST API to manage TXT records for a domain.
type DNSProvider struct {
baseURL string
userName string
password string
configName string
dnsView string
config *Config
token string
client *http.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS.
// Credentials must be passed in the environment variables: BLUECAT_SERVER_URL,
// BLUECAT_USER_NAME and BLUECAT_PASSWORD. BLUECAT_SERVER_URL should have the
// scheme, hostname, and port (if required) of the authoritative Bluecat BAM
// server. The REST endpoint will be appended. In addition, the Configuration name
// and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and
// BLUECAT_DNS_VIEW
// Credentials must be passed in the environment variables: BLUECAT_SERVER_URL, BLUECAT_USER_NAME and BLUECAT_PASSWORD.
// BLUECAT_SERVER_URL should have the scheme, hostname, and port (if required) of the authoritative Bluecat BAM server.
// The REST endpoint will be appended. In addition, the Configuration name
// and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and BLUECAT_DNS_VIEW
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("BLUECAT_SERVER_URL", "BLUECAT_USER_NAME", "BLUECAT_CONFIG_NAME", "BLUECAT_CONFIG_NAME", "BLUECAT_DNS_VIEW")
if err != nil {
return nil, fmt.Errorf("BlueCat: %v", err)
return nil, fmt.Errorf("bluecat: %v", err)
}
httpClient := &http.Client{Timeout: 30 * time.Second}
config := NewDefaultConfig()
config.BaseURL = values["BLUECAT_SERVER_URL"]
config.UserName = values["BLUECAT_USER_NAME"]
config.Password = values["BLUECAT_PASSWORD"]
config.ConfigName = values["BLUECAT_CONFIG_NAME"]
config.DNSView = values["BLUECAT_DNS_VIEW"]
return NewDNSProviderCredentials(
values["BLUECAT_SERVER_URL"],
values["BLUECAT_USER_NAME"],
values["BLUECAT_PASSWORD"],
values["BLUECAT_CONFIG_NAME"],
values["BLUECAT_DNS_VIEW"],
httpClient,
)
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Bluecat DNS.
func NewDNSProviderCredentials(server, userName, password, configName, dnsView string, httpClient *http.Client) (*DNSProvider, error) {
if server == "" || userName == "" || password == "" || configName == "" || dnsView == "" {
return nil, fmt.Errorf("Bluecat credentials missing")
}
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Bluecat DNS.
// Deprecated
func NewDNSProviderCredentials(baseURL, userName, password, configName, dnsView string, httpClient *http.Client) (*DNSProvider, error) {
config := NewDefaultConfig()
config.BaseURL = baseURL
config.UserName = userName
config.Password = password
config.ConfigName = configName
config.DNSView = dnsView
client := http.DefaultClient
if httpClient != nil {
client = httpClient
config.HTTPClient = httpClient
}
return &DNSProvider{
baseURL: fmt.Sprintf(bluecatURLTemplate, server),
userName: userName,
password: password,
configName: configName,
dnsView: dnsView,
client: client,
}, nil
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("bluecat: the configuration of the DNS provider is nil")
}
if config.BaseURL == "" || config.UserName == "" || config.Password == "" || config.ConfigName == "" || config.DNSView == "" {
return nil, fmt.Errorf("bluecat: credentials missing")
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record using the specified parameters
// This will *not* create a subzone to contain the TXT record,
// so make sure the FQDN specified is within an extant zone.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewID, err := d.lookupViewID(d.config.DNSView)
if err != nil {
return err
}
parentZoneID, name, err := d.lookupParentZoneID(viewID, fqdn)
if err != nil {
return err
}
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(parentZoneID), 10),
}
body := bluecatEntity{
Name: name,
Type: "TXTRecord",
Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", d.config.TTL, fqdn, value),
}
resp, err := d.sendRequest(http.MethodPost, "addEntity", body, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
addTxtBytes, _ := ioutil.ReadAll(resp.Body)
addTxtResp := string(addTxtBytes)
// addEntity responds only with body text containing the ID of the created record
_, err = strconv.ParseUint(addTxtResp, 10, 64)
if err != nil {
return fmt.Errorf("bluecat: addEntity request failed: %s", addTxtResp)
}
err = d.deploy(parentZoneID)
if err != nil {
return err
}
return d.logout()
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewID, err := d.lookupViewID(d.config.DNSView)
if err != nil {
return err
}
parentID, name, err := d.lookupParentZoneID(viewID, fqdn)
if err != nil {
return err
}
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(parentID), 10),
"name": name,
"type": txtType,
}
resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
var txtRec entityResponse
err = json.NewDecoder(resp.Body).Decode(&txtRec)
if err != nil {
return fmt.Errorf("bluecat: %v", err)
}
queryArgs = map[string]string{
"objectId": strconv.FormatUint(uint64(txtRec.ID), 10),
}
resp, err = d.sendRequest(http.MethodDelete, http.MethodDelete, nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
err = d.deploy(parentID)
if err != nil {
return err
}
return d.logout()
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Send a REST request, using query parameters specified. The Authorization
// header will be set if we have an active auth token
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) {
url := fmt.Sprintf("%s/%s", d.baseURL, resource)
url := fmt.Sprintf("%s/Services/REST/v1/%s", d.config.BaseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
return nil, err
return nil, fmt.Errorf("bluecat: %v", err)
}
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
return nil, fmt.Errorf("bluecat: %v", err)
}
req.Header.Set("Content-Type", "application/json")
if len(d.token) > 0 {
@ -114,15 +245,15 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{},
q.Add(argName, argVal)
}
req.URL.RawQuery = q.Encode()
resp, err := d.client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("bluecat: %v", err)
}
if resp.StatusCode >= 400 {
errBytes, _ := ioutil.ReadAll(resp.Body)
errResp := string(errBytes)
return nil, fmt.Errorf("Bluecat API request failed with HTTP status code %d\n Full message: %s",
return nil, fmt.Errorf("bluecat: request failed with HTTP status code %d\n Full message: %s",
resp.StatusCode, errResp)
}
@ -133,8 +264,8 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{},
// password and receives a token to be used in for subsequent requests.
func (d *DNSProvider) login() error {
queryArgs := map[string]string{
"username": d.userName,
"password": d.password,
"username": d.config.UserName,
"password": d.config.Password,
}
resp, err := d.sendRequest(http.MethodGet, "login", nil, queryArgs)
@ -145,18 +276,16 @@ func (d *DNSProvider) login() error {
authBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
return fmt.Errorf("bluecat: %v", err)
}
authResp := string(authBytes)
if strings.Contains(authResp, "Authentication Error") {
msg := strings.Trim(authResp, "\"")
return fmt.Errorf("Bluecat API request failed: %s", msg)
return fmt.Errorf("bluecat: request failed: %s", msg)
}
// Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username"
re := regexp.MustCompile("BAMAuthToken: [^ ]+")
token := re.FindString(authResp)
d.token = token
d.token = regexp.MustCompile("BAMAuthToken: [^ ]+").FindString(authResp)
return nil
}
@ -174,7 +303,7 @@ func (d *DNSProvider) logout() error {
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("Bluecat API request failed to delete session with HTTP status code %d", resp.StatusCode)
return fmt.Errorf("bluecat: request failed to delete session with HTTP status code %d", resp.StatusCode)
}
authBytes, err := ioutil.ReadAll(resp.Body)
@ -185,7 +314,7 @@ func (d *DNSProvider) logout() error {
if !strings.Contains(authResp, "successfully") {
msg := strings.Trim(authResp, "\"")
return fmt.Errorf("Bluecat API request failed to delete session: %s", msg)
return fmt.Errorf("bluecat: request failed to delete session: %s", msg)
}
d.token = ""
@ -197,7 +326,7 @@ func (d *DNSProvider) logout() error {
func (d *DNSProvider) lookupConfID() (uint, error) {
queryArgs := map[string]string{
"parentId": strconv.Itoa(0),
"name": d.configName,
"name": d.config.ConfigName,
"type": configType,
}
@ -210,7 +339,7 @@ func (d *DNSProvider) lookupConfID() (uint, error) {
var conf entityResponse
err = json.NewDecoder(resp.Body).Decode(&conf)
if err != nil {
return 0, err
return 0, fmt.Errorf("bluecat: %v", err)
}
return conf.ID, nil
}
@ -224,7 +353,7 @@ func (d *DNSProvider) lookupViewID(viewName string) (uint, error) {
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(confID), 10),
"name": d.dnsView,
"name": d.config.DNSView,
"type": viewType,
}
@ -237,7 +366,7 @@ func (d *DNSProvider) lookupViewID(viewName string) (uint, error) {
var view entityResponse
err = json.NewDecoder(resp.Body).Decode(&view)
if err != nil {
return 0, err
return 0, fmt.Errorf("bluecat: %v", err)
}
return view.ID, nil
@ -280,7 +409,7 @@ func (d *DNSProvider) getZone(parentID uint, name string) (uint, error) {
resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
// Return an empty zone if the named zone doesn't exist
if resp != nil && resp.StatusCode == 404 {
return 0, fmt.Errorf("Bluecat API could not find zone named %s", name)
return 0, fmt.Errorf("bluecat: could not find zone named %s", name)
}
if err != nil {
return 0, err
@ -290,65 +419,12 @@ func (d *DNSProvider) getZone(parentID uint, name string) (uint, error) {
var zone entityResponse
err = json.NewDecoder(resp.Body).Decode(&zone)
if err != nil {
return 0, err
return 0, fmt.Errorf("bluecat: %v", err)
}
return zone.ID, nil
}
// Present creates a TXT record using the specified parameters
// This will *not* create a subzone to contain the TXT record,
// so make sure the FQDN specified is within an extant zone.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewID, err := d.lookupViewID(d.dnsView)
if err != nil {
return err
}
parentZoneID, name, err := d.lookupParentZoneID(viewID, fqdn)
if err != nil {
return err
}
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(parentZoneID), 10),
}
body := bluecatEntity{
Name: name,
Type: "TXTRecord",
Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value),
}
resp, err := d.sendRequest(http.MethodPost, "addEntity", body, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
addTxtBytes, _ := ioutil.ReadAll(resp.Body)
addTxtResp := string(addTxtBytes)
// addEntity responds only with body text containing the ID of the created record
_, err = strconv.ParseUint(addTxtResp, 10, 64)
if err != nil {
return fmt.Errorf("Bluecat API addEntity request failed: %s", addTxtResp)
}
err = d.deploy(parentZoneID)
if err != nil {
return err
}
return d.logout()
}
// Deploy the DNS config for the specified entity to the authoritative servers
func (d *DNSProvider) deploy(entityID uint) error {
queryArgs := map[string]string{
@ -363,65 +439,3 @@ func (d *DNSProvider) deploy(entityID uint) error {
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewID, err := d.lookupViewID(d.dnsView)
if err != nil {
return err
}
parentID, name, err := d.lookupParentZoneID(viewID, fqdn)
if err != nil {
return err
}
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(parentID), 10),
"name": name,
"type": txtType,
}
resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
var txtRec entityResponse
err = json.NewDecoder(resp.Body).Decode(&txtRec)
if err != nil {
return err
}
queryArgs = map[string]string{
"objectId": strconv.FormatUint(uint64(txtRec.ID), 10),
}
resp, err = d.sendRequest(http.MethodDelete, http.MethodDelete, nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
err = d.deploy(parentID)
if err != nil {
return err
}
return d.logout()
}
// JSON body for Bluecat entity requests and responses
type bluecatEntity struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Properties string `json:"properties"`
}

View file

@ -0,0 +1,16 @@
package bluecat
// JSON body for Bluecat entity requests and responses
type bluecatEntity struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Properties string `json:"properties"`
}
type entityResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Properties string `json:"properties"`
}

View file

@ -0,0 +1,212 @@
package cloudflare
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/xenolf/lego/acme"
)
// defaultBaseURL represents the API endpoint to call.
const defaultBaseURL = "https://api.cloudflare.com/client/v4"
// APIError contains error details for failed requests
type APIError struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
ErrorChain []APIError `json:"error_chain,omitempty"`
}
// APIResponse represents a response from Cloudflare API
type APIResponse struct {
Success bool `json:"success"`
Errors []*APIError `json:"errors"`
Result json.RawMessage `json:"result"`
}
// TxtRecord represents a Cloudflare DNS record
type TxtRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
ID string `json:"id,omitempty"`
TTL int `json:"ttl,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
}
// HostedZone represents a Cloudflare DNS zone
type HostedZone struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Client Cloudflare API client
type Client struct {
authEmail string
authKey string
BaseURL string
HTTPClient *http.Client
}
// NewClient create a Cloudflare API client
func NewClient(authEmail string, authKey string) (*Client, error) {
if authEmail == "" {
return nil, errors.New("cloudflare: some credentials information are missing: email")
}
if authKey == "" {
return nil, errors.New("cloudflare: some credentials information are missing: key")
}
return &Client{
authEmail: authEmail,
authKey: authKey,
BaseURL: defaultBaseURL,
HTTPClient: http.DefaultClient,
}, nil
}
// GetHostedZoneID get hosted zone
func (c *Client) GetHostedZoneID(fqdn string) (string, error) {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
result, err := c.doRequest(http.MethodGet, "/zones?name="+acme.UnFqdn(authZone), nil)
if err != nil {
return "", err
}
var hostedZone []HostedZone
err = json.Unmarshal(result, &hostedZone)
if err != nil {
return "", fmt.Errorf("cloudflare: HostedZone unmarshaling error: %v", err)
}
count := len(hostedZone)
if count == 0 {
return "", fmt.Errorf("cloudflare: zone %s not found for domain %s", authZone, fqdn)
} else if count > 1 {
return "", fmt.Errorf("cloudflare: zone %s cannot be find for domain %s: too many hostedZone: %v", authZone, fqdn, hostedZone)
}
return hostedZone[0].ID, nil
}
// FindTxtRecord Find a TXT record
func (c *Client) FindTxtRecord(zoneID, fqdn string) (*TxtRecord, error) {
result, err := c.doRequest(
http.MethodGet,
fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)),
nil,
)
if err != nil {
return nil, err
}
var records []TxtRecord
err = json.Unmarshal(result, &records)
if err != nil {
return nil, fmt.Errorf("cloudflare: record unmarshaling error: %v", err)
}
for _, rec := range records {
fmt.Println(rec.Name, acme.UnFqdn(fqdn))
if rec.Name == acme.UnFqdn(fqdn) {
return &rec, nil
}
}
return nil, fmt.Errorf("cloudflare: no existing record found for %s", fqdn)
}
// AddTxtRecord add a TXT record
func (c *Client) AddTxtRecord(fqdn string, record TxtRecord) error {
zoneID, err := c.GetHostedZoneID(fqdn)
if err != nil {
return err
}
body, err := json.Marshal(record)
if err != nil {
return fmt.Errorf("cloudflare: record marshaling error: %v", err)
}
_, err = c.doRequest(http.MethodPost, fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
return err
}
// RemoveTxtRecord Remove a TXT record
func (c *Client) RemoveTxtRecord(fqdn string) error {
zoneID, err := c.GetHostedZoneID(fqdn)
if err != nil {
return err
}
record, err := c.FindTxtRecord(zoneID, fqdn)
if err != nil {
return err
}
_, err = c.doRequest(http.MethodDelete, fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
return err
}
func (c *Client) doRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.BaseURL, uri), body)
if err != nil {
return nil, err
}
req.Header.Set("X-Auth-Email", c.authEmail)
req.Header.Set("X-Auth-Key", c.authKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cloudflare: error querying API: %v", err)
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("cloudflare: %s", toUnreadableBodyMessage(req, content))
}
var r APIResponse
err = json.Unmarshal(content, &r)
if err != nil {
return nil, fmt.Errorf("cloudflare: APIResponse unmarshaling error: %v: %s", err, toUnreadableBodyMessage(req, content))
}
if !r.Success {
if len(r.Errors) > 0 {
return nil, fmt.Errorf("cloudflare: error \n%s", toError(r))
}
return nil, fmt.Errorf("cloudflare: %s", toUnreadableBodyMessage(req, content))
}
return r.Result, nil
}
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
}
func toError(r APIResponse) error {
errStr := ""
for _, apiErr := range r.Errors {
errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
for _, chainErr := range apiErr.ErrorChain {
errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
}
}
return fmt.Errorf("cloudflare: error \n%s", errStr)
}

View file

@ -0,0 +1,188 @@
package cloudflare
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func handlerMock(method string, response *APIResponse, data interface{}) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Method != method {
content, err := json.Marshal(APIResponse{
Success: false,
Errors: []*APIError{
{
Code: 666,
Message: fmt.Sprintf("invalid method: got %s want %s", req.Method, method),
ErrorChain: nil,
},
},
})
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
http.Error(rw, string(content), http.StatusBadRequest)
return
}
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
response.Result = jsonData
content, err := json.Marshal(response)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.Write(content)
})
}
func TestClient_GetHostedZoneID(t *testing.T) {
type result struct {
zoneID string
error bool
}
testCases := []struct {
desc string
fqdn string
response *APIResponse
data []HostedZone
expected result
}{
{
desc: "zone found",
fqdn: "_acme-challenge.foo.com.",
response: &APIResponse{Success: true},
data: []HostedZone{
{
ID: "A",
Name: "ZONE_A",
},
},
expected: result{zoneID: "A"},
},
{
desc: "no many zones",
fqdn: "_acme-challenge.foo.com.",
response: &APIResponse{Success: true},
data: []HostedZone{
{
ID: "A",
Name: "ZONE_A",
},
{
ID: "B",
Name: "ZONE_B",
},
},
expected: result{error: true},
},
{
desc: "no zone found",
fqdn: "_acme-challenge.foo.com.",
response: &APIResponse{Success: true},
expected: result{error: true},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
server := httptest.NewServer(handlerMock(http.MethodGet, test.response, test.data))
client, _ := NewClient("authEmail", "authKey")
client.BaseURL = server.URL
zoneID, err := client.GetHostedZoneID(test.fqdn)
if test.expected.error {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected.zoneID, zoneID)
}
})
}
}
func TestClient_FindTxtRecord(t *testing.T) {
type result struct {
txtRecord *TxtRecord
error bool
}
testCases := []struct {
desc string
fqdn string
zoneID string
response *APIResponse
data []TxtRecord
expected result
}{
{
desc: "TXT record found",
fqdn: "_acme-challenge.foo.com.",
zoneID: "ZONE_A",
response: &APIResponse{Success: true},
data: []TxtRecord{
{
Name: "_acme-challenge.foo.com",
Type: "TXT",
Content: "txtTXTtxtTXTtxtTXTtxtTXT",
ID: "A",
TTL: 50,
ZoneID: "ZONE_A",
},
},
expected: result{
txtRecord: &TxtRecord{
Name: "_acme-challenge.foo.com",
Type: "TXT",
Content: "txtTXTtxtTXTtxtTXTtxtTXT",
ID: "A",
TTL: 50,
ZoneID: "ZONE_A",
},
},
},
{
desc: "TXT record not found",
fqdn: "_acme-challenge.foo.com.",
zoneID: "ZONE_A",
response: &APIResponse{Success: true},
expected: result{error: true},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
server := httptest.NewServer(handlerMock(http.MethodGet, test.response, test.data))
client, _ := NewClient("authEmail", "authKey")
client.BaseURL = server.URL
txtRecord, err := client.FindTxtRecord(test.zoneID, test.fqdn)
if test.expected.error {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected.txtRecord, txtRecord)
}
})
}
}

View file

@ -3,12 +3,8 @@
package cloudflare
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
@ -17,208 +13,108 @@ import (
)
// CloudFlareAPIURL represents the API endpoint to call.
// TODO: Unexport?
const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4"
const CloudFlareAPIURL = defaultBaseURL // Deprecated
// Config is used to configure the creation of the DNSProvider
type Config struct {
AuthEmail string
AuthKey string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("CLOUDFLARE_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("CLOUDFLARE_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("CLOUDFLARE_POLLING_INTERVAL", 2*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("CLOUDFLARE_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
authEmail string
authKey string
client *http.Client
client *Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for cloudflare.
// Credentials must be passed in the environment variables: CLOUDFLARE_EMAIL
// and CLOUDFLARE_API_KEY.
// NewDNSProvider returns a DNSProvider instance configured for Cloudflare.
// Credentials must be passed in the environment variables:
// CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("CLOUDFLARE_EMAIL", "CLOUDFLARE_API_KEY")
if err != nil {
return nil, fmt.Errorf("CloudFlare: %v", err)
return nil, fmt.Errorf("cloudflare: %v", err)
}
return NewDNSProviderCredentials(values["CLOUDFLARE_EMAIL"], values["CLOUDFLARE_API_KEY"])
config := NewDefaultConfig()
config.AuthEmail = values["CLOUDFLARE_EMAIL"]
config.AuthKey = values["CLOUDFLARE_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for cloudflare.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Cloudflare.
// Deprecated
func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
if email == "" || key == "" {
return nil, errors.New("CloudFlare: some credentials information are missing")
config := NewDefaultConfig()
config.AuthEmail = email
config.AuthKey = key
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Cloudflare.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("cloudflare: the configuration of the DNS provider is nil")
}
client, err := NewClient(config.AuthEmail, config.AuthKey)
if err != nil {
return nil, err
}
client.HTTPClient = config.HTTPClient
// TODO: must be remove. keep only for compatibility reason.
client.BaseURL = CloudFlareAPIURL
return &DNSProvider{
authEmail: email,
authKey: key,
client: &http.Client{Timeout: 30 * time.Second},
client: client,
config: config,
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 120 * time.Second, 2 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
zoneID, err := d.getHostedZoneID(fqdn)
if err != nil {
return err
}
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
rec := cloudFlareRecord{
rec := TxtRecord{
Type: "TXT",
Name: acme.UnFqdn(fqdn),
Content: value,
TTL: ttl,
TTL: d.config.TTL,
}
body, err := json.Marshal(rec)
if err != nil {
return err
}
_, err = d.doRequest(http.MethodPost, fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
return err
return d.client.AddTxtRecord(fqdn, rec)
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
record, err := d.findTxtRecord(fqdn)
if err != nil {
return err
}
_, err = d.doRequest(http.MethodDelete, fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
return err
}
func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
// HostedZone represents a CloudFlare DNS zone
type HostedZone struct {
ID string `json:"id"`
Name string `json:"name"`
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
result, err := d.doRequest(http.MethodGet, "/zones?name="+acme.UnFqdn(authZone), nil)
if err != nil {
return "", err
}
var hostedZone []HostedZone
err = json.Unmarshal(result, &hostedZone)
if err != nil {
return "", err
}
if len(hostedZone) != 1 {
return "", fmt.Errorf("zone %s not found in CloudFlare for domain %s", authZone, fqdn)
}
return hostedZone[0].ID, nil
}
func (d *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
zoneID, err := d.getHostedZoneID(fqdn)
if err != nil {
return nil, err
}
result, err := d.doRequest(
http.MethodGet,
fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)),
nil,
)
if err != nil {
return nil, err
}
var records []cloudFlareRecord
err = json.Unmarshal(result, &records)
if err != nil {
return nil, err
}
for _, rec := range records {
if rec.Name == acme.UnFqdn(fqdn) {
return &rec, nil
}
}
return nil, fmt.Errorf("no existing record found for %s", fqdn)
}
func (d *DNSProvider) doRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareAPIURL, uri), body)
if err != nil {
return nil, err
}
req.Header.Set("X-Auth-Email", d.authEmail)
req.Header.Set("X-Auth-Key", d.authKey)
resp, err := d.client.Do(req)
if err != nil {
return nil, fmt.Errorf("error querying Cloudflare API -> %v", err)
}
defer resp.Body.Close()
var r APIResponse
err = json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
return nil, err
}
if !r.Success {
if len(r.Errors) > 0 {
errStr := ""
for _, apiErr := range r.Errors {
errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
for _, chainErr := range apiErr.ErrorChain {
errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
}
}
return nil, fmt.Errorf("Cloudflare API Error \n%s", errStr)
}
strBody := "Unreadable body"
if body, err := ioutil.ReadAll(resp.Body); err == nil {
strBody = string(body)
}
return nil, fmt.Errorf("Cloudflare API error: the request %s sent a response with a body which is not in JSON format: %s", req.URL.String(), strBody)
}
return r.Result, nil
}
// APIError contains error details for failed requests
type APIError struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
ErrorChain []APIError `json:"error_chain,omitempty"`
}
// APIResponse represents a response from CloudFlare API
type APIResponse struct {
Success bool `json:"success"`
Errors []*APIError `json:"errors"`
Result json.RawMessage `json:"result"`
}
// cloudFlareRecord represents a CloudFlare DNS record
type cloudFlareRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
ID string `json:"id,omitempty"`
TTL int `json:"ttl,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
return d.client.RemoveTxtRecord(fqdn)
}

View file

@ -34,7 +34,11 @@ func TestNewDNSProviderValid(t *testing.T) {
os.Setenv("CLOUDFLARE_API_KEY", "")
defer restoreEnv()
_, err := NewDNSProviderCredentials("123", "123")
config := NewDefaultConfig()
config.AuthEmail = "123"
config.AuthKey = "123"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
@ -54,7 +58,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("CLOUDFLARE_API_KEY", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "CloudFlare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY")
assert.EqualError(t, err, "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY")
}
func TestNewDNSProviderMissingCredErrSingle(t *testing.T) {
@ -62,7 +66,7 @@ func TestNewDNSProviderMissingCredErrSingle(t *testing.T) {
os.Setenv("CLOUDFLARE_EMAIL", "awesome@possum.com")
_, err := NewDNSProvider()
assert.EqualError(t, err, "CloudFlare: some credentials information are missing: CLOUDFLARE_API_KEY")
assert.EqualError(t, err, "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY")
}
func TestCloudFlarePresent(t *testing.T) {
@ -70,7 +74,11 @@ func TestCloudFlarePresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(cflareEmail, cflareAPIKey)
config := NewDefaultConfig()
config.AuthEmail = cflareEmail
config.AuthKey = cflareAPIKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(cflareDomain, "", "123d==")
@ -84,7 +92,11 @@ func TestCloudFlareCleanUp(t *testing.T) {
time.Sleep(time.Second * 2)
provider, err := NewDNSProviderCredentials(cflareEmail, cflareAPIKey)
config := NewDefaultConfig()
config.AuthEmail = cflareEmail
config.AuthKey = cflareAPIKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(cflareDomain, "", "123d==")

View file

@ -0,0 +1,26 @@
package digitalocean
const defaultBaseURL = "https://api.digitalocean.com"
// txtRecordRequest represents the request body to DO's API to make a TXT record
type txtRecordRequest struct {
RecordType string `json:"type"`
Name string `json:"name"`
Data string `json:"data"`
TTL int `json:"ttl"`
}
// txtRecordResponse represents a response from DO's API after making a TXT record
type txtRecordResponse struct {
DomainRecord struct {
ID int `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Data string `json:"data"`
} `json:"domain_record"`
}
type digitalOceanAPIError struct {
ID string `json:"id"`
Message string `json:"message"`
}

View file

@ -5,7 +5,10 @@ package digitalocean
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"sync"
"time"
@ -14,13 +17,35 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
AuthToken string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
BaseURL: defaultBaseURL,
TTL: env.GetOrDefaultInt("DO_TTL", 30),
PropagationTimeout: env.GetOrDefaultSecond("DO_PROPAGATION_TIMEOUT", 60*time.Second),
PollingInterval: env.GetOrDefaultSecond("DO_POLLING_INTERVAL", 5*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DO_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// that uses DigitalOcean's REST API to manage TXT records for a domain.
type DNSProvider struct {
apiAuthToken string
config *Config
recordIDs map[string]int
recordIDsMu sync.Mutex
client *http.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Digital
@ -29,74 +54,60 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DO_AUTH_TOKEN")
if err != nil {
return nil, fmt.Errorf("DigitalOcean: %v", err)
return nil, fmt.Errorf("digitalocean: %v", err)
}
return NewDNSProviderCredentials(values["DO_AUTH_TOKEN"])
config := NewDefaultConfig()
config.AuthToken = values["DO_AUTH_TOKEN"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Digital Ocean.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Digital Ocean.
// Deprecated
func NewDNSProviderCredentials(apiAuthToken string) (*DNSProvider, error) {
if apiAuthToken == "" {
return nil, fmt.Errorf("DigitalOcean credentials missing")
config := NewDefaultConfig()
config.AuthToken = apiAuthToken
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Digital Ocean.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("digitalocean: the configuration of the DNS provider is nil")
}
if config.AuthToken == "" {
return nil, fmt.Errorf("digitalocean: credentials missing")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
return &DNSProvider{
apiAuthToken: apiAuthToken,
config: config,
recordIDs: make(map[string]int),
client: &http.Client{Timeout: 30 * time.Second},
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 60 * time.Second, 5 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
respData, err := d.addTxtRecord(domain, fqdn, value)
if err != nil {
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
return fmt.Errorf("digitalocean: %v", err)
}
authZone = acme.UnFqdn(authZone)
reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone)
reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value, TTL: 30}
body, err := json.Marshal(reqData)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken))
resp, err := d.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
var errInfo digitalOceanAPIError
json.NewDecoder(resp.Body).Decode(&errInfo)
return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
}
// Everything looks good; but we'll need the ID later to delete the record
var respData txtRecordResponse
err = json.NewDecoder(resp.Body).Decode(&respData)
if err != nil {
return err
}
d.recordIDsMu.Lock()
d.recordIDs[fqdn] = respData.DomainRecord.ID
d.recordIDsMu.Unlock()
@ -113,35 +124,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
recordID, ok := d.recordIDs[fqdn]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("unknown record ID for '%s'", fqdn)
return fmt.Errorf("digitalocean: unknown record ID for '%s'", fqdn)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
err := d.removeTxtRecord(domain, recordID)
if err != nil {
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", digitalOceanBaseURL, authZone, recordID)
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken))
resp, err := d.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
var errInfo digitalOceanAPIError
json.NewDecoder(resp.Body).Decode(&errInfo)
return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
return fmt.Errorf("digitalocean: %v", err)
}
// Delete record ID from map
@ -152,27 +140,101 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
type digitalOceanAPIError struct {
ID string `json:"id"`
Message string `json:"message"`
func (d *DNSProvider) removeTxtRecord(domain string, recordID int) error {
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
}
var digitalOceanBaseURL = "https://api.digitalocean.com"
// txtRecordRequest represents the request body to DO's API to make a TXT record
type txtRecordRequest struct {
RecordType string `json:"type"`
Name string `json:"name"`
Data string `json:"data"`
TTL int `json:"ttl"`
reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", d.config.BaseURL, acme.UnFqdn(authZone), recordID)
req, err := d.newRequest(http.MethodDelete, reqURL, nil)
if err != nil {
return err
}
// txtRecordResponse represents a response from DO's API after making a TXT record
type txtRecordResponse struct {
DomainRecord struct {
ID int `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Data string `json:"data"`
} `json:"domain_record"`
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return readError(req, resp)
}
return nil
}
func (d *DNSProvider) addTxtRecord(domain, fqdn, value string) (*txtRecordResponse, error) {
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return nil, fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
}
reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value, TTL: d.config.TTL}
body, err := json.Marshal(reqData)
if err != nil {
return nil, err
}
reqURL := fmt.Sprintf("%s/v2/domains/%s/records", d.config.BaseURL, acme.UnFqdn(authZone))
req, err := d.newRequest(http.MethodPost, reqURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, readError(req, resp)
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.New(toUnreadableBodyMessage(req, content))
}
// Everything looks good; but we'll need the ID later to delete the record
respData := &txtRecordResponse{}
err = json.Unmarshal(content, respData)
if err != nil {
return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(req, content))
}
return respData, nil
}
func (d *DNSProvider) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, reqURL, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.config.AuthToken))
return req, nil
}
func readError(req *http.Request, resp *http.Response) error {
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.New(toUnreadableBodyMessage(req, content))
}
var errInfo digitalOceanAPIError
err = json.Unmarshal(content, &errInfo)
if err != nil {
return fmt.Errorf("digitalOceanAPIError unmarshaling error: %v: %s", err, toUnreadableBodyMessage(req, content))
}
return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
}
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
}

View file

@ -42,13 +42,16 @@ func TestDigitalOceanPresent(t *testing.T) {
}`)
}))
defer mock.Close()
digitalOceanBaseURL = mock.URL
doprov, err := NewDNSProviderCredentials(fakeDigitalOceanAuth)
config := NewDefaultConfig()
config.AuthToken = fakeDigitalOceanAuth
config.BaseURL = mock.URL
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
require.NotNil(t, doprov)
require.NotNil(t, provider)
err = doprov.Present("example.com", "", "foobar")
err = provider.Present("example.com", "", "foobar")
require.NoError(t, err, "fail to create TXT record")
assert.True(t, requestReceived, "Expected request to be received by mock backend, but it wasn't")
@ -69,17 +72,20 @@ func TestDigitalOceanCleanUp(t *testing.T) {
w.WriteHeader(http.StatusNoContent)
}))
defer mock.Close()
digitalOceanBaseURL = mock.URL
doprov, err := NewDNSProviderCredentials(fakeDigitalOceanAuth)
config := NewDefaultConfig()
config.AuthToken = fakeDigitalOceanAuth
config.BaseURL = mock.URL
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
require.NotNil(t, doprov)
require.NotNil(t, provider)
doprov.recordIDsMu.Lock()
doprov.recordIDs["_acme-challenge.example.com."] = 1234567
doprov.recordIDsMu.Unlock()
provider.recordIDsMu.Lock()
provider.recordIDs["_acme-challenge.example.com."] = 1234567
provider.recordIDsMu.Unlock()
err = doprov.CleanUp("example.com", "", "")
err = provider.CleanUp("example.com", "", "")
require.NoError(t, err, "fail to remove TXT record")
assert.True(t, requestReceived, "Expected request to be received by mock backend, but it wasn't")

View file

@ -3,17 +3,39 @@
package dnsimple
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/dnsimple/dnsimple-go/dnsimple"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
AccessToken string
BaseURL string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DNSIMPLE_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("DNSIMPLE_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DNSIMPLE_POLLING_INTERVAL", acme.DefaultPollingInterval),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *dnsimple.Client
}
@ -22,24 +44,39 @@ type DNSProvider struct {
//
// See: https://developer.dnsimple.com/v2/#authentication
func NewDNSProvider() (*DNSProvider, error) {
accessToken := os.Getenv("DNSIMPLE_OAUTH_TOKEN")
baseURL := os.Getenv("DNSIMPLE_BASE_URL")
config := NewDefaultConfig()
config.AccessToken = os.Getenv("DNSIMPLE_OAUTH_TOKEN")
config.BaseURL = os.Getenv("DNSIMPLE_BASE_URL")
return NewDNSProviderCredentials(accessToken, baseURL)
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for dnsimple.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for DNSimple.
// Deprecated
func NewDNSProviderCredentials(accessToken, baseURL string) (*DNSProvider, error) {
if accessToken == "" {
return nil, fmt.Errorf("DNSimple OAuth token is missing")
config := NewDefaultConfig()
config.AccessToken = accessToken
config.BaseURL = baseURL
return NewDNSProviderConfig(config)
}
client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(accessToken))
client.UserAgent = "lego"
// NewDNSProviderConfig return a DNSProvider instance configured for DNSimple.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dnsimple: the configuration of the DNS provider is nil")
}
if baseURL != "" {
client.BaseURL = baseURL
if config.AccessToken == "" {
return nil, fmt.Errorf("dnsimple: OAuth token is missing")
}
client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(config.AccessToken))
client.UserAgent = acme.UserAgent
if config.BaseURL != "" {
client.BaseURL = config.BaseURL
}
return &DNSProvider{client: client}, nil
@ -47,10 +84,9 @@ func NewDNSProviderCredentials(accessToken, baseURL string) (*DNSProvider, error
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneName, err := d.getHostedZone(domain)
if err != nil {
return err
}
@ -60,10 +96,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return err
}
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, ttl)
_, err = d.client.Zones.CreateRecord(accountID, zoneName, *recordAttributes)
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, d.config.TTL)
_, err = d.client.Zones.CreateRecord(accountID, zoneName, recordAttributes)
if err != nil {
return fmt.Errorf("DNSimple API call failed: %v", err)
return fmt.Errorf("API call failed: %v", err)
}
return nil
@ -93,6 +129,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(domain string) (string, error) {
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
@ -108,7 +150,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) {
zones, err := d.client.Zones.ListZones(accountID, &dnsimple.ZoneListOptions{NameLike: zoneName})
if err != nil {
return "", fmt.Errorf("DNSimple API call failed: %v", err)
return "", fmt.Errorf("API call failed: %v", err)
}
var hostedZone dnsimple.Zone
@ -140,16 +182,16 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnsimple.ZoneRecord
result, err := d.client.Zones.ListRecords(accountID, zoneName, &dnsimple.ZoneRecordListOptions{Name: recordName, Type: "TXT", ListOptions: dnsimple.ListOptions{}})
if err != nil {
return []dnsimple.ZoneRecord{}, fmt.Errorf("DNSimple API call has failed: %v", err)
return []dnsimple.ZoneRecord{}, fmt.Errorf("API call has failed: %v", err)
}
return result.Data, nil
}
func (d *DNSProvider) newTxtRecord(zoneName, fqdn, value string, ttl int) *dnsimple.ZoneRecord {
func (d *DNSProvider) newTxtRecord(zoneName, fqdn, value string, ttl int) dnsimple.ZoneRecord {
name := d.extractRecordName(fqdn, zoneName)
return &dnsimple.ZoneRecord{
return dnsimple.ZoneRecord{
Type: "TXT",
Name: name,
Content: value,
@ -172,7 +214,7 @@ func (d *DNSProvider) getAccountID() (string, error) {
}
if whoamiResponse.Data.Account == nil {
return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token")
return "", fmt.Errorf("user tokens are not supported, please use an account token")
}
return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil

View file

@ -6,6 +6,8 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xenolf/lego/acme"
)
var (
@ -44,6 +46,8 @@ func TestNewDNSProviderValid(t *testing.T) {
defer restoreEnv()
os.Setenv("DNSIMPLE_OAUTH_TOKEN", "123")
acme.UserAgent = "lego"
provider, err := NewDNSProvider()
assert.NotNil(t, provider)
@ -71,7 +75,7 @@ func TestNewDNSProviderInvalidWithMissingOauthToken(t *testing.T) {
provider, err := NewDNSProvider()
assert.Nil(t, provider)
assert.EqualError(t, err, "DNSimple OAuth token is missing")
assert.EqualError(t, err, "dnsimple: OAuth token is missing")
}
//
@ -79,27 +83,39 @@ func TestNewDNSProviderInvalidWithMissingOauthToken(t *testing.T) {
//
func TestNewDNSProviderCredentialsValid(t *testing.T) {
provider, err := NewDNSProviderCredentials("123", "")
config := NewDefaultConfig()
config.AccessToken = "123"
config.BaseURL = ""
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
require.NotNil(t, provider)
assert.NotNil(t, provider)
assert.Equal(t, "lego", provider.client.UserAgent)
assert.NoError(t, err)
}
func TestNewDNSProviderCredentialsValidWithBaseUrl(t *testing.T) {
provider, err := NewDNSProviderCredentials("123", "https://api.dnsimple.test")
config := NewDefaultConfig()
config.AccessToken = "123"
config.BaseURL = "https://api.dnsimple.test"
assert.NotNil(t, provider)
assert.NoError(t, err)
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
require.NotNil(t, provider)
assert.Equal(t, provider.client.BaseURL, "https://api.dnsimple.test")
}
func TestNewDNSProviderCredentialsInvalidWithMissingOauthToken(t *testing.T) {
provider, err := NewDNSProviderCredentials("", "")
config := NewDefaultConfig()
config.AccessToken = ""
config.BaseURL = ""
provider, err := NewDNSProviderConfig(config)
assert.Nil(t, provider)
assert.EqualError(t, err, "DNSimple OAuth token is missing")
assert.EqualError(t, err, "dnsimple: OAuth token is missing")
}
//
@ -111,7 +127,11 @@ func TestLiveDNSimplePresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseURL)
config := NewDefaultConfig()
config.AccessToken = dnsimpleOauthToken
config.BaseURL = dnsimpleBaseURL
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(dnsimpleDomain, "", "123d==")
@ -129,7 +149,11 @@ func TestLiveDNSimpleCleanUp(t *testing.T) {
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseURL)
config := NewDefaultConfig()
config.AccessToken = dnsimpleOauthToken
config.BaseURL = dnsimpleBaseURL
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(dnsimpleDomain, "", "123d==")

View file

@ -0,0 +1,168 @@
package dnsmadeeasy
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"time"
)
// Domain holds the DNSMadeEasy API representation of a Domain
type Domain struct {
ID int `json:"id"`
Name string `json:"name"`
}
// Record holds the DNSMadeEasy API representation of a Domain Record
type Record struct {
ID int `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL int `json:"ttl"`
SourceID int `json:"sourceId"`
}
// Client DNSMadeEasy client
type Client struct {
apiKey string
apiSecret string
BaseURL string
HTTPClient *http.Client
}
// NewClient creates a DNSMadeEasy client
func NewClient(apiKey string, apiSecret string) (*Client, error) {
if apiKey == "" {
return nil, fmt.Errorf("DNSMadeEasy: credentials missing: API key")
}
if apiSecret == "" {
return nil, fmt.Errorf("DNSMadeEasy: credentials missing: API secret")
}
return &Client{
apiKey: apiKey,
apiSecret: apiSecret,
HTTPClient: &http.Client{},
}, nil
}
// GetDomain gets a domain
func (c *Client) GetDomain(authZone string) (*Domain, error) {
domainName := authZone[0 : len(authZone)-1]
resource := fmt.Sprintf("%s%s", "/dns/managed/name?domainname=", domainName)
resp, err := c.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
domain := &Domain{}
err = json.NewDecoder(resp.Body).Decode(&domain)
if err != nil {
return nil, err
}
return domain, nil
}
// GetRecords gets all TXT records
func (c *Client) GetRecords(domain *Domain, recordName, recordType string) (*[]Record, error) {
resource := fmt.Sprintf("%s/%d/%s%s%s%s", "/dns/managed", domain.ID, "records?recordName=", recordName, "&type=", recordType)
resp, err := c.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
type recordsResponse struct {
Records *[]Record `json:"data"`
}
records := &recordsResponse{}
err = json.NewDecoder(resp.Body).Decode(&records)
if err != nil {
return nil, err
}
return records.Records, nil
}
// CreateRecord creates a TXT records
func (c *Client) CreateRecord(domain *Domain, record *Record) error {
url := fmt.Sprintf("%s/%d/%s", "/dns/managed", domain.ID, "records")
resp, err := c.sendRequest(http.MethodPost, url, record)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// DeleteRecord deletes a TXT records
func (c *Client) DeleteRecord(record Record) error {
resource := fmt.Sprintf("%s/%d/%s/%d", "/dns/managed", record.SourceID, "records", record.ID)
resp, err := c.sendRequest(http.MethodDelete, resource, nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (c *Client) sendRequest(method, resource string, payload interface{}) (*http.Response, error) {
url := fmt.Sprintf("%s%s", c.BaseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
timestamp := time.Now().UTC().Format(time.RFC1123)
signature, err := computeHMAC(timestamp, c.apiSecret)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("x-dnsme-apiKey", c.apiKey)
req.Header.Set("x-dnsme-requestDate", timestamp)
req.Header.Set("x-dnsme-hmac", signature)
req.Header.Set("accept", "application/json")
req.Header.Set("content-type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 {
return nil, fmt.Errorf("DNSMadeEasy API request failed with HTTP status code %d", resp.StatusCode)
}
return resp, nil
}
func computeHMAC(message string, secret string) (string, error) {
key := []byte(secret)
h := hmac.New(sha1.New, key)
_, err := h.Write([]byte(message))
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}

View file

@ -1,12 +1,8 @@
package dnsmadeeasy
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
@ -18,38 +14,46 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
APISecret string
HTTPClient *http.Client
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DNSMADEEASY_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("DNSMADEEASY_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DNSMADEEASY_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DNSMADEEASY_HTTP_TIMEOUT", 10*time.Second),
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// DNSMadeEasy's DNS API to manage TXT records for a domain.
type DNSProvider struct {
baseURL string
apiKey string
apiSecret string
client *http.Client
}
// Domain holds the DNSMadeEasy API representation of a Domain
type Domain struct {
ID int `json:"id"`
Name string `json:"name"`
}
// Record holds the DNSMadeEasy API representation of a Domain Record
type Record struct {
ID int `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL int `json:"ttl"`
SourceID int `json:"sourceId"`
config *Config
client *Client
}
// NewDNSProvider returns a DNSProvider instance configured for DNSMadeEasy DNS.
// Credentials must be passed in the environment variables: DNSMADEEASY_API_KEY
// and DNSMADEEASY_API_SECRET.
// Credentials must be passed in the environment variables:
// DNSMADEEASY_API_KEY and DNSMADEEASY_API_SECRET.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DNSMADEEASY_API_KEY", "DNSMADEEASY_API_SECRET")
if err != nil {
return nil, fmt.Errorf("DNSMadeEasy: %v", err)
return nil, fmt.Errorf("dnsmadeeasy: %v", err)
}
var baseURL string
@ -59,35 +63,53 @@ func NewDNSProvider() (*DNSProvider, error) {
baseURL = "https://api.dnsmadeeasy.com/V2.0"
}
return NewDNSProviderCredentials(baseURL, values["DNSMADEEASY_API_KEY"], values["DNSMADEEASY_API_SECRET"])
config := NewDefaultConfig()
config.BaseURL = baseURL
config.APIKey = values["DNSMADEEASY_API_KEY"]
config.APISecret = values["DNSMADEEASY_API_SECRET"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for DNSMadeEasy.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for DNS Made Easy.
// Deprecated
func NewDNSProviderCredentials(baseURL, apiKey, apiSecret string) (*DNSProvider, error) {
if baseURL == "" || apiKey == "" || apiSecret == "" {
return nil, fmt.Errorf("DNS Made Easy credentials missing")
config := NewDefaultConfig()
config.BaseURL = baseURL
config.APIKey = apiKey
config.APISecret = apiSecret
return NewDNSProviderConfig(config)
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
// NewDNSProviderConfig return a DNSProvider instance configured for DNS Made Easy.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dnsmadeeasy: the configuration of the DNS provider is nil")
}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
if config.BaseURL == "" {
return nil, fmt.Errorf("dnsmadeeasy: base URL missing")
}
client, err := NewClient(config.APIKey, config.APISecret)
if err != nil {
return nil, fmt.Errorf("dnsmadeeasy: %v", err)
}
client.HTTPClient = config.HTTPClient
client.BaseURL = config.BaseURL
return &DNSProvider{
baseURL: baseURL,
apiKey: apiKey,
apiSecret: apiSecret,
client: client,
config: config,
}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domainName, keyAuth)
fqdn, value, _ := acme.DNS01Record(domainName, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
@ -95,16 +117,16 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
}
// fetch the domain details
domain, err := d.getDomain(authZone)
domain, err := d.client.GetDomain(authZone)
if err != nil {
return err
}
// create the TXT record
name := strings.Replace(fqdn, "."+authZone, "", 1)
record := &Record{Type: "TXT", Name: name, Value: value, TTL: ttl}
record := &Record{Type: "TXT", Name: name, Value: value, TTL: d.config.TTL}
err = d.createRecord(domain, record)
err = d.client.CreateRecord(domain, record)
return err
}
@ -118,21 +140,21 @@ func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
}
// fetch the domain details
domain, err := d.getDomain(authZone)
domain, err := d.client.GetDomain(authZone)
if err != nil {
return err
}
// find matching records
name := strings.Replace(fqdn, "."+authZone, "", 1)
records, err := d.getRecords(domain, name, "TXT")
records, err := d.client.GetRecords(domain, name, "TXT")
if err != nil {
return err
}
// delete records
for _, record := range *records {
err = d.deleteRecord(record)
err = d.client.DeleteRecord(record)
if err != nil {
return err
}
@ -141,107 +163,8 @@ func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
return nil
}
func (d *DNSProvider) getDomain(authZone string) (*Domain, error) {
domainName := authZone[0 : len(authZone)-1]
resource := fmt.Sprintf("%s%s", "/dns/managed/name?domainname=", domainName)
resp, err := d.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
domain := &Domain{}
err = json.NewDecoder(resp.Body).Decode(&domain)
if err != nil {
return nil, err
}
return domain, nil
}
func (d *DNSProvider) getRecords(domain *Domain, recordName, recordType string) (*[]Record, error) {
resource := fmt.Sprintf("%s/%d/%s%s%s%s", "/dns/managed", domain.ID, "records?recordName=", recordName, "&type=", recordType)
resp, err := d.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
type recordsResponse struct {
Records *[]Record `json:"data"`
}
records := &recordsResponse{}
err = json.NewDecoder(resp.Body).Decode(&records)
if err != nil {
return nil, err
}
return records.Records, nil
}
func (d *DNSProvider) createRecord(domain *Domain, record *Record) error {
url := fmt.Sprintf("%s/%d/%s", "/dns/managed", domain.ID, "records")
resp, err := d.sendRequest(http.MethodPost, url, record)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (d *DNSProvider) deleteRecord(record Record) error {
resource := fmt.Sprintf("%s/%d/%s/%d", "/dns/managed", record.SourceID, "records", record.ID)
resp, err := d.sendRequest(http.MethodDelete, resource, nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*http.Response, error) {
url := fmt.Sprintf("%s%s", d.baseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
timestamp := time.Now().UTC().Format(time.RFC1123)
signature := computeHMAC(timestamp, d.apiSecret)
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("x-dnsme-apiKey", d.apiKey)
req.Header.Set("x-dnsme-requestDate", timestamp)
req.Header.Set("x-dnsme-hmac", signature)
req.Header.Set("accept", "application/json")
req.Header.Set("content-type", "application/json")
resp, err := d.client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 {
return nil, fmt.Errorf("DNSMadeEasy API request failed with HTTP status code %d", resp.StatusCode)
}
return resp, nil
}
func computeHMAC(message string, secret string) string {
key := []byte(secret)
h := hmac.New(sha1.New, key)
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -3,16 +3,42 @@
package dnspod
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/decker502/dnspod-go"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
LoginToken string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DNSPOD_TTL", 600),
PropagationTimeout: env.GetOrDefaultSecond("ALICLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("ALICLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DNSPOD_HTTP_TIMEOUT", 0),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *dnspod.Client
}
@ -21,37 +47,55 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DNSPOD_API_KEY")
if err != nil {
return nil, fmt.Errorf("DNSPod: %v", err)
return nil, fmt.Errorf("dnspod: %v", err)
}
return NewDNSProviderCredentials(values["DNSPOD_API_KEY"])
config := NewDefaultConfig()
config.LoginToken = values["DNSPOD_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for dnspod.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for dnspod.
// Deprecated
func NewDNSProviderCredentials(key string) (*DNSProvider, error) {
if key == "" {
return nil, fmt.Errorf("dnspod credentials missing")
config := NewDefaultConfig()
config.LoginToken = key
return NewDNSProviderConfig(config)
}
params := dnspod.CommonParams{LoginToken: key, Format: "json"}
return &DNSProvider{
client: dnspod.NewClient(params),
}, nil
// NewDNSProviderConfig return a DNSProvider instance configured for dnspod.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dnspod: the configuration of the DNS provider is nil")
}
if config.LoginToken == "" {
return nil, fmt.Errorf("dnspod: credentials missing")
}
params := dnspod.CommonParams{LoginToken: config.LoginToken, Format: "json"}
client := dnspod.NewClient(params)
client.HttpClient = config.HTTPClient
return &DNSProvider{client: client}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneID, zoneName, err := d.getHostedZone(domain)
if err != nil {
return err
}
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, ttl)
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, d.config.TTL)
_, _, err = d.client.Domains.CreateRecord(zoneID, *recordAttributes)
if err != nil {
return fmt.Errorf("dnspod API call failed: %v", err)
return fmt.Errorf("API call failed: %v", err)
}
return nil
@ -80,10 +124,16 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(domain string) (string, string, error) {
zones, _, err := d.client.Domains.List()
if err != nil {
return "", "", fmt.Errorf("dnspod API call failed: %v", err)
return "", "", fmt.Errorf("API call failed: %v", err)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
@ -114,7 +164,7 @@ func (d *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnspod.Re
Name: name,
Value: value,
Line: "默认",
TTL: "600",
TTL: strconv.Itoa(ttl),
}
}
@ -127,7 +177,7 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnspod.Record, erro
var records []dnspod.Record
result, _, err := d.client.Domains.ListRecords(zoneID, "")
if err != nil {
return records, fmt.Errorf("dnspod API call has failed: %v", err)
return records, fmt.Errorf("API call has failed: %v", err)
}
recordName := d.extractRecordName(fqdn, zoneName)

View file

@ -30,7 +30,10 @@ func TestNewDNSProviderValid(t *testing.T) {
defer restoreEnv()
os.Setenv("DNSPOD_API_KEY", "")
_, err := NewDNSProviderCredentials("123")
config := NewDefaultConfig()
config.LoginToken = "123"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
func TestNewDNSProviderValidEnv(t *testing.T) {
@ -46,7 +49,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("DNSPOD_API_KEY", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "DNSPod: some credentials information are missing: DNSPOD_API_KEY")
assert.EqualError(t, err, "dnspod: some credentials information are missing: DNSPOD_API_KEY")
}
func TestLivednspodPresent(t *testing.T) {
@ -54,7 +57,10 @@ func TestLivednspodPresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(dnspodAPIKey)
config := NewDefaultConfig()
config.LoginToken = dnspodAPIKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(dnspodDomain, "", "123d==")
@ -68,7 +74,10 @@ func TestLivednspodCleanUp(t *testing.T) {
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderCredentials(dnspodAPIKey)
config := NewDefaultConfig()
config.LoginToken = dnspodAPIKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(dnspodDomain, "", "123d==")

View file

@ -6,15 +6,36 @@ import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Token string
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
client := acme.HTTPClient
client.Timeout = env.GetOrDefaultSecond("DUCKDNS_HTTP_TIMEOUT", 30*time.Second)
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("DUCKDNS_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DUCKDNS_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &client,
}
}
// DNSProvider adds and removes the record for the DNS challenge
type DNSProvider struct {
// The api token
token string
config *Config
}
// NewDNSProvider returns a new DNS provider using
@ -22,31 +43,53 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DUCKDNS_TOKEN")
if err != nil {
return nil, fmt.Errorf("DuckDNS: %v", err)
return nil, fmt.Errorf("duckdns: %v", err)
}
return NewDNSProviderCredentials(values["DUCKDNS_TOKEN"])
config := NewDefaultConfig()
config.Token = values["DUCKDNS_TOKEN"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for http://duckdns.org .
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for http://duckdns.org
// Deprecated
func NewDNSProviderCredentials(token string) (*DNSProvider, error) {
if token == "" {
return nil, errors.New("DuckDNS: credentials missing")
config := NewDefaultConfig()
config.Token = token
return NewDNSProviderConfig(config)
}
return &DNSProvider{token: token}, nil
// NewDNSProviderConfig return a DNSProvider instance configured for DuckDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("duckdns: the configuration of the DNS provider is nil")
}
if config.Token == "" {
return nil, errors.New("duckdns: credentials missing")
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
return updateTxtRecord(domain, d.token, txtRecord, false)
return updateTxtRecord(domain, d.config.Token, txtRecord, false)
}
// CleanUp clears DuckDNS TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return updateTxtRecord(domain, d.token, "", true)
return updateTxtRecord(domain, d.config.Token, "", true)
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// updateTxtRecord Update the domains TXT record

View file

@ -39,7 +39,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("DUCKDNS_TOKEN", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "DuckDNS: some credentials information are missing: DUCKDNS_TOKEN")
assert.EqualError(t, err, "duckdns: some credentials information are missing: DUCKDNS_TOKEN")
}
func TestLiveDuckdnsPresent(t *testing.T) {

View file

@ -0,0 +1,35 @@
package dyn
import "encoding/json"
const defaultBaseURL = "https://api.dynect.net/REST"
type dynResponse struct {
// One of 'success', 'failure', or 'incomplete'
Status string `json:"status"`
// The structure containing the actual results of the request
Data json.RawMessage `json:"data"`
// The ID of the job that was created in response to a request.
JobID int `json:"job_id"`
// A list of zero or more messages
Messages json.RawMessage `json:"msgs"`
}
type creds struct {
Customer string `json:"customer_name"`
User string `json:"user_name"`
Pass string `json:"password"`
}
type session struct {
Token string `json:"token"`
Version string `json:"version"`
}
type publish struct {
Publish bool `json:"publish"`
Notes string `json:"notes"`
}

View file

@ -5,6 +5,7 @@ package dyn
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
@ -14,122 +15,166 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
var dynBaseURL = "https://api.dynect.net/REST"
// Config is used to configure the creation of the DNSProvider
type Config struct {
CustomerName string
UserName string
Password string
HTTPClient *http.Client
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
type dynResponse struct {
// One of 'success', 'failure', or 'incomplete'
Status string `json:"status"`
// The structure containing the actual results of the request
Data json.RawMessage `json:"data"`
// The ID of the job that was created in response to a request.
JobID int `json:"job_id"`
// A list of zero or more messages
Messages json.RawMessage `json:"msgs"`
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DYN_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("DYN_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DYN_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DYN_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// Dyn's Managed DNS API to manage TXT records for a domain.
type DNSProvider struct {
customerName string
userName string
password string
config *Config
token string
client *http.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
// Credentials must be passed in the environment variables: DYN_CUSTOMER_NAME,
// DYN_USER_NAME and DYN_PASSWORD.
// Credentials must be passed in the environment variables:
// DYN_CUSTOMER_NAME, DYN_USER_NAME and DYN_PASSWORD.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DYN_CUSTOMER_NAME", "DYN_USER_NAME", "DYN_PASSWORD")
if err != nil {
return nil, fmt.Errorf("DynDNS: %v", err)
return nil, fmt.Errorf("dyn: %v", err)
}
return NewDNSProviderCredentials(values["DYN_CUSTOMER_NAME"], values["DYN_USER_NAME"], values["DYN_PASSWORD"])
config := NewDefaultConfig()
config.CustomerName = values["DYN_CUSTOMER_NAME"]
config.UserName = values["DYN_USER_NAME"]
config.Password = values["DYN_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Dyn DNS.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Dyn DNS.
// Deprecated
func NewDNSProviderCredentials(customerName, userName, password string) (*DNSProvider, error) {
if customerName == "" || userName == "" || password == "" {
return nil, fmt.Errorf("DynDNS credentials missing")
config := NewDefaultConfig()
config.CustomerName = customerName
config.UserName = userName
config.Password = password
return NewDNSProviderConfig(config)
}
return &DNSProvider{
customerName: customerName,
userName: userName,
password: password,
client: &http.Client{Timeout: 10 * time.Second},
}, nil
// NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dyn: the configuration of the DNS provider is nil")
}
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) {
url := fmt.Sprintf("%s/%s", dynBaseURL, resource)
if config.CustomerName == "" || config.UserName == "" || config.Password == "" {
return nil, fmt.Errorf("dyn: credentials missing")
}
body, err := json.Marshal(payload)
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return nil, err
return fmt.Errorf("dyn: %v", err)
}
req, err := http.NewRequest(method, url, bytes.NewReader(body))
err = d.login()
if err != nil {
return nil, err
return fmt.Errorf("dyn: %v", err)
}
data := map[string]interface{}{
"rdata": map[string]string{
"txtdata": value,
},
"ttl": strconv.Itoa(d.config.TTL),
}
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
_, err = d.sendRequest(http.MethodPost, resource, data)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client")
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
return d.logout()
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
err = d.login()
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
url := fmt.Sprintf("%s/%s", defaultBaseURL, resource)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
req.Header.Set("Content-Type", "application/json")
if len(d.token) > 0 {
req.Header.Set("Auth-Token", d.token)
}
resp, err := d.client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
return fmt.Errorf("dyn: %v", err)
}
defer resp.Body.Close()
resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("dyn: API request failed to delete TXT record HTTP status code %d", resp.StatusCode)
}
var dynRes dynResponse
err = json.NewDecoder(resp.Body).Decode(&dynRes)
err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client")
if err != nil {
return nil, err
return fmt.Errorf("dyn: %v", err)
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages)
} else if resp.StatusCode == 307 {
// TODO add support for HTTP 307 response and long running jobs
return nil, fmt.Errorf("Dyn API request returned HTTP 307. This is currently unsupported")
return d.logout()
}
if dynRes.Status == "failure" {
// TODO add better error handling
return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages)
}
return &dynRes, nil
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Starts a new Dyn API Session. Authenticates using customerName, userName,
// password and receives a token to be used in for subsequent requests.
func (d *DNSProvider) login() error {
type creds struct {
Customer string `json:"customer_name"`
User string `json:"user_name"`
Pass string `json:"password"`
}
type session struct {
Token string `json:"token"`
Version string `json:"version"`
}
payload := &creds{Customer: d.customerName, User: d.userName, Pass: d.password}
payload := &creds{Customer: d.config.CustomerName, User: d.config.UserName, Pass: d.config.Password}
dynRes, err := d.sendRequest(http.MethodPost, "Session", payload)
if err != nil {
return err
@ -153,7 +198,7 @@ func (d *DNSProvider) logout() error {
return nil
}
url := fmt.Sprintf("%s/Session", dynBaseURL)
url := fmt.Sprintf("%s/Session", defaultBaseURL)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return err
@ -161,14 +206,14 @@ func (d *DNSProvider) logout() error {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Auth-Token", d.token)
resp, err := d.client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("Dyn API request failed to delete session with HTTP status code %d", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("API request failed to delete session with HTTP status code %d", resp.StatusCode)
}
d.token = ""
@ -176,47 +221,7 @@ func (d *DNSProvider) logout() error {
return nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return err
}
err = d.login()
if err != nil {
return err
}
data := map[string]interface{}{
"rdata": map[string]string{
"txtdata": value,
},
"ttl": strconv.Itoa(ttl),
}
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
_, err = d.sendRequest(http.MethodPost, resource, data)
if err != nil {
return err
}
err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client")
if err != nil {
return err
}
return d.logout()
}
func (d *DNSProvider) publish(zone, notes string) error {
type publish struct {
Publish bool `json:"publish"`
Notes string `json:"notes"`
}
pub := &publish{Publish: true, Notes: notes}
resource := fmt.Sprintf("Zone/%s/", zone)
@ -224,45 +229,50 @@ func (d *DNSProvider) publish(zone, notes string) error {
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) {
url := fmt.Sprintf("%s/%s", defaultBaseURL, resource)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
body, err := json.Marshal(payload)
if err != nil {
return err
return nil, err
}
err = d.login()
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return err
return nil, err
}
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
url := fmt.Sprintf("%s/%s", dynBaseURL, resource)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if len(d.token) > 0 {
req.Header.Set("Auth-Token", d.token)
}
resp, err := d.client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
return nil, err
}
resp.Body.Close()
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("Dyn API request failed to delete TXT record HTTP status code %d", resp.StatusCode)
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("API request failed with HTTP status code %d", resp.StatusCode)
}
err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client")
var dynRes dynResponse
err = json.NewDecoder(resp.Body).Decode(&dynRes)
if err != nil {
return err
return nil, err
}
return d.logout()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages)
} else if resp.StatusCode == 307 {
// TODO add support for HTTP 307 response and long running jobs
return nil, fmt.Errorf("API request returned HTTP 307. This is currently unsupported")
}
if dynRes.Status == "failure" {
// TODO add better error handling
return nil, fmt.Errorf("API request failed: %s", dynRes.Messages)
}
return &dynRes, nil
}

View file

@ -5,15 +5,43 @@ package exoscale
import (
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/exoscale/egoscale"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const defaultBaseURL = "https://api.exoscale.ch/dns"
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
APISecret string
Endpoint string
HTTPClient *http.Client
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("EXOSCALE_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("EXOSCALE_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("EXOSCALE_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("EXOSCALE_HTTP_TIMEOUT", 0),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *egoscale.Client
}
@ -22,32 +50,52 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("EXOSCALE_API_KEY", "EXOSCALE_API_SECRET")
if err != nil {
return nil, fmt.Errorf("Exoscale: %v", err)
return nil, fmt.Errorf("exoscale: %v", err)
}
endpoint := os.Getenv("EXOSCALE_ENDPOINT")
return NewDNSProviderClient(values["EXOSCALE_API_KEY"], values["EXOSCALE_API_SECRET"], endpoint)
config := NewDefaultConfig()
config.APIKey = values["EXOSCALE_API_KEY"]
config.APISecret = values["EXOSCALE_API_SECRET"]
config.Endpoint = os.Getenv("EXOSCALE_ENDPOINT")
return NewDNSProviderConfig(config)
}
// NewDNSProviderClient Uses the supplied parameters to return a DNSProvider instance
// configured for Exoscale.
// NewDNSProviderClient Uses the supplied parameters
// to return a DNSProvider instance configured for Exoscale.
// Deprecated
func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) {
if key == "" || secret == "" {
return nil, fmt.Errorf("Exoscale credentials missing")
config := NewDefaultConfig()
config.APIKey = key
config.APISecret = secret
config.Endpoint = endpoint
return NewDNSProviderConfig(config)
}
if endpoint == "" {
endpoint = "https://api.exoscale.ch/dns"
// NewDNSProviderConfig return a DNSProvider instance configured for Exoscale.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the DNS provider is nil")
}
return &DNSProvider{
client: egoscale.NewClient(endpoint, key, secret),
}, nil
if config.APIKey == "" || config.APISecret == "" {
return nil, fmt.Errorf("exoscale: credentials missing")
}
if config.Endpoint == "" {
config.Endpoint = defaultBaseURL
}
client := egoscale.NewClient(config.Endpoint, config.APIKey, config.APISecret)
client.HTTPClient = config.HTTPClient
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, recordName, err := d.FindZoneAndRecordName(fqdn, domain)
if err != nil {
return err
@ -61,7 +109,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
if recordID == 0 {
record := egoscale.DNSRecord{
Name: recordName,
TTL: ttl,
TTL: d.config.TTL,
Content: value,
RecordType: "TXT",
}
@ -74,7 +122,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
record := egoscale.UpdateDNSRecord{
ID: recordID,
Name: recordName,
TTL: ttl,
TTL: d.config.TTL,
Content: value,
RecordType: "TXT",
}
@ -111,6 +159,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// FindExistingRecordID Query Exoscale to find an existing record for this name.
// Returns nil if no record could be found
func (d *DNSProvider) FindExistingRecordID(zone, recordName string) (int64, error) {

View file

@ -34,7 +34,11 @@ func TestNewDNSProviderValid(t *testing.T) {
os.Setenv("EXOSCALE_API_KEY", "")
os.Setenv("EXOSCALE_API_SECRET", "")
_, err := NewDNSProviderClient("example@example.com", "123", "")
config := NewDefaultConfig()
config.APIKey = "example@example.com"
config.APISecret = "123"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
@ -53,11 +57,15 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
defer restoreEnv()
_, err := NewDNSProvider()
assert.EqualError(t, err, "Exoscale: some credentials information are missing: EXOSCALE_API_KEY,EXOSCALE_API_SECRET")
assert.EqualError(t, err, "exoscale: some credentials information are missing: EXOSCALE_API_KEY,EXOSCALE_API_SECRET")
}
func TestExtractRootRecordName(t *testing.T) {
provider, err := NewDNSProviderClient("example@example.com", "123", "")
config := NewDefaultConfig()
config.APIKey = "example@example.com"
config.APISecret = "123"
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
zone, recordName, err := provider.FindZoneAndRecordName("_acme-challenge.bar.com.", "bar.com")
@ -67,7 +75,11 @@ func TestExtractRootRecordName(t *testing.T) {
}
func TestExtractSubRecordName(t *testing.T) {
provider, err := NewDNSProviderClient("example@example.com", "123", "")
config := NewDefaultConfig()
config.APIKey = "example@example.com"
config.APISecret = "123"
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
zone, recordName, err := provider.FindZoneAndRecordName("_acme-challenge.foo.bar.com.", "foo.bar.com")
@ -81,7 +93,11 @@ func TestLiveExoscalePresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderClient(exoscaleAPIKey, exoscaleAPISecret, "")
config := NewDefaultConfig()
config.APIKey = exoscaleAPIKey
config.APISecret = exoscaleAPISecret
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(exoscaleDomain, "", "123d==")
@ -99,7 +115,11 @@ func TestLiveExoscaleCleanUp(t *testing.T) {
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderClient(exoscaleAPIKey, exoscaleAPISecret, "")
config := NewDefaultConfig()
config.APIKey = exoscaleAPIKey
config.APISecret = exoscaleAPISecret
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(exoscaleDomain, "", "123d==")

View file

@ -1,8 +1,10 @@
package fastdns
import (
"errors"
"fmt"
"reflect"
"time"
configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v1"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
@ -10,9 +12,26 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
edgegrid.Config
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("AKAMAI_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("AKAMAI_POLLING_INTERVAL", acme.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("AKAMAI_TTL", 120),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config edgegrid.Config
config *Config
}
// NewDNSProvider uses the supplied environment variables to return a DNSProvider instance:
@ -20,24 +39,27 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("AKAMAI_HOST", "AKAMAI_CLIENT_TOKEN", "AKAMAI_CLIENT_SECRET", "AKAMAI_ACCESS_TOKEN")
if err != nil {
return nil, fmt.Errorf("FastDNS: %v", err)
return nil, fmt.Errorf("fastdns: %v", err)
}
return NewDNSProviderClient(
values["AKAMAI_HOST"],
values["AKAMAI_CLIENT_TOKEN"],
values["AKAMAI_CLIENT_SECRET"],
values["AKAMAI_ACCESS_TOKEN"],
)
config := NewDefaultConfig()
config.Config = edgegrid.Config{
Host: values["AKAMAI_HOST"],
ClientToken: values["AKAMAI_CLIENT_TOKEN"],
ClientSecret: values["AKAMAI_CLIENT_SECRET"],
AccessToken: values["AKAMAI_ACCESS_TOKEN"],
MaxBody: 131072,
}
// NewDNSProviderClient uses the supplied parameters to return a DNSProvider instance
// configured for FastDNS.
return NewDNSProviderConfig(config)
}
// NewDNSProviderClient uses the supplied parameters
// to return a DNSProvider instance configured for FastDNS.
// Deprecated
func NewDNSProviderClient(host, clientToken, clientSecret, accessToken string) (*DNSProvider, error) {
if clientToken == "" || clientSecret == "" || accessToken == "" || host == "" {
return nil, fmt.Errorf("FastDNS credentials are missing")
}
config := edgegrid.Config{
config := NewDefaultConfig()
config.Config = edgegrid.Config{
Host: host,
ClientToken: clientToken,
ClientSecret: clientSecret,
@ -45,29 +67,40 @@ func NewDNSProviderClient(host, clientToken, clientSecret, accessToken string) (
MaxBody: 131072,
}
return &DNSProvider{
config: config,
}, nil
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for FastDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("fastdns: the configuration of the DNS provider is nil")
}
if config.ClientToken == "" || config.ClientSecret == "" || config.AccessToken == "" || config.Host == "" {
return nil, fmt.Errorf("FastDNS credentials are missing")
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record to fullfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneName, recordName, err := d.findZoneAndRecordName(fqdn, domain)
if err != nil {
return err
return fmt.Errorf("fastdns: %v", err)
}
configdns.Init(d.config)
configdns.Init(d.config.Config)
zone, err := configdns.GetZone(zoneName)
if err != nil {
return err
return fmt.Errorf("fastdns: %v", err)
}
record := configdns.NewTxtRecord()
record.SetField("name", recordName)
record.SetField("ttl", ttl)
record.SetField("ttl", d.config.TTL)
record.SetField("target", value)
record.SetField("active", true)
@ -89,14 +122,14 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
zoneName, recordName, err := d.findZoneAndRecordName(fqdn, domain)
if err != nil {
return err
return fmt.Errorf("fastdns: %v", err)
}
configdns.Init(d.config)
configdns.Init(d.config.Config)
zone, err := configdns.GetZone(zoneName)
if err != nil {
return err
return fmt.Errorf("fastdns: %v", err)
}
existingRecord := d.findExistingRecord(zone, recordName)
@ -104,7 +137,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
if existingRecord != nil {
err := zone.RemoveRecord(existingRecord)
if err != nil {
return err
return fmt.Errorf("fastdns: %v", err)
}
return zone.Save()
}
@ -112,6 +145,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) findZoneAndRecordName(fqdn, domain string) (string, string, error) {
zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {

View file

@ -43,7 +43,13 @@ func TestNewDNSProviderValid(t *testing.T) {
os.Setenv("AKAMAI_CLIENT_SECRET", "")
os.Setenv("AKAMAI_ACCESS_TOKEN", "")
_, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken")
config := NewDefaultConfig()
config.Host = "somehost"
config.ClientToken = "someclienttoken"
config.ClientSecret = "someclientsecret"
config.AccessToken = "someaccesstoken"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
@ -66,7 +72,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("AKAMAI_ACCESS_TOKEN", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "FastDNS: some credentials information are missing: AKAMAI_HOST,AKAMAI_CLIENT_TOKEN,AKAMAI_CLIENT_SECRET,AKAMAI_ACCESS_TOKEN")
assert.EqualError(t, err, "fastdns: some credentials information are missing: AKAMAI_HOST,AKAMAI_CLIENT_TOKEN,AKAMAI_CLIENT_SECRET,AKAMAI_ACCESS_TOKEN")
}
func TestLiveFastdnsPresent(t *testing.T) {
@ -74,7 +80,13 @@ func TestLiveFastdnsPresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderClient(host, clientToken, clientSecret, accessToken)
config := NewDefaultConfig()
config.Host = host
config.ClientToken = clientToken
config.ClientSecret = clientSecret
config.AccessToken = accessToken
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(testDomain, "", "123d==")
@ -86,7 +98,13 @@ func TestLiveFastdnsPresent(t *testing.T) {
}
func TestExtractRootRecordName(t *testing.T) {
provider, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken")
config := NewDefaultConfig()
config.Host = "somehost"
config.ClientToken = "someclienttoken"
config.ClientSecret = "someclientsecret"
config.AccessToken = "someaccesstoken"
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
zone, recordName, err := provider.findZoneAndRecordName("_acme-challenge.bar.com.", "bar.com")
@ -96,7 +114,13 @@ func TestExtractRootRecordName(t *testing.T) {
}
func TestExtractSubRecordName(t *testing.T) {
provider, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken")
config := NewDefaultConfig()
config.Host = "somehost"
config.ClientToken = "someclienttoken"
config.ClientSecret = "someclientsecret"
config.AccessToken = "someaccesstoken"
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
zone, recordName, err := provider.findZoneAndRecordName("_acme-challenge.foo.bar.com.", "foo.bar.com")
@ -112,7 +136,13 @@ func TestLiveFastdnsCleanUp(t *testing.T) {
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderClient(host, clientToken, clientSecret, accessToken)
config := NewDefaultConfig()
config.Host = host
config.ClientToken = clientToken
config.ClientSecret = clientSecret
config.AccessToken = accessToken
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(testDomain, "", "123d==")

View file

@ -0,0 +1,94 @@
package gandi
import (
"encoding/xml"
"fmt"
)
// types for XML-RPC method calls and parameters
type param interface {
param()
}
type paramString struct {
XMLName xml.Name `xml:"param"`
Value string `xml:"value>string"`
}
type paramInt struct {
XMLName xml.Name `xml:"param"`
Value int `xml:"value>int"`
}
type structMember interface {
structMember()
}
type structMemberString struct {
Name string `xml:"name"`
Value string `xml:"value>string"`
}
type structMemberInt struct {
Name string `xml:"name"`
Value int `xml:"value>int"`
}
type paramStruct struct {
XMLName xml.Name `xml:"param"`
StructMembers []structMember `xml:"value>struct>member"`
}
func (p paramString) param() {}
func (p paramInt) param() {}
func (m structMemberString) structMember() {}
func (m structMemberInt) structMember() {}
func (p paramStruct) param() {}
type methodCall struct {
XMLName xml.Name `xml:"methodCall"`
MethodName string `xml:"methodName"`
Params []param `xml:"params"`
}
// types for XML-RPC responses
type response interface {
faultCode() int
faultString() string
}
type responseFault struct {
FaultCode int `xml:"fault>value>struct>member>value>int"`
FaultString string `xml:"fault>value>struct>member>value>string"`
}
func (r responseFault) faultCode() int { return r.FaultCode }
func (r responseFault) faultString() string { return r.FaultString }
type responseStruct struct {
responseFault
StructMembers []struct {
Name string `xml:"name"`
ValueInt int `xml:"value>int"`
} `xml:"params>param>value>struct>member"`
}
type responseInt struct {
responseFault
Value int `xml:"params>param>value>int"`
}
type responseBool struct {
responseFault
Value bool `xml:"params>param>value>boolean"`
}
// POSTing/Marshalling/Unmarshalling
type rpcError struct {
faultCode int
faultString string
}
func (e rpcError) Error() string {
return fmt.Sprintf("Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString)
}

View file

@ -5,6 +5,7 @@ package gandi
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
@ -20,15 +21,38 @@ import (
// Gandi API reference: http://doc.rpc.gandi.net/index.html
// Gandi API domain examples: http://doc.rpc.gandi.net/domain/faq.html
var (
// endpoint is the Gandi XML-RPC endpoint used by Present and
// CleanUp. It is overridden during tests.
endpoint = "https://rpc.gandi.net/xmlrpc/"
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
// during tests.
findZoneByFqdn = acme.FindZoneByFqdn
const (
// defaultBaseURL Gandi XML-RPC endpoint used by Present and CleanUp
defaultBaseURL = "https://rpc.gandi.net/xmlrpc/"
minTTL = 300
)
// findZoneByFqdn determines the DNS zone of an fqdn.
// It is overridden during tests.
var findZoneByFqdn = acme.FindZoneByFqdn
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GANDI_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GANDI_PROPAGATION_TIMEOUT", 40*time.Minute),
PollingInterval: env.GetOrDefaultSecond("GANDI_POLLING_INTERVAL", 60*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GANDI_HTTP_TIMEOUT", 60*time.Second),
},
}
}
// inProgressInfo contains information about an in-progress challenge
type inProgressInfo struct {
zoneID int // zoneID of gandi zone to restore in CleanUp
@ -40,11 +64,10 @@ type inProgressInfo struct {
// acme.ChallengeProviderTimeout interface that uses Gandi's XML-RPC
// API to manage TXT records for a domain.
type DNSProvider struct {
apiKey string
inProgressFQDNs map[string]inProgressInfo
inProgressAuthZones map[string]struct{}
inProgressMu sync.Mutex
client *http.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for Gandi.
@ -52,23 +75,43 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("GANDI_API_KEY")
if err != nil {
return nil, fmt.Errorf("GandiDNS: %v", err)
return nil, fmt.Errorf("gandi: %v", err)
}
return NewDNSProviderCredentials(values["GANDI_API_KEY"])
config := NewDefaultConfig()
config.APIKey = values["GANDI_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Gandi.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Gandi.
// Deprecated
func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
if apiKey == "" {
return nil, fmt.Errorf("no Gandi API Key given")
config := NewDefaultConfig()
config.APIKey = apiKey
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Gandi.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("gandi: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, fmt.Errorf("gandi: no API Key given")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
return &DNSProvider{
apiKey: apiKey,
config: config,
inProgressFQDNs: make(map[string]inProgressInfo),
inProgressAuthZones: make(map[string]struct{}),
client: &http.Client{Timeout: 60 * time.Second},
}, nil
}
@ -76,27 +119,27 @@ func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
// does this by creating and activating a new temporary Gandi DNS
// zone. This new zone contains the TXT record.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
if ttl < 300 {
ttl = 300 // 300 is gandi minimum value for ttl
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
if d.config.TTL < minTTL {
d.config.TTL = minTTL // 300 is gandi minimum value for ttl
}
// find authZone and Gandi zone_id for fqdn
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
return fmt.Errorf("gandi: findZoneByFqdn failure: %v", err)
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return err
return fmt.Errorf("gandi: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf(
"Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
return fmt.Errorf("gandi: unexpected authZone %s for fqdn %s", authZone, fqdn)
}
name := fqdn[:len(fqdn)-len("."+authZone)]
@ -106,16 +149,12 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
defer d.inProgressMu.Unlock()
if _, ok := d.inProgressAuthZones[authZone]; ok {
return fmt.Errorf(
"Gandi DNS: challenge already in progress for authZone %s",
authZone)
return fmt.Errorf("gandi: challenge already in progress for authZone %s", authZone)
}
// perform API actions to create and activate new gandi zone
// containing the required TXT record
newZoneName := fmt.Sprintf(
"%s [ACME Challenge %s]",
acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
newZoneName := fmt.Sprintf("%s [ACME Challenge %s]", acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
newZoneID, err := d.cloneZone(zoneID, newZoneName)
if err != nil {
@ -124,22 +163,22 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
newZoneVersion, err := d.newZoneVersion(newZoneID)
if err != nil {
return err
return fmt.Errorf("gandi: %v", err)
}
err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, ttl)
err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, d.config.TTL)
if err != nil {
return err
return fmt.Errorf("gandi: %v", err)
}
err = d.setZoneVersion(newZoneID, newZoneVersion)
if err != nil {
return err
return fmt.Errorf("gandi: %v", err)
}
err = d.setZone(authZone, newZoneID)
if err != nil {
return err
return fmt.Errorf("gandi: %v", err)
}
// save data necessary for CleanUp
@ -149,6 +188,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
authZone: authZone,
}
d.inProgressAuthZones[authZone] = struct{}{}
return nil
}
@ -157,6 +197,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// removing the temporary one created by Present.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
// acquire lock and retrieve zoneID, newZoneID and authZone
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()
@ -175,7 +216,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// perform API actions to restore old gandi zone for authZone
err := d.setZone(authZone, zoneID)
if err != nil {
return err
return fmt.Errorf("gandi: %v", err)
}
return d.deleteZone(newZoneID)
@ -185,109 +226,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// are used by the acme package as timeout and check interval values
// when checking for DNS record propagation with Gandi.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 40 * time.Minute, 60 * time.Second
}
// types for XML-RPC method calls and parameters
type param interface {
param()
}
type paramString struct {
XMLName xml.Name `xml:"param"`
Value string `xml:"value>string"`
}
type paramInt struct {
XMLName xml.Name `xml:"param"`
Value int `xml:"value>int"`
}
type structMember interface {
structMember()
}
type structMemberString struct {
Name string `xml:"name"`
Value string `xml:"value>string"`
}
type structMemberInt struct {
Name string `xml:"name"`
Value int `xml:"value>int"`
}
type paramStruct struct {
XMLName xml.Name `xml:"param"`
StructMembers []structMember `xml:"value>struct>member"`
}
func (p paramString) param() {}
func (p paramInt) param() {}
func (m structMemberString) structMember() {}
func (m structMemberInt) structMember() {}
func (p paramStruct) param() {}
type methodCall struct {
XMLName xml.Name `xml:"methodCall"`
MethodName string `xml:"methodName"`
Params []param `xml:"params"`
}
// types for XML-RPC responses
type response interface {
faultCode() int
faultString() string
}
type responseFault struct {
FaultCode int `xml:"fault>value>struct>member>value>int"`
FaultString string `xml:"fault>value>struct>member>value>string"`
}
func (r responseFault) faultCode() int { return r.FaultCode }
func (r responseFault) faultString() string { return r.FaultString }
type responseStruct struct {
responseFault
StructMembers []struct {
Name string `xml:"name"`
ValueInt int `xml:"value>int"`
} `xml:"params>param>value>struct>member"`
}
type responseInt struct {
responseFault
Value int `xml:"params>param>value>int"`
}
type responseBool struct {
responseFault
Value bool `xml:"params>param>value>boolean"`
}
// POSTing/Marshalling/Unmarshalling
type rpcError struct {
faultCode int
faultString string
}
func (e rpcError) Error() string {
return fmt.Sprintf(
"Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString)
}
func (d *DNSProvider) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
resp, err := d.client.Post(url, bodyType, body)
if err != nil {
return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
}
return b, nil
return d.config.PropagationTimeout, d.config.PollingInterval
}
// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by
@ -298,12 +237,12 @@ func (d *DNSProvider) rpcCall(call *methodCall, resp response) error {
// marshal
b, err := xml.MarshalIndent(call, "", " ")
if err != nil {
return fmt.Errorf("Gandi DNS: Marshal Error: %v", err)
return fmt.Errorf("marshal error: %v", err)
}
// post
b = append([]byte(`<?xml version="1.0"?>`+"\n"), b...)
respBody, err := d.httpPost(endpoint, "text/xml", bytes.NewReader(b))
respBody, err := d.httpPost(d.config.BaseURL, "text/xml", bytes.NewReader(b))
if err != nil {
return err
}
@ -311,7 +250,7 @@ func (d *DNSProvider) rpcCall(call *methodCall, resp response) error {
// unmarshal
err = xml.Unmarshal(respBody, resp)
if err != nil {
return fmt.Errorf("Gandi DNS: Unmarshal Error: %v", err)
return fmt.Errorf("unmarshal error: %v", err)
}
if resp.faultCode() != 0 {
return rpcError{
@ -327,7 +266,7 @@ func (d *DNSProvider) getZoneID(domain string) (int, error) {
err := d.rpcCall(&methodCall{
MethodName: "domain.info",
Params: []param{
paramString{Value: d.apiKey},
paramString{Value: d.config.APIKey},
paramString{Value: domain},
},
}, resp)
@ -343,8 +282,7 @@ func (d *DNSProvider) getZoneID(domain string) (int, error) {
}
if zoneID == 0 {
return 0, fmt.Errorf(
"Gandi DNS: Could not determine zone_id for %s", domain)
return 0, fmt.Errorf("could not determine zone_id for %s", domain)
}
return zoneID, nil
}
@ -354,7 +292,7 @@ func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) {
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.clone",
Params: []param{
paramString{Value: d.apiKey},
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
paramInt{Value: 0},
paramStruct{
@ -378,7 +316,7 @@ func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) {
}
if newZoneID == 0 {
return 0, fmt.Errorf("Gandi DNS: Could not determine cloned zone_id")
return 0, fmt.Errorf("could not determine cloned zone_id")
}
return newZoneID, nil
}
@ -388,7 +326,7 @@ func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) {
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.version.new",
Params: []param{
paramString{Value: d.apiKey},
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
},
}, resp)
@ -397,7 +335,7 @@ func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) {
}
if resp.Value == 0 {
return 0, fmt.Errorf("Gandi DNS: Could not create new zone version")
return 0, fmt.Errorf("could not create new zone version")
}
return resp.Value, nil
}
@ -407,7 +345,7 @@ func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value s
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.record.add",
Params: []param{
paramString{Value: d.apiKey},
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
paramInt{Value: version},
paramStruct{
@ -436,7 +374,7 @@ func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.version.set",
Params: []param{
paramString{Value: d.apiKey},
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
paramInt{Value: version},
},
@ -446,7 +384,7 @@ func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
}
if !resp.Value {
return fmt.Errorf("Gandi DNS: could not set zone version")
return fmt.Errorf("could not set zone version")
}
return nil
}
@ -456,7 +394,7 @@ func (d *DNSProvider) setZone(domain string, zoneID int) error {
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.set",
Params: []param{
paramString{Value: d.apiKey},
paramString{Value: d.config.APIKey},
paramString{Value: domain},
paramInt{Value: zoneID},
},
@ -473,8 +411,7 @@ func (d *DNSProvider) setZone(domain string, zoneID int) error {
}
if respZoneID != zoneID {
return fmt.Errorf(
"Gandi DNS: Could not set new zone_id for %s", domain)
return fmt.Errorf("could not set new zone_id for %s", domain)
}
return nil
}
@ -484,7 +421,7 @@ func (d *DNSProvider) deleteZone(zoneID int) error {
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.delete",
Params: []param{
paramString{Value: d.apiKey},
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
},
}, resp)
@ -493,7 +430,22 @@ func (d *DNSProvider) deleteZone(zoneID int) error {
}
if !resp.Value {
return fmt.Errorf("Gandi DNS: could not delete zone_id")
return fmt.Errorf("could not delete zone_id")
}
return nil
}
func (d *DNSProvider) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
resp, err := d.config.HTTPClient.Post(url, bodyType, body)
if err != nil {
return nil, fmt.Errorf("HTTP Post Error: %v", err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("HTTP Post Error: %v", err)
}
return b, nil
}

View file

@ -15,12 +15,8 @@ import (
// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
// Server, whose responses are predetermined for particular requests.
func TestDNSProvider(t *testing.T) {
fakeAPIKey := "123412341234123412341234"
fakeKeyAuth := "XXXX"
provider, err := NewDNSProviderCredentials(fakeAPIKey)
require.NoError(t, err)
regexpDate, err := regexp.Compile(`\[ACME Challenge [^\]:]*:[^\]]*\]`)
require.NoError(t, err)
@ -45,13 +41,19 @@ func TestDNSProvider(t *testing.T) {
return "example.com.", nil
}
// override gandi endpoint and findZoneByFqdn function
savedEndpoint, savedFindZoneByFqdn := endpoint, findZoneByFqdn
defer func() {
endpoint, findZoneByFqdn = savedEndpoint, savedFindZoneByFqdn
}()
config := NewDefaultConfig()
config.BaseURL = fakeServer.URL + "/"
config.APIKey = "123412341234123412341234"
endpoint, findZoneByFqdn = fakeServer.URL+"/", fakeFindZoneByFqdn
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
// override findZoneByFqdn function
savedFindZoneByFqdn := findZoneByFqdn
defer func() {
findZoneByFqdn = savedFindZoneByFqdn
}()
findZoneByFqdn = fakeFindZoneByFqdn
// run Present
err = provider.Present("abc.def.example.com", "", fakeKeyAuth)

View file

@ -0,0 +1,18 @@
package gandiv5
// types for JSON method calls and parameters
type addFieldRequest struct {
RRSetTTL int `json:"rrset_ttl"`
RRSetValues []string `json:"rrset_values"`
}
type deleteFieldRequest struct {
Delete bool `json:"delete"`
}
// types for JSON responses
type responseStruct struct {
Message string `json:"message"`
}

View file

@ -5,6 +5,7 @@ package gandiv5
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
@ -18,30 +19,51 @@ import (
// Gandi API reference: http://doc.livedns.gandi.net/
var (
// endpoint is the Gandi API endpoint used by Present and
// CleanUp. It is overridden during tests.
endpoint = "https://dns.api.gandi.net/api/v5"
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
// during tests.
findZoneByFqdn = acme.FindZoneByFqdn
const (
// defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp.
defaultBaseURL = "https://dns.api.gandi.net/api/v5"
minTTL = 300
)
// findZoneByFqdn determines the DNS zone of an fqdn.
// It is overridden during tests.
var findZoneByFqdn = acme.FindZoneByFqdn
// inProgressInfo contains information about an in-progress challenge
type inProgressInfo struct {
fieldName string
authZone string
}
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GANDIV5_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GANDIV5_PROPAGATION_TIMEOUT", 20*time.Minute),
PollingInterval: env.GetOrDefaultSecond("GANDIV5_POLLING_INTERVAL", 20*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GANDIV5_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses Gandi's LiveDNS
// API to manage TXT records for a domain.
type DNSProvider struct {
apiKey string
config *Config
inProgressFQDNs map[string]inProgressInfo
inProgressMu sync.Mutex
client *http.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Gandi.
@ -49,43 +71,63 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("GANDIV5_API_KEY")
if err != nil {
return nil, fmt.Errorf("GandiDNS: %v", err)
return nil, fmt.Errorf("gandi: %v", err)
}
return NewDNSProviderCredentials(values["GANDIV5_API_KEY"])
config := NewDefaultConfig()
config.APIKey = values["GANDIV5_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Gandi.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Gandi.
// Deprecated
func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
if apiKey == "" {
return nil, fmt.Errorf("Gandi DNS: No Gandi API Key given")
config := NewDefaultConfig()
config.APIKey = apiKey
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Gandi.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("gandiv5: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, fmt.Errorf("gandiv5: no API Key given")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
return &DNSProvider{
apiKey: apiKey,
config: config,
inProgressFQDNs: make(map[string]inProgressInfo),
client: &http.Client{Timeout: 10 * time.Second},
}, nil
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
if ttl < 300 {
ttl = 300 // 300 is gandi minimum value for ttl
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
if d.config.TTL < minTTL {
d.config.TTL = minTTL // 300 is gandi minimum value for ttl
}
// find authZone
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
return fmt.Errorf("gandiv5: findZoneByFqdn failure: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf(
"Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
return fmt.Errorf("gandiv5: unexpected authZone %s for fqdn %s", authZone, fqdn)
}
name := fqdn[:len(fqdn)-len("."+authZone)]
@ -95,7 +137,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
defer d.inProgressMu.Unlock()
// add TXT record into authZone
err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, ttl)
err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, d.config.TTL)
if err != nil {
return err
}
@ -125,37 +167,47 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
delete(d.inProgressFQDNs, fqdn)
// delete TXT record from authZone
return d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName)
err := d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName)
if err != nil {
return fmt.Errorf("gandiv5: %v", err)
}
return nil
}
// Timeout returns the values (20*time.Minute, 20*time.Second) which
// are used by the acme package as timeout and check interval values
// when checking for DNS record propagation with Gandi.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 20 * time.Minute, 20 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// types for JSON method calls and parameters
// functions to perform API actions
type addFieldRequest struct {
RRSetTTL int `json:"rrset_ttl"`
RRSetValues []string `json:"rrset_values"`
func (d *DNSProvider) addTXTRecord(domain string, name string, value string, ttl int) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
response, err := d.sendRequest(http.MethodPut, target, addFieldRequest{
RRSetTTL: ttl,
RRSetValues: []string{value},
})
if response != nil {
log.Infof("gandiv5: %s", response.Message)
}
return err
}
type deleteFieldRequest struct {
Delete bool `json:"delete"`
func (d *DNSProvider) deleteTXTRecord(domain string, name string) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
response, err := d.sendRequest(http.MethodDelete, target, deleteFieldRequest{
Delete: true,
})
if response != nil && response.Message == "" {
log.Infof("gandiv5: Zone record deleted")
}
// types for JSON responses
type responseStruct struct {
Message string `json:"message"`
return err
}
// POSTing/Marshalling/Unmarshalling
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
url := fmt.Sprintf("%s/%s", endpoint, resource)
url := fmt.Sprintf("%s/%s", d.config.BaseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
@ -168,19 +220,20 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf
}
req.Header.Set("Content-Type", "application/json")
if len(d.apiKey) > 0 {
req.Header.Set("X-Api-Key", d.apiKey)
if len(d.config.APIKey) > 0 {
req.Header.Set("X-Api-Key", d.config.APIKey)
}
resp, err := d.client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("Gandi DNS: request failed with HTTP status code %d", resp.StatusCode)
return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode)
}
var response responseStruct
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil && method != http.MethodDelete {
@ -189,28 +242,3 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf
return &response, nil
}
// functions to perform API actions
func (d *DNSProvider) addTXTRecord(domain string, name string, value string, ttl int) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
response, err := d.sendRequest(http.MethodPut, target, addFieldRequest{
RRSetTTL: ttl,
RRSetValues: []string{value},
})
if response != nil {
log.Infof("Gandi DNS: %s", response.Message)
}
return err
}
func (d *DNSProvider) deleteTXTRecord(domain string, name string) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
response, err := d.sendRequest(http.MethodDelete, target, deleteFieldRequest{
Delete: true,
})
if response != nil && response.Message == "" {
log.Infof("Gandi DNS: Zone record deleted")
}
return err
}

View file

@ -15,12 +15,8 @@ import (
// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
// Server, whose responses are predetermined for particular requests.
func TestDNSProvider(t *testing.T) {
fakeAPIKey := "123412341234123412341234"
fakeKeyAuth := "XXXX"
provider, err := NewDNSProviderCredentials(fakeAPIKey)
require.NoError(t, err)
regexpToken, err := regexp.Compile(`"rrset_values":\[".+"\]`)
require.NoError(t, err)
@ -46,13 +42,19 @@ func TestDNSProvider(t *testing.T) {
return "example.com.", nil
}
// override gandi endpoint and findZoneByFqdn function
savedEndpoint, savedFindZoneByFqdn := endpoint, findZoneByFqdn
defer func() {
endpoint, findZoneByFqdn = savedEndpoint, savedFindZoneByFqdn
}()
config := NewDefaultConfig()
config.APIKey = "123412341234123412341234"
config.BaseURL = fakeServer.URL
endpoint, findZoneByFqdn = fakeServer.URL, fakeFindZoneByFqdn
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
// override findZoneByFqdn function
savedFindZoneByFqdn := findZoneByFqdn
defer func() {
findZoneByFqdn = savedFindZoneByFqdn
}()
findZoneByFqdn = fakeFindZoneByFqdn
// run Present
err = provider.Present("abc.def.example.com", "", fakeKeyAuth)

View file

@ -4,29 +4,47 @@ package gcloud
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/dns/v1"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Project string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GCE_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("GCE_PROPAGATION_TIMEOUT", 180*time.Second),
PollingInterval: env.GetOrDefaultSecond("GCE_POLLING_INTERVAL", 5*time.Second),
}
}
// DNSProvider is an implementation of the DNSProvider interface.
type DNSProvider struct {
project string
config *Config
client *dns.Service
}
// NewDNSProvider returns a DNSProvider instance configured for Google Cloud
// DNS. Project name must be passed in the environment variable: GCE_PROJECT.
// A Service Account file can be passed in the environment variable:
// GCE_SERVICE_ACCOUNT_FILE
// NewDNSProvider returns a DNSProvider instance configured for Google Cloud DNS.
// Project name must be passed in the environment variable: GCE_PROJECT.
// A Service Account file can be passed in the environment variable: GCE_SERVICE_ACCOUNT_FILE
func NewDNSProvider() (*DNSProvider, error) {
if saFile, ok := os.LookupEnv("GCE_SERVICE_ACCOUNT_FILE"); ok {
return NewDNSProviderServiceAccount(saFile)
@ -36,37 +54,35 @@ func NewDNSProvider() (*DNSProvider, error) {
return NewDNSProviderCredentials(project)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Google Cloud DNS.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderCredentials(project string) (*DNSProvider, error) {
if project == "" {
return nil, fmt.Errorf("Google Cloud project name missing")
return nil, fmt.Errorf("googlecloud: project name missing")
}
client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
if err != nil {
return nil, fmt.Errorf("unable to get Google Cloud client: %v", err)
}
svc, err := dns.New(client)
if err != nil {
return nil, fmt.Errorf("unable to create Google Cloud DNS service: %v", err)
}
return &DNSProvider{
project: project,
client: svc,
}, nil
return nil, fmt.Errorf("googlecloud: unable to get Google Cloud client: %v", err)
}
// NewDNSProviderServiceAccount uses the supplied service account JSON file to
// return a DNSProvider instance configured for Google Cloud DNS.
config := NewDefaultConfig()
config.Project = project
config.HTTPClient = client
return NewDNSProviderConfig(config)
}
// NewDNSProviderServiceAccount uses the supplied service account JSON file
// to return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderServiceAccount(saFile string) (*DNSProvider, error) {
if saFile == "" {
return nil, fmt.Errorf("Google Cloud Service Account file missing")
return nil, fmt.Errorf("googlecloud: Service Account file missing")
}
dat, err := ioutil.ReadFile(saFile)
if err != nil {
return nil, fmt.Errorf("unable to read Service Account file: %v", err)
return nil, fmt.Errorf("googlecloud: unable to read Service Account file: %v", err)
}
// read project id from service account file
@ -75,39 +91,50 @@ func NewDNSProviderServiceAccount(saFile string) (*DNSProvider, error) {
}
err = json.Unmarshal(dat, &datJSON)
if err != nil || datJSON.ProjectID == "" {
return nil, fmt.Errorf("project ID not found in Google Cloud Service Account file")
return nil, fmt.Errorf("googlecloud: project ID not found in Google Cloud Service Account file")
}
project := datJSON.ProjectID
conf, err := google.JWTConfigFromJSON(dat, dns.NdevClouddnsReadwriteScope)
if err != nil {
return nil, fmt.Errorf("unable to acquire config: %v", err)
return nil, fmt.Errorf("googlecloud: unable to acquire config: %v", err)
}
client := conf.Client(context.Background())
svc, err := dns.New(client)
if err != nil {
return nil, fmt.Errorf("unable to create Google Cloud DNS service: %v", err)
config := NewDefaultConfig()
config.Project = project
config.HTTPClient = client
return NewDNSProviderConfig(config)
}
return &DNSProvider{
project: project,
client: svc,
}, nil
// NewDNSProviderConfig return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("googlecloud: the configuration of the DNS provider is nil")
}
svc, err := dns.New(config.HTTPClient)
if err != nil {
return nil, fmt.Errorf("googlecloud: unable to create Google Cloud DNS service: %v", err)
}
return &DNSProvider{config: config, client: svc}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("googlecloud: %v", err)
}
rec := &dns.ResourceRecordSet{
Name: fqdn,
Rrdatas: []string{value},
Ttl: int64(ttl),
Ttl: int64(d.config.TTL),
Type: "TXT",
}
change := &dns.Change{
@ -117,25 +144,25 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// Look for existing records.
existing, err := d.findTxtRecords(zone, fqdn)
if err != nil {
return err
return fmt.Errorf("googlecloud: %v", err)
}
if len(existing) > 0 {
// Attempt to delete the existing records when adding our new one.
change.Deletions = existing
}
chg, err := d.client.Changes.Create(d.project, zone, change).Do()
chg, err := d.client.Changes.Create(d.config.Project, zone, change).Do()
if err != nil {
return err
return fmt.Errorf("googlecloud: %v", err)
}
// wait for change to be acknowledged
for chg.Status == "pending" {
time.Sleep(time.Second)
chg, err = d.client.Changes.Get(d.project, zone, chg.Id).Do()
chg, err = d.client.Changes.Get(d.config.Project, zone, chg.Id).Do()
if err != nil {
return err
return fmt.Errorf("googlecloud: %v", err)
}
}
@ -148,26 +175,26 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
zone, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("googlecloud: %v", err)
}
records, err := d.findTxtRecords(zone, fqdn)
if err != nil {
return err
return fmt.Errorf("googlecloud: %v", err)
}
if len(records) == 0 {
return nil
}
_, err = d.client.Changes.Create(d.project, zone, &dns.Change{Deletions: records}).Do()
return err
_, err = d.client.Changes.Create(d.config.Project, zone, &dns.Change{Deletions: records}).Do()
return fmt.Errorf("googlecloud: %v", err)
}
// Timeout customizes the timeout values used by the ACME package for checking
// DNS record validity.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 180 * time.Second, 5 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// getHostedZone returns the managed-zone
@ -178,23 +205,22 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) {
}
zones, err := d.client.ManagedZones.
List(d.project).
List(d.config.Project).
DnsName(authZone).
Do()
if err != nil {
return "", fmt.Errorf("GoogleCloud API call failed: %v", err)
return "", fmt.Errorf("API call failed: %v", err)
}
if len(zones.ManagedZones) == 0 {
return "", fmt.Errorf("no matching GoogleCloud domain found for domain %s", authZone)
return "", fmt.Errorf("no matching domain found for domain %s", authZone)
}
return zones.ManagedZones[0].Name, nil
}
func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
recs, err := d.client.ResourceRecordSets.List(d.project, zone).Name(fqdn).Type("TXT").Do()
recs, err := d.client.ResourceRecordSets.List(d.config.Project, zone).Name(fqdn).Type("TXT").Do()
if err != nil {
return nil, err
}

View file

@ -60,7 +60,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("GCE_PROJECT", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "Google Cloud project name missing")
assert.EqualError(t, err, "googlecloud: project name missing")
}
func TestLiveGoogleCloudPresent(t *testing.T) {

View file

@ -0,0 +1,24 @@
package glesys
// types for JSON method calls, parameters, and responses
type addRecordRequest struct {
DomainName string `json:"domainname"`
Host string `json:"host"`
Type string `json:"type"`
Data string `json:"data"`
TTL int `json:"ttl,omitempty"`
}
type deleteRecordRequest struct {
RecordID int `json:"recordid"`
}
type responseStruct struct {
Response struct {
Status struct {
Code int `json:"code"`
} `json:"status"`
Record deleteRecordRequest `json:"record"`
} `json:"response"`
}

View file

@ -5,6 +5,7 @@ package glesys
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
@ -18,64 +19,102 @@ import (
// GleSYS API reference: https://github.com/GleSYS/API/wiki/API-Documentation
// domainAPI is the GleSYS API endpoint used by Present and CleanUp.
const domainAPI = "https://api.glesys.com/domain"
const (
// defaultBaseURL is the GleSYS API endpoint used by Present and CleanUp.
defaultBaseURL = "https://api.glesys.com/domain"
minTTL = 60
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIUser string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GLESYS_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GLESYS_PROPAGATION_TIMEOUT", 20*time.Minute),
PollingInterval: env.GetOrDefaultSecond("GLESYS_POLLING_INTERVAL", 20*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GLESYS_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses GleSYS
// API to manage TXT records for a domain.
type DNSProvider struct {
apiUser string
apiKey string
config *Config
activeRecords map[string]int
inProgressMu sync.Mutex
client *http.Client
}
// NewDNSProvider returns a DNSProvider instance configured for GleSYS.
// Credentials must be passed in the environment variables: GLESYS_API_USER
// and GLESYS_API_KEY.
// Credentials must be passed in the environment variables:
// GLESYS_API_USER and GLESYS_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("GLESYS_API_USER", "GLESYS_API_KEY")
if err != nil {
return nil, fmt.Errorf("GleSYS DNS: %v", err)
return nil, fmt.Errorf("glesys: %v", err)
}
return NewDNSProviderCredentials(values["GLESYS_API_USER"], values["GLESYS_API_KEY"])
config := NewDefaultConfig()
config.APIUser = values["GLESYS_API_USER"]
config.APIKey = values["GLESYS_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for GleSYS.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for GleSYS.
// Deprecated
func NewDNSProviderCredentials(apiUser string, apiKey string) (*DNSProvider, error) {
if apiUser == "" || apiKey == "" {
return nil, fmt.Errorf("GleSYS DNS: Incomplete credentials provided")
config := NewDefaultConfig()
config.APIUser = apiUser
config.APIKey = apiKey
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for GleSYS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("glesys: the configuration of the DNS provider is nil")
}
if config.APIUser == "" || config.APIKey == "" {
return nil, fmt.Errorf("glesys: incomplete credentials provided")
}
return &DNSProvider{
apiUser: apiUser,
apiKey: apiKey,
activeRecords: make(map[string]int),
client: &http.Client{Timeout: 10 * time.Second},
}, nil
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
if ttl < 60 {
ttl = 60 // 60 is GleSYS minimum value for ttl
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
if d.config.TTL < minTTL {
d.config.TTL = minTTL // 60 is GleSYS minimum value for ttl
}
// find authZone
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("GleSYS DNS: findZoneByFqdn failure: %v", err)
return fmt.Errorf("glesys: findZoneByFqdn failure: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf(
"GleSYS DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
return fmt.Errorf("glesys: unexpected authZone %s for fqdn %s", authZone, fqdn)
}
name := fqdn[:len(fqdn)-len("."+authZone)]
@ -85,7 +124,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
defer d.inProgressMu.Unlock()
// add TXT record into authZone
recordID, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, ttl)
recordID, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, d.config.TTL)
if err != nil {
return err
}
@ -118,36 +157,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// are used by the acme package as timeout and check interval values
// when checking for DNS record propagation with GleSYS.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 20 * time.Minute, 20 * time.Second
}
// types for JSON method calls, parameters, and responses
type addRecordRequest struct {
DomainName string `json:"domainname"`
Host string `json:"host"`
Type string `json:"type"`
Data string `json:"data"`
TTL int `json:"ttl,omitempty"`
}
type deleteRecordRequest struct {
RecordID int `json:"recordid"`
}
type responseStruct struct {
Response struct {
Status struct {
Code int `json:"code"`
} `json:"status"`
Record deleteRecordRequest `json:"record"`
} `json:"response"`
return d.config.PropagationTimeout, d.config.PollingInterval
}
// POSTing/Marshalling/Unmarshalling
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
url := fmt.Sprintf("%s/%s", domainAPI, resource)
url := fmt.Sprintf("%s/%s", defaultBaseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
@ -160,16 +176,16 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(d.apiUser, d.apiKey)
req.SetBasicAuth(d.config.APIUser, d.config.APIKey)
resp, err := d.client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("GleSYS DNS: request failed with HTTP status code %d", resp.StatusCode)
return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode)
}
var response responseStruct
@ -190,7 +206,7 @@ func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, valu
})
if response != nil && response.Response.Status.Code == http.StatusOK {
log.Infof("[%s] GleSYS DNS: Successfully created record id %d", fqdn, response.Response.Record.RecordID)
log.Infof("[%s]: Successfully created record id %d", fqdn, response.Response.Record.RecordID)
return response.Response.Record.RecordID, nil
}
return 0, err
@ -201,7 +217,7 @@ func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error {
RecordID: recordid,
})
if response != nil && response.Response.Status.Code == 200 {
log.Infof("[%s] GleSYS DNS: Successfully deleted record id %d", fqdn, recordid)
log.Infof("[%s]: Successfully deleted record id %d", fqdn, recordid)
}
return err
}

View file

@ -4,6 +4,7 @@ package godaddy
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@ -15,46 +16,83 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// GoDaddyAPIURL represents the API endpoint to call.
const apiURL = "https://api.godaddy.com"
const (
// defaultBaseURL represents the API endpoint to call.
defaultBaseURL = "https://api.godaddy.com"
minTTL = 600
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
APISecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GODADDY_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GODADDY_PROPAGATION_TIMEOUT", 120*time.Second),
PollingInterval: env.GetOrDefaultSecond("GODADDY_POLLING_INTERVAL", 2*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GODADDY_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
apiKey string
apiSecret string
client *http.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for godaddy.
// Credentials must be passed in the environment variables: GODADDY_API_KEY
// and GODADDY_API_SECRET.
// Credentials must be passed in the environment variables:
// GODADDY_API_KEY and GODADDY_API_SECRET.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("GODADDY_API_KEY", "GODADDY_API_SECRET")
if err != nil {
return nil, fmt.Errorf("GoDaddy: %v", err)
return nil, fmt.Errorf("godaddy: %v", err)
}
return NewDNSProviderCredentials(values["GODADDY_API_KEY"], values["GODADDY_API_SECRET"])
config := NewDefaultConfig()
config.APIKey = values["GODADDY_API_KEY"]
config.APISecret = values["GODADDY_API_SECRET"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for godaddy.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for godaddy.
// Deprecated
func NewDNSProviderCredentials(apiKey, apiSecret string) (*DNSProvider, error) {
if apiKey == "" || apiSecret == "" {
return nil, fmt.Errorf("GoDaddy credentials missing")
config := NewDefaultConfig()
config.APIKey = apiKey
config.APISecret = apiSecret
return NewDNSProviderConfig(config)
}
return &DNSProvider{
apiKey: apiKey,
apiSecret: apiSecret,
client: &http.Client{Timeout: 30 * time.Second},
}, nil
// NewDNSProviderConfig return a DNSProvider instance configured for godaddy.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("godaddy: the configuration of the DNS provider is nil")
}
if config.APIKey == "" || config.APISecret == "" {
return nil, fmt.Errorf("godaddy: credentials missing")
}
return &DNSProvider{config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 120 * time.Second, 2 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
@ -67,14 +105,14 @@ func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
// Present creates a TXT record to fulfil the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
domainZone, err := d.getZone(fqdn)
if err != nil {
return err
}
if ttl < 600 {
ttl = 600
if d.config.TTL < minTTL {
d.config.TTL = minTTL
}
recordName := d.extractRecordName(fqdn, domainZone)
@ -83,7 +121,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
Type: "TXT",
Name: recordName,
Data: value,
TTL: ttl,
TTL: d.config.TTL,
},
}
@ -141,16 +179,16 @@ func (d *DNSProvider) getZone(fqdn string) (string, error) {
}
func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", apiURL, uri), body)
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", defaultBaseURL, uri), body)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.apiKey, d.apiSecret))
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.config.APIKey, d.config.APISecret))
return d.client.Do(req)
return d.config.HTTPClient.Do(req)
}
// DNSRecord a DNS record

View file

@ -3,6 +3,7 @@ package iij
import (
"fmt"
"strconv"
"strings"
"time"
@ -17,6 +18,18 @@ type Config struct {
AccessKey string
SecretKey string
DoServiceCode string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("IIJ_TTL", 300),
PropagationTimeout: env.GetOrDefaultSecond("IIJ_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("IIJ_POLLING_INTERVAL", 4*time.Second),
}
}
// DNSProvider implements the acme.ChallengeProvider interface
@ -29,19 +42,24 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("IIJ_API_ACCESS_KEY", "IIJ_API_SECRET_KEY", "IIJ_DO_SERVICE_CODE")
if err != nil {
return nil, fmt.Errorf("IIJ: %v", err)
return nil, fmt.Errorf("iij: %v", err)
}
return NewDNSProviderConfig(&Config{
AccessKey: values["IIJ_API_ACCESS_KEY"],
SecretKey: values["IIJ_API_SECRET_KEY"],
DoServiceCode: values["IIJ_DO_SERVICE_CODE"],
})
config := NewDefaultConfig()
config.AccessKey = values["IIJ_API_ACCESS_KEY"]
config.SecretKey = values["IIJ_API_SECRET_KEY"]
config.DoServiceCode = values["IIJ_DO_SERVICE_CODE"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig takes a given config ans returns a custom configured
// DNSProvider instance
// NewDNSProviderConfig takes a given config
// and returns a custom configured DNSProvider instance
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config.SecretKey == "" || config.AccessKey == "" || config.DoServiceCode == "" {
return nil, fmt.Errorf("iij: credentials missing")
}
return &DNSProvider{
api: doapi.NewAPI(config.AccessKey, config.SecretKey),
config: config,
@ -49,24 +67,28 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
return time.Minute * 2, time.Second * 4
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (p *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, value, _ := acme.DNS01Record(domain, keyAuth)
return p.addTxtRecord(domain, value)
err := d.addTxtRecord(domain, value)
return fmt.Errorf("iij: %v", err)
}
// CleanUp removes the TXT record matching the specified parameters
func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
_, value, _ := acme.DNS01Record(domain, keyAuth)
return p.deleteTxtRecord(domain, value)
err := d.deleteTxtRecord(domain, value)
return fmt.Errorf("iij: %v", err)
}
func (p *DNSProvider) addTxtRecord(domain, value string) error {
zones, err := p.listZones()
func (d *DNSProvider) addTxtRecord(domain, value string) error {
zones, err := d.listZones()
if err != nil {
return err
}
@ -77,25 +99,25 @@ func (p *DNSProvider) addTxtRecord(domain, value string) error {
}
request := protocol.RecordAdd{
DoServiceCode: p.config.DoServiceCode,
DoServiceCode: d.config.DoServiceCode,
ZoneName: zone,
Owner: owner,
TTL: "300",
TTL: strconv.Itoa(d.config.TTL),
RecordType: "TXT",
RData: value,
}
response := &protocol.RecordAddResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
if err := doapi.Call(*d.api, request, response); err != nil {
return err
}
return p.commit()
return d.commit()
}
func (p *DNSProvider) deleteTxtRecord(domain, value string) error {
zones, err := p.listZones()
func (d *DNSProvider) deleteTxtRecord(domain, value string) error {
zones, err := d.listZones()
if err != nil {
return err
}
@ -105,45 +127,45 @@ func (p *DNSProvider) deleteTxtRecord(domain, value string) error {
return err
}
id, err := p.findTxtRecord(owner, zone, value)
id, err := d.findTxtRecord(owner, zone, value)
if err != nil {
return err
}
request := protocol.RecordDelete{
DoServiceCode: p.config.DoServiceCode,
DoServiceCode: d.config.DoServiceCode,
ZoneName: zone,
RecordID: id,
}
response := &protocol.RecordDeleteResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
if err := doapi.Call(*d.api, request, response); err != nil {
return err
}
return p.commit()
return d.commit()
}
func (p *DNSProvider) commit() error {
func (d *DNSProvider) commit() error {
request := protocol.Commit{
DoServiceCode: p.config.DoServiceCode,
DoServiceCode: d.config.DoServiceCode,
}
response := &protocol.CommitResponse{}
return doapi.Call(*p.api, request, response)
return doapi.Call(*d.api, request, response)
}
func (p *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) {
func (d *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) {
request := protocol.RecordListGet{
DoServiceCode: p.config.DoServiceCode,
DoServiceCode: d.config.DoServiceCode,
ZoneName: zone,
}
response := &protocol.RecordListGetResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
if err := doapi.Call(*d.api, request, response); err != nil {
return "", err
}
@ -162,14 +184,14 @@ func (p *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) {
return id, nil
}
func (p *DNSProvider) listZones() ([]string, error) {
func (d *DNSProvider) listZones() ([]string, error) {
request := protocol.ZoneListGet{
DoServiceCode: p.config.DoServiceCode,
DoServiceCode: d.config.DoServiceCode,
}
response := &protocol.ZoneListGetResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
if err := doapi.Call(*d.api, request, response); err != nil {
return nil, err
}

View file

@ -95,7 +95,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("IIJ_DO_SERVICE_CODE", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "IIJ: some credentials information are missing: IIJ_API_ACCESS_KEY,IIJ_API_SECRET_KEY,IIJ_DO_SERVICE_CODE")
assert.EqualError(t, err, "iij: some credentials information are missing: IIJ_API_ACCESS_KEY,IIJ_API_SECRET_KEY,IIJ_DO_SERVICE_CODE")
}
func TestNewDNSProvider(t *testing.T) {

View file

@ -3,6 +3,8 @@
package lightsail
import (
"errors"
"fmt"
"math/rand"
"os"
"time"
@ -13,21 +15,15 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lightsail"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const (
maxRetries = 5
)
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *lightsail.Lightsail
dnsZone string
}
// customRetryer implements the client.Retryer interface by composing the
// DefaultRetryer. It controls the logic for retrying recoverable request
// errors (e.g. when rate limits are exceeded).
// customRetryer implements the client.Retryer interface by composing the DefaultRetryer.
// It controls the logic for retrying recoverable request errors (e.g. when rate limits are exceeded).
type customRetryer struct {
client.DefaultRetryer
}
@ -47,13 +43,36 @@ func (c customRetryer) RetryRules(r *request.Request) time.Duration {
return time.Duration(delay) * time.Millisecond
}
// NewDNSProvider returns a DNSProvider instance configured for the AWS
// Lightsail service.
// Config is used to configure the creation of the DNSProvider
type Config struct {
DNSZone string
Region string
PropagationTimeout time.Duration
PollingInterval time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
DNSZone: os.Getenv("DNS_ZONE"),
PropagationTimeout: env.GetOrDefaultSecond("LIGHTSAIL_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("LIGHTSAIL_POLLING_INTERVAL", acme.DefaultPollingInterval),
Region: env.GetOrDefaultString("LIGHTSAIL_REGION", "us-east-1"),
}
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *lightsail.Lightsail
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for the AWS Lightsail service.
//
// AWS Credentials are automatically detected in the following locations
// and prioritized in the following order:
// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
// [AWS_SESSION_TOKEN], [DNS_ZONE]
// [AWS_SESSION_TOKEN], [DNS_ZONE], [LIGHTSAIL_REGION]
// 2. Shared credentials file (defaults to ~/.aws/credentials)
// 3. Amazon EC2 IAM role
//
@ -61,17 +80,26 @@ func (c customRetryer) RetryRules(r *request.Request) time.Duration {
//
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
func NewDNSProvider() (*DNSProvider, error) {
r := customRetryer{}
r.NumMaxRetries = maxRetries
return NewDNSProviderConfig(NewDefaultConfig())
}
config := aws.NewConfig().WithRegion("us-east-1")
sess, err := session.NewSession(request.WithRetryer(config, r))
// NewDNSProviderConfig return a DNSProvider instance configured for AWS Lightsail.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("lightsail: the configuration of the DNS provider is nil")
}
retryer := customRetryer{}
retryer.NumMaxRetries = maxRetries
conf := aws.NewConfig().WithRegion(config.Region)
sess, err := session.NewSession(request.WithRetryer(conf, retryer))
if err != nil {
return nil, err
}
return &DNSProvider{
dnsZone: os.Getenv("DNS_ZONE"),
config: config,
client: lightsail.New(sess),
}, nil
}
@ -79,31 +107,43 @@ func NewDNSProvider() (*DNSProvider, error) {
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
err := d.newTxtRecord(domain, fqdn, value)
return err
err := d.newTxtRecord(domain, fqdn, `"`+value+`"`)
if err != nil {
return fmt.Errorf("lightsail: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
params := &lightsail.DeleteDomainEntryInput{
DomainName: aws.String(d.dnsZone),
DomainName: aws.String(d.config.DNSZone),
DomainEntry: &lightsail.DomainEntry{
Name: aws.String(fqdn),
Type: aws.String("TXT"),
Target: aws.String(value),
Target: aws.String(`"` + value + `"`),
},
}
_, err := d.client.DeleteDomainEntry(params)
return err
if err != nil {
return fmt.Errorf("lightsail: %v", err)
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) newTxtRecord(domain string, fqdn string, value string) error {
params := &lightsail.CreateDomainEntryInput{
DomainName: aws.String(d.dnsZone),
DomainName: aws.String(d.config.DNSZone),
DomainEntry: &lightsail.DomainEntry{
Name: aws.String(fqdn),
Target: aws.String(value),

View file

@ -43,8 +43,10 @@ func makeLightsailProvider(ts *httptest.Server) (*DNSProvider, error) {
return nil, err
}
conf := NewDefaultConfig()
client := lightsail.New(sess)
return &DNSProvider{client: client}, nil
return &DNSProvider{client: client, config: conf}, nil
}
func TestCredentialsFromEnv(t *testing.T) {

View file

@ -19,6 +19,21 @@ const (
dnsUpdateFudgeSecs = 120
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PollingInterval: env.GetOrDefaultSecond("LINODE_POLLING_INTERVAL", 15*time.Second),
TTL: env.GetOrDefaultInt("LINODE_TTL", 60),
}
}
type hostedZoneInfo struct {
domainID int
resourceName string
@ -26,6 +41,7 @@ type hostedZoneInfo struct {
// DNSProvider implements the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *dns.DNS
}
@ -34,27 +50,44 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("LINODE_API_KEY")
if err != nil {
return nil, fmt.Errorf("Linode: %v", err)
return nil, fmt.Errorf("linode: %v", err)
}
return NewDNSProviderCredentials(values["LINODE_API_KEY"])
config := NewDefaultConfig()
config.APIKey = values["LINODE_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Linode.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Linode.
// Deprecated
func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
if len(apiKey) == 0 {
return nil, errors.New("Linode credentials missing")
config := NewDefaultConfig()
config.APIKey = apiKey
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Linode.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("linode: the configuration of the DNS provider is nil")
}
if len(config.APIKey) == 0 {
return nil, errors.New("linode: credentials missing")
}
return &DNSProvider{
client: dns.New(apiKey),
config: config,
client: dns.New(config.APIKey),
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
// Since Linode only updates their zone files every X minutes, we need
// to figure out how many minutes we have to wait until we hit the next
// interval of X. We then wait another couple of minutes, just to be
@ -65,19 +98,19 @@ func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
timeout = (time.Duration(minsRemaining) * time.Minute) +
(dnsMinTTLSecs * time.Second) +
(dnsUpdateFudgeSecs * time.Second)
interval = 15 * time.Second
interval = d.config.PollingInterval
return
}
// Present creates a TXT record using the specified parameters.
func (p *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := p.getHostedZoneInfo(fqdn)
zone, err := d.getHostedZoneInfo(fqdn)
if err != nil {
return err
}
if _, err = p.client.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, 60); err != nil {
if _, err = d.client.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, 60); err != nil {
return err
}
@ -85,15 +118,15 @@ func (p *DNSProvider) Present(domain, token, keyAuth string) error {
}
// CleanUp removes the TXT record matching the specified parameters.
func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := p.getHostedZoneInfo(fqdn)
zone, err := d.getHostedZoneInfo(fqdn)
if err != nil {
return err
}
// Get all TXT records for the specified domain.
resources, err := p.client.GetResourcesByType(zone.domainID, "TXT")
resources, err := d.client.GetResourcesByType(zone.domainID, "TXT")
if err != nil {
return err
}
@ -101,7 +134,7 @@ func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// Remove the specified resource, if it exists.
for _, resource := range resources {
if resource.Name == zone.resourceName && resource.Target == value {
resp, err := p.client.DeleteDomainResource(resource.DomainID, resource.ResourceID)
resp, err := d.client.DeleteDomainResource(resource.DomainID, resource.ResourceID)
if err != nil {
return err
}
@ -115,16 +148,17 @@ func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
func (p *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
// Lookup the zone that handles the specified FQDN.
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return nil, err
}
resourceName := strings.TrimSuffix(fqdn, "."+authZone)
// Query the authority zone.
domain, err := p.client.GetDomain(acme.UnFqdn(authZone))
domain, err := d.client.GetDomain(acme.UnFqdn(authZone))
if err != nil {
return nil, err
}

View file

@ -86,17 +86,22 @@ func TestNewDNSProviderWithoutEnv(t *testing.T) {
os.Setenv("LINODE_API_KEY", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "Linode: some credentials information are missing: LINODE_API_KEY")
assert.EqualError(t, err, "linode: some credentials information are missing: LINODE_API_KEY")
}
func TestNewDNSProviderCredentialsWithKey(t *testing.T) {
_, err := NewDNSProviderCredentials("testing")
config := NewDefaultConfig()
config.APIKey = "testing"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
func TestNewDNSProviderCredentialsWithoutKey(t *testing.T) {
_, err := NewDNSProviderCredentials("")
assert.EqualError(t, err, "Linode credentials missing")
config := NewDefaultConfig()
_, err := NewDNSProviderConfig(config)
assert.EqualError(t, err, "linode: credentials missing")
}
func TestDNSProvider_Present(t *testing.T) {

View file

@ -0,0 +1,44 @@
package namecheap
import "encoding/xml"
// host describes a DNS record returned by the Namecheap DNS gethosts API.
// Namecheap uses the term "host" to refer to all DNS records that include
// a host field (A, AAAA, CNAME, NS, TXT, URL).
type host struct {
Type string `xml:",attr"`
Name string `xml:",attr"`
Address string `xml:",attr"`
MXPref string `xml:",attr"`
TTL string `xml:",attr"`
}
// apierror describes an error record in a namecheap API response.
type apierror struct {
Number int `xml:",attr"`
Description string `xml:",innerxml"`
}
type setHostsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apierror `xml:"Errors>Error"`
Result struct {
IsSuccess string `xml:",attr"`
} `xml:"CommandResponse>DomainDNSSetHostsResult"`
}
type getHostsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apierror `xml:"Errors>Error"`
Hosts []host `xml:"CommandResponse>DomainDNSGetHostsResult>host"`
}
type getTldsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Errors []apierror `xml:"Errors>Error"`
Result []struct {
Name string `xml:",attr"`
} `xml:"CommandResponse>Tlds>Tld"`
}

View file

@ -4,10 +4,12 @@ package namecheap
import (
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -29,84 +31,175 @@ import (
// address as a form or query string value. This code uses a namecheap
// service to query the client's IP address.
var (
debug = false
const (
defaultBaseURL = "https://api.namecheap.com/xml.response"
getIPURL = "https://dynamicdns.park-your-domain.com/getip"
)
// A challenge represents all the data needed to specify a dns-01 challenge
// to lets-encrypt.
type challenge struct {
domain string
key string
keyFqdn string
keyValue string
tld string
sld string
host string
}
// Config is used to configure the creation of the DNSProvider
type Config struct {
Debug bool
BaseURL string
APIUser string
APIKey string
ClientIP string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
BaseURL: defaultBaseURL,
Debug: env.GetOrDefaultBool("NAMECHEAP_DEBUG", false),
TTL: env.GetOrDefaultInt("NAMECHEAP_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("NAMECHEAP_PROPAGATION_TIMEOUT", 60*time.Minute),
PollingInterval: env.GetOrDefaultSecond("NAMECHEAP_POLLING_INTERVAL", 15*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NAMECHEAP_HTTP_TIMEOUT", 60*time.Second),
},
}
}
// DNSProvider is an implementation of the ChallengeProviderTimeout interface
// that uses Namecheap's tool API to manage TXT records for a domain.
type DNSProvider struct {
baseURL string
apiUser string
apiKey string
clientIP string
client *http.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for namecheap.
// Credentials must be passed in the environment variables: NAMECHEAP_API_USER
// and NAMECHEAP_API_KEY.
// Credentials must be passed in the environment variables:
// NAMECHEAP_API_USER and NAMECHEAP_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NAMECHEAP_API_USER", "NAMECHEAP_API_KEY")
if err != nil {
return nil, fmt.Errorf("NameCheap: %v", err)
return nil, fmt.Errorf("namecheap: %v", err)
}
return NewDNSProviderCredentials(values["NAMECHEAP_API_USER"], values["NAMECHEAP_API_KEY"])
config := NewDefaultConfig()
config.APIUser = values["NAMECHEAP_API_USER"]
config.APIKey = values["NAMECHEAP_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for namecheap.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for namecheap.
// Deprecated
func NewDNSProviderCredentials(apiUser, apiKey string) (*DNSProvider, error) {
if apiUser == "" || apiKey == "" {
return nil, fmt.Errorf("Namecheap credentials missing")
config := NewDefaultConfig()
config.APIUser = apiUser
config.APIKey = apiKey
return NewDNSProviderConfig(config)
}
client := &http.Client{Timeout: 60 * time.Second}
// NewDNSProviderConfig return a DNSProvider instance configured for namecheap.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("namecheap: the configuration of the DNS provider is nil")
}
clientIP, err := getClientIP(client)
if config.APIUser == "" || config.APIKey == "" {
return nil, fmt.Errorf("namecheap: credentials missing")
}
if len(config.ClientIP) == 0 {
clientIP, err := getClientIP(config.HTTPClient, config.Debug)
if err != nil {
return nil, err
return nil, fmt.Errorf("namecheap: %v", err)
}
config.ClientIP = clientIP
}
return &DNSProvider{
baseURL: defaultBaseURL,
apiUser: apiUser,
apiKey: apiKey,
clientIP: clientIP,
client: client,
}, nil
return &DNSProvider{config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Namecheap can sometimes take a long time to complete an
// update, so wait up to 60 minutes for the update to propagate.
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Namecheap can sometimes take a long time to complete an update, so wait up to 60 minutes for the update to propagate.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 60 * time.Minute, 15 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// host describes a DNS record returned by the Namecheap DNS gethosts API.
// Namecheap uses the term "host" to refer to all DNS records that include
// a host field (A, AAAA, CNAME, NS, TXT, URL).
type host struct {
Type string `xml:",attr"`
Name string `xml:",attr"`
Address string `xml:",attr"`
MXPref string `xml:",attr"`
TTL string `xml:",attr"`
// Present installs a TXT record for the DNS challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
tlds, err := d.getTLDs()
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
// apierror describes an error record in a namecheap API response.
type apierror struct {
Number int `xml:",attr"`
Description string `xml:",innerxml"`
ch, err := newChallenge(domain, keyAuth, tlds)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
// getClientIP returns the client's public IP address. It uses namecheap's
// IP discovery service to perform the lookup.
func getClientIP(client *http.Client) (addr string, err error) {
hosts, err := d.getHosts(ch)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
d.addChallengeRecord(ch, &hosts)
if d.config.Debug {
for _, h := range hosts {
log.Printf(
"%-5.5s %-30.30s %-6s %-70.70s\n",
h.Type, h.Name, h.TTL, h.Address)
}
}
err = d.setHosts(ch, hosts)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
return nil
}
// CleanUp removes a TXT record used for a previous DNS challenge.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
tlds, err := d.getTLDs()
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
ch, err := newChallenge(domain, keyAuth, tlds)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
hosts, err := d.getHosts(ch)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
if removed := d.removeChallengeRecord(ch, &hosts); !removed {
return nil
}
err = d.setHosts(ch, hosts)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
return nil
}
// getClientIP returns the client's public IP address.
// It uses namecheap's IP discovery service to perform the lookup.
func getClientIP(client *http.Client, debug bool) (addr string, err error) {
resp, err := client.Get(getIPURL)
if err != nil {
return "", err
@ -124,18 +217,6 @@ func getClientIP(client *http.Client) (addr string, err error) {
return string(clientIP), nil
}
// A challenge represents all the data needed to specify a dns-01 challenge
// to lets-encrypt.
type challenge struct {
domain string
key string
keyFqdn string
keyValue string
tld string
sld string
host string
}
// newChallenge builds a challenge record from a domain name, a challenge
// authentication key, and a map of available TLDs.
func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, error) {
@ -178,11 +259,11 @@ func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, e
// setGlobalParams adds the namecheap global parameters to the provided url
// Values record.
func (d *DNSProvider) setGlobalParams(v *url.Values, cmd string) {
v.Set("ApiUser", d.apiUser)
v.Set("ApiKey", d.apiKey)
v.Set("UserName", d.apiUser)
v.Set("ClientIp", d.clientIP)
v.Set("ApiUser", d.config.APIUser)
v.Set("ApiKey", d.config.APIKey)
v.Set("UserName", d.config.APIUser)
v.Set("Command", cmd)
v.Set("ClientIp", d.config.ClientIP)
}
// getTLDs requests the list of available TLDs from namecheap.
@ -190,10 +271,13 @@ func (d *DNSProvider) getTLDs() (tlds map[string]string, err error) {
values := make(url.Values)
d.setGlobalParams(&values, "namecheap.domains.getTldList")
reqURL, _ := url.Parse(d.baseURL)
reqURL, err := url.Parse(d.config.BaseURL)
if err != nil {
return nil, err
}
reqURL.RawQuery = values.Encode()
resp, err := d.client.Get(reqURL.String())
resp, err := d.config.HTTPClient.Get(reqURL.String())
if err != nil {
return nil, err
}
@ -208,21 +292,12 @@ func (d *DNSProvider) getTLDs() (tlds map[string]string, err error) {
return nil, err
}
type GetTldsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Errors []apierror `xml:"Errors>Error"`
Result []struct {
Name string `xml:",attr"`
} `xml:"CommandResponse>Tlds>Tld"`
}
var gtr GetTldsResponse
var gtr getTldsResponse
if err := xml.Unmarshal(body, &gtr); err != nil {
return nil, err
}
if len(gtr.Errors) > 0 {
return nil, fmt.Errorf("Namecheap error: %s [%d]",
gtr.Errors[0].Description, gtr.Errors[0].Number)
return nil, fmt.Errorf("%s [%d]", gtr.Errors[0].Description, gtr.Errors[0].Number)
}
tlds = make(map[string]string)
@ -236,13 +311,17 @@ func (d *DNSProvider) getTLDs() (tlds map[string]string, err error) {
func (d *DNSProvider) getHosts(ch *challenge) (hosts []host, err error) {
values := make(url.Values)
d.setGlobalParams(&values, "namecheap.domains.dns.getHosts")
values.Set("SLD", ch.sld)
values.Set("TLD", ch.tld)
reqURL, _ := url.Parse(d.baseURL)
reqURL, err := url.Parse(d.config.BaseURL)
if err != nil {
return nil, err
}
reqURL.RawQuery = values.Encode()
resp, err := d.client.Get(reqURL.String())
resp, err := d.config.HTTPClient.Get(reqURL.String())
if err != nil {
return nil, err
}
@ -257,20 +336,12 @@ func (d *DNSProvider) getHosts(ch *challenge) (hosts []host, err error) {
return nil, err
}
type GetHostsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apierror `xml:"Errors>Error"`
Hosts []host `xml:"CommandResponse>DomainDNSGetHostsResult>host"`
}
var ghr GetHostsResponse
var ghr getHostsResponse
if err = xml.Unmarshal(body, &ghr); err != nil {
return nil, err
}
if len(ghr.Errors) > 0 {
return nil, fmt.Errorf("Namecheap error: %s [%d]",
ghr.Errors[0].Description, ghr.Errors[0].Number)
return nil, fmt.Errorf("%s [%d]", ghr.Errors[0].Description, ghr.Errors[0].Number)
}
return ghr.Hosts, nil
@ -280,6 +351,7 @@ func (d *DNSProvider) getHosts(ch *challenge) (hosts []host, err error) {
func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error {
values := make(url.Values)
d.setGlobalParams(&values, "namecheap.domains.dns.setHosts")
values.Set("SLD", ch.sld)
values.Set("TLD", ch.tld)
@ -292,7 +364,7 @@ func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error {
values.Add("TTL"+ind, h.TTL)
}
resp, err := d.client.PostForm(d.baseURL, values)
resp, err := d.config.HTTPClient.PostForm(d.config.BaseURL, values)
if err != nil {
return err
}
@ -307,25 +379,15 @@ func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error {
return err
}
type SetHostsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apierror `xml:"Errors>Error"`
Result struct {
IsSuccess string `xml:",attr"`
} `xml:"CommandResponse>DomainDNSSetHostsResult"`
}
var shr SetHostsResponse
var shr setHostsResponse
if err := xml.Unmarshal(body, &shr); err != nil {
return err
}
if len(shr.Errors) > 0 {
return fmt.Errorf("Namecheap error: %s [%d]",
shr.Errors[0].Description, shr.Errors[0].Number)
return fmt.Errorf("%s [%d]", shr.Errors[0].Description, shr.Errors[0].Number)
}
if shr.Result.IsSuccess != "true" {
return fmt.Errorf("Namecheap setHosts failed")
return fmt.Errorf("setHosts failed")
}
return nil
@ -339,7 +401,7 @@ func (d *DNSProvider) addChallengeRecord(ch *challenge, hosts *[]host) {
Type: "TXT",
Address: ch.keyValue,
MXPref: "10",
TTL: "120",
TTL: strconv.Itoa(d.config.TTL),
}
// If there's already a TXT record with the same name, replace it.
@ -367,57 +429,3 @@ func (d *DNSProvider) removeChallengeRecord(ch *challenge, hosts *[]host) bool {
return false
}
// Present installs a TXT record for the DNS challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
tlds, err := d.getTLDs()
if err != nil {
return err
}
ch, err := newChallenge(domain, keyAuth, tlds)
if err != nil {
return err
}
hosts, err := d.getHosts(ch)
if err != nil {
return err
}
d.addChallengeRecord(ch, &hosts)
if debug {
for _, h := range hosts {
log.Printf(
"%-5.5s %-30.30s %-6s %-70.70s\n",
h.Type, h.Name, h.TTL, h.Address)
}
}
return d.setHosts(ch, hosts)
}
// CleanUp removes a TXT record used for a previous DNS challenge.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
tlds, err := d.getTLDs()
if err != nil {
return err
}
ch, err := newChallenge(domain, keyAuth, tlds)
if err != nil {
return err
}
hosts, err := d.getHosts(ch)
if err != nil {
return err
}
if removed := d.removeChallengeRecord(ch, &hosts); !removed {
return nil
}
return d.setHosts(ch, hosts)
}

View file

@ -7,6 +7,9 @@ import (
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
@ -25,6 +28,174 @@ var (
}
)
func TestGetHosts(t *testing.T) {
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(&test, t, w, r)
}))
defer mock.Close()
config := NewDefaultConfig()
config.BaseURL = mock.URL
config.APIUser = fakeUser
config.APIKey = fakeKey
config.ClientIP = fakeClientIP
config.HTTPClient = &http.Client{Timeout: 60 * time.Second}
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
ch, _ := newChallenge(test.domain, "", tlds)
hosts, err := provider.getHosts(ch)
if test.errString != "" {
assert.EqualError(t, err, test.errString)
} else {
assert.NoError(t, err)
}
next1:
for _, h := range hosts {
for _, th := range test.hosts {
if h == th {
continue next1
}
}
t.Errorf("getHosts case %s unexpected record [%s:%s:%s]", test.name, h.Type, h.Name, h.Address)
}
next2:
for _, th := range test.hosts {
for _, h := range hosts {
if h == th {
continue next2
}
}
t.Errorf("getHosts case %s missing record [%s:%s:%s]", test.name, th.Type, th.Name, th.Address)
}
})
}
}
func TestSetHosts(t *testing.T) {
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(&test, t, w, r)
}))
defer mock.Close()
prov := mockDNSProvider(mock.URL)
ch, _ := newChallenge(test.domain, "", tlds)
hosts, err := prov.getHosts(ch)
if test.errString != "" {
assert.EqualError(t, err, test.errString)
} else {
assert.NoError(t, err)
}
if err != nil {
return
}
err = prov.setHosts(ch, hosts)
assert.NoError(t, err)
})
}
}
func TestPresent(t *testing.T) {
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(&test, t, w, r)
}))
defer mock.Close()
prov := mockDNSProvider(mock.URL)
err := prov.Present(test.domain, "", "dummyKey")
if test.errString != "" {
assert.EqualError(t, err, "namecheap: "+test.errString)
} else {
assert.NoError(t, err)
}
})
}
}
func TestCleanUp(t *testing.T) {
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(&test, t, w, r)
}))
defer mock.Close()
prov := mockDNSProvider(mock.URL)
err := prov.CleanUp(test.domain, "", "dummyKey")
if test.errString != "" {
assert.EqualError(t, err, "namecheap: "+test.errString)
} else {
assert.NoError(t, err)
}
})
}
}
func TestNamecheapDomainSplit(t *testing.T) {
tests := []struct {
domain string
valid bool
tld string
sld string
host string
}{
{domain: "a.b.c.test.co.uk", valid: true, tld: "co.uk", sld: "test", host: "a.b.c"},
{domain: "test.co.uk", valid: true, tld: "co.uk", sld: "test"},
{domain: "test.com", valid: true, tld: "com", sld: "test"},
{domain: "test.co.com", valid: true, tld: "co.com", sld: "test"},
{domain: "www.test.com.au", valid: true, tld: "com.au", sld: "test", host: "www"},
{domain: "www.za.com", valid: true, tld: "za.com", sld: "www"},
{},
{domain: "a"},
{domain: "com"},
{domain: "co.com"},
{domain: "co.uk"},
{domain: "test.au"},
{domain: "za.com"},
{domain: "www.za"},
{domain: "www.test.au"},
{domain: "www.test.unk"},
}
for _, test := range tests {
test := test
t.Run(test.domain, func(t *testing.T) {
valid := true
ch, err := newChallenge(test.domain, "", tlds)
if err != nil {
valid = false
}
if test.valid && !valid {
t.Errorf("Expected '%s' to split", test.domain)
} else if !test.valid && valid {
t.Errorf("Expected '%s' to produce error", test.domain)
}
if test.valid && valid {
assertEq(t, "domain", ch.domain, test.domain)
assertEq(t, "tld", ch.tld, test.tld)
assertEq(t, "sld", ch.sld, test.sld)
assertEq(t, "host", ch.host, test.host)
}
})
}
}
func assertEq(t *testing.T, variable, got, want string) {
if got != want {
t.Errorf("Expected %s to be '%s' but got '%s'", variable, want, got)
@ -79,193 +250,16 @@ func mockServer(tc *testcase, t *testing.T, w http.ResponseWriter, r *http.Reque
}
}
func testGetHosts(tc *testcase, t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(tc, t, w, r)
}))
defer mock.Close()
prov := &DNSProvider{
baseURL: mock.URL,
apiUser: fakeUser,
apiKey: fakeKey,
clientIP: fakeClientIP,
client: &http.Client{Timeout: 60 * time.Second},
}
ch, _ := newChallenge(tc.domain, "", tlds)
hosts, err := prov.getHosts(ch)
if tc.errString != "" {
if err == nil || err.Error() != tc.errString {
t.Errorf("Namecheap getHosts case %s expected error", tc.name)
}
} else {
if err != nil {
t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err)
}
}
next1:
for _, h := range hosts {
for _, th := range tc.hosts {
if h == th {
continue next1
}
}
t.Errorf("getHosts case %s unexpected record [%s:%s:%s]",
tc.name, h.Type, h.Name, h.Address)
}
next2:
for _, th := range tc.hosts {
for _, h := range hosts {
if h == th {
continue next2
}
}
t.Errorf("getHosts case %s missing record [%s:%s:%s]",
tc.name, th.Type, th.Name, th.Address)
}
}
func mockDNSProvider(url string) *DNSProvider {
return &DNSProvider{
baseURL: url,
apiUser: fakeUser,
apiKey: fakeKey,
clientIP: fakeClientIP,
client: &http.Client{Timeout: 60 * time.Second},
}
}
config := NewDefaultConfig()
config.BaseURL = url
config.APIUser = fakeUser
config.APIKey = fakeKey
config.ClientIP = fakeClientIP
config.HTTPClient = &http.Client{Timeout: 60 * time.Second}
func testSetHosts(tc *testcase, t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(tc, t, w, r)
}))
defer mock.Close()
prov := mockDNSProvider(mock.URL)
ch, _ := newChallenge(tc.domain, "", tlds)
hosts, err := prov.getHosts(ch)
if tc.errString != "" {
if err == nil || err.Error() != tc.errString {
t.Errorf("Namecheap getHosts case %s expected error", tc.name)
}
} else {
if err != nil {
t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err)
}
}
if err != nil {
return
}
err = prov.setHosts(ch, hosts)
if err != nil {
t.Errorf("Namecheap setHosts case %s failed", tc.name)
}
}
func testPresent(tc *testcase, t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(tc, t, w, r)
}))
defer mock.Close()
prov := mockDNSProvider(mock.URL)
err := prov.Present(tc.domain, "", "dummyKey")
if tc.errString != "" {
if err == nil || err.Error() != tc.errString {
t.Errorf("Namecheap Present case %s expected error", tc.name)
}
} else {
if err != nil {
t.Errorf("Namecheap Present case %s failed\n%v", tc.name, err)
}
}
}
func testCleanUp(tc *testcase, t *testing.T) {
mock := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
mockServer(tc, t, w, r)
}))
defer mock.Close()
prov := mockDNSProvider(mock.URL)
err := prov.CleanUp(tc.domain, "", "dummyKey")
if tc.errString != "" {
if err == nil || err.Error() != tc.errString {
t.Errorf("Namecheap CleanUp case %s expected error", tc.name)
}
} else {
if err != nil {
t.Errorf("Namecheap CleanUp case %s failed\n%v", tc.name, err)
}
}
}
func TestNamecheap(t *testing.T) {
for _, tc := range testcases {
testGetHosts(&tc, t)
testSetHosts(&tc, t)
testPresent(&tc, t)
testCleanUp(&tc, t)
}
}
func TestNamecheapDomainSplit(t *testing.T) {
tests := []struct {
domain string
valid bool
tld string
sld string
host string
}{
{"a.b.c.test.co.uk", true, "co.uk", "test", "a.b.c"},
{"test.co.uk", true, "co.uk", "test", ""},
{"test.com", true, "com", "test", ""},
{"test.co.com", true, "co.com", "test", ""},
{"www.test.com.au", true, "com.au", "test", "www"},
{"www.za.com", true, "za.com", "www", ""},
{"", false, "", "", ""},
{"a", false, "", "", ""},
{"com", false, "", "", ""},
{"co.com", false, "", "", ""},
{"co.uk", false, "", "", ""},
{"test.au", false, "", "", ""},
{"za.com", false, "", "", ""},
{"www.za", false, "", "", ""},
{"www.test.au", false, "", "", ""},
{"www.test.unk", false, "", "", ""},
}
for _, test := range tests {
test := test
t.Run(test.domain, func(t *testing.T) {
valid := true
ch, err := newChallenge(test.domain, "", tlds)
if err != nil {
valid = false
}
if test.valid && !valid {
t.Errorf("Expected '%s' to split", test.domain)
} else if !test.valid && valid {
t.Errorf("Expected '%s' to produce error", test.domain)
}
if test.valid && valid {
assertEq(t, "domain", ch.domain, test.domain)
assertEq(t, "tld", ch.tld, test.tld)
assertEq(t, "sld", ch.sld, test.sld)
assertEq(t, "host", ch.host, test.host)
}
})
}
provider, _ := NewDNSProviderConfig(config)
return provider
}
type testcase struct {
@ -279,38 +273,34 @@ type testcase struct {
var testcases = []testcase{
{
"Test:Success:1",
"test.example.com",
[]host{
{"A", "home", "10.0.0.1", "10", "1799"},
{"A", "www", "10.0.0.2", "10", "1200"},
{"AAAA", "a", "::0", "10", "1799"},
{"CNAME", "*", "example.com.", "10", "1799"},
{"MXE", "example.com", "10.0.0.5", "10", "1800"},
{"URL", "xyz", "https://google.com", "10", "1799"},
name: "Test:Success:1",
domain: "test.example.com",
hosts: []host{
{Type: "A", Name: "home", Address: "10.0.0.1", MXPref: "10", TTL: "1799"},
{Type: "A", Name: "www", Address: "10.0.0.2", MXPref: "10", TTL: "1200"},
{Type: "AAAA", Name: "a", Address: "::0", MXPref: "10", TTL: "1799"},
{Type: "CNAME", Name: "*", Address: "example.com.", MXPref: "10", TTL: "1799"},
{Type: "MXE", Name: "example.com", Address: "10.0.0.5", MXPref: "10", TTL: "1800"},
{Type: "URL", Name: "xyz", Address: "https://google.com", MXPref: "10", TTL: "1799"},
},
"",
responseGetHostsSuccess1,
responseSetHostsSuccess1,
getHostsResponse: responseGetHostsSuccess1,
setHostsResponse: responseSetHostsSuccess1,
},
{
"Test:Success:2",
"example.com",
[]host{
{"A", "@", "10.0.0.2", "10", "1200"},
{"A", "www", "10.0.0.3", "10", "60"},
name: "Test:Success:2",
domain: "example.com",
hosts: []host{
{Type: "A", Name: "@", Address: "10.0.0.2", MXPref: "10", TTL: "1200"},
{Type: "A", Name: "www", Address: "10.0.0.3", MXPref: "10", TTL: "60"},
},
"",
responseGetHostsSuccess2,
responseSetHostsSuccess2,
getHostsResponse: responseGetHostsSuccess2,
setHostsResponse: responseSetHostsSuccess2,
},
{
"Test:Error:BadApiKey:1",
"test.example.com",
nil,
"Namecheap error: API Key is invalid or API access has not been enabled [1011102]",
responseGetHostsErrorBadAPIKey1,
"",
name: "Test:Error:BadApiKey:1",
domain: "test.example.com",
errString: "API Key is invalid or API access has not been enabled [1011102]",
getHostsResponse: responseGetHostsErrorBadAPIKey1,
},
}

View file

@ -3,66 +3,115 @@
package namedotcom
import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/namedotcom/go/namecom"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Username string
APIToken string
Server string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NAMECOM_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("NAMECOM_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NAMECOM_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NAMECOM_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *namecom.NameCom
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for namedotcom.
// Credentials must be passed in the environment variables: NAMECOM_USERNAME and NAMECOM_API_TOKEN
// Credentials must be passed in the environment variables:
// NAMECOM_USERNAME and NAMECOM_API_TOKEN
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NAMECOM_USERNAME", "NAMECOM_API_TOKEN")
if err != nil {
return nil, fmt.Errorf("Name.com: %v", err)
return nil, fmt.Errorf("namedotcom: %v", err)
}
server := os.Getenv("NAMECOM_SERVER")
return NewDNSProviderCredentials(values["NAMECOM_USERNAME"], values["NAMECOM_API_TOKEN"], server)
config := NewDefaultConfig()
config.Username = values["NAMECOM_USERNAME"]
config.APIToken = values["NAMECOM_API_TOKEN"]
config.Server = os.Getenv("NAMECOM_SERVER")
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for namedotcom.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for namedotcom.
// Deprecated
func NewDNSProviderCredentials(username, apiToken, server string) (*DNSProvider, error) {
if username == "" {
return nil, fmt.Errorf("Name.com Username is required")
}
if apiToken == "" {
return nil, fmt.Errorf("Name.com API token is required")
config := NewDefaultConfig()
config.Username = username
config.APIToken = apiToken
config.Server = server
return NewDNSProviderConfig(config)
}
client := namecom.New(username, apiToken)
if server != "" {
client.Server = server
// NewDNSProviderConfig return a DNSProvider instance configured for namedotcom.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("namedotcom: the configuration of the DNS provider is nil")
}
return &DNSProvider{client: client}, nil
if config.Username == "" {
return nil, fmt.Errorf("namedotcom: username is required")
}
if config.APIToken == "" {
return nil, fmt.Errorf("namedotcom: API token is required")
}
client := namecom.New(config.Username, config.APIToken)
client.Client = config.HTTPClient
if config.Server != "" {
client.Server = config.Server
}
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
request := &namecom.Record{
DomainName: domain,
Host: d.extractRecordName(fqdn, domain),
Type: "TXT",
TTL: uint32(ttl),
TTL: uint32(d.config.TTL),
Answer: value,
}
_, err := d.client.CreateRecord(request)
if err != nil {
return fmt.Errorf("Name.com API call failed: %v", err)
return fmt.Errorf("namedotcom: API call failed: %v", err)
}
return nil
@ -74,7 +123,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
records, err := d.getRecords(domain)
if err != nil {
return err
return fmt.Errorf("namedotcom: %v", err)
}
for _, rec := range records {
@ -85,7 +134,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
_, err := d.client.DeleteRecord(request)
if err != nil {
return err
return fmt.Errorf("namedotcom: %v", err)
}
}
}
@ -93,20 +142,21 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
func (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) {
var (
err error
records []*namecom.Record
response *namecom.ListRecordsResponse
)
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) {
request := &namecom.ListRecordsRequest{
DomainName: domain,
Page: 1,
}
var records []*namecom.Record
for request.Page > 0 {
response, err = d.client.ListRecords(request)
response, err := d.client.ListRecords(request)
if err != nil {
return nil, err
}

View file

@ -32,7 +32,12 @@ func TestLiveNamedotcomPresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(namedotcomUsername, namedotcomAPIToken, namedotcomServer)
config := NewDefaultConfig()
config.Username = namedotcomUsername
config.APIToken = namedotcomAPIToken
config.Server = namedotcomServer
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(namedotcomDomain, "", "123d==")
@ -50,7 +55,12 @@ func TestLiveNamedotcomCleanUp(t *testing.T) {
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderCredentials(namedotcomUsername, namedotcomAPIToken, namedotcomServer)
config := NewDefaultConfig()
config.Username = namedotcomUsername
config.APIToken = namedotcomAPIToken
config.Server = namedotcomServer
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(namedotcomDomain, "", "123d==")

View file

@ -6,12 +6,13 @@ import (
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/xenolf/lego/acme"
)
// netcupBaseURL for reaching the jSON-based API-Endpoint of netcup
const netcupBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
// defaultBaseURL for reaching the jSON-based API-Endpoint of netcup
const defaultBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
// success response status
const success = "success"
@ -80,6 +81,7 @@ type DNSRecord struct {
Destination string `json:"destination"`
DeleteRecord bool `json:"deleterecord,omitempty"`
State string `json:"state,omitempty"`
TTL int `json:"ttl,omitempty"`
}
// ResponseMsg as specified in netcup WSDL
@ -119,21 +121,20 @@ type Client struct {
customerNumber string
apiKey string
apiPassword string
client *http.Client
HTTPClient *http.Client
BaseURL string
}
// NewClient creates a netcup DNS client
func NewClient(httpClient *http.Client, customerNumber string, apiKey string, apiPassword string) *Client {
client := http.DefaultClient
if httpClient != nil {
client = httpClient
}
func NewClient(customerNumber string, apiKey string, apiPassword string) *Client {
return &Client{
customerNumber: customerNumber,
apiKey: apiKey,
apiPassword: apiPassword,
client: client,
BaseURL: defaultBaseURL,
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
@ -153,17 +154,17 @@ func (c *Client) Login() (string, error) {
response, err := c.sendRequest(payload)
if err != nil {
return "", fmt.Errorf("netcup: error sending request to DNS-API, %v", err)
return "", fmt.Errorf("error sending request to DNS-API, %v", err)
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return "", fmt.Errorf("netcup: error decoding response of DNS-API, %v", err)
return "", fmt.Errorf("error decoding response of DNS-API, %v", err)
}
if r.Status != success {
return "", fmt.Errorf("netcup: error logging into DNS-API, %v", r.LongMessage)
return "", fmt.Errorf("error logging into DNS-API, %v", r.LongMessage)
}
return r.ResponseData.APISessionID, nil
}
@ -183,18 +184,18 @@ func (c *Client) Logout(sessionID string) error {
response, err := c.sendRequest(payload)
if err != nil {
return fmt.Errorf("netcup: error logging out of DNS-API: %v", err)
return fmt.Errorf("error logging out of DNS-API: %v", err)
}
var r LogoutResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return fmt.Errorf("netcup: error logging out of DNS-API: %v", err)
return fmt.Errorf("error logging out of DNS-API: %v", err)
}
if r.Status != success {
return fmt.Errorf("netcup: error logging out of DNS-API: %v", r.ShortMessage)
return fmt.Errorf("error logging out of DNS-API: %v", r.ShortMessage)
}
return nil
}
@ -216,18 +217,18 @@ func (c *Client) UpdateDNSRecord(sessionID, domainName string, record DNSRecord)
response, err := c.sendRequest(payload)
if err != nil {
return fmt.Errorf("netcup: %v", err)
return err
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return fmt.Errorf("netcup: %v", err)
return err
}
if r.Status != success {
return fmt.Errorf("netcup: %s: %+v", r.ShortMessage, r)
return fmt.Errorf("%s: %+v", r.ShortMessage, r)
}
return nil
}
@ -249,18 +250,18 @@ func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, erro
response, err := c.sendRequest(payload)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
return nil, err
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
return nil, err
}
if r.Status != success {
return nil, fmt.Errorf("netcup: %s", r.ShortMessage)
return nil, fmt.Errorf("%s", r.ShortMessage)
}
return r.ResponseData.DNSRecords, nil
@ -271,30 +272,30 @@ func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, erro
func (c *Client) sendRequest(payload interface{}) ([]byte, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
return nil, err
}
req, err := http.NewRequest(http.MethodPost, netcupBaseURL, bytes.NewReader(body))
req, err := http.NewRequest(http.MethodPost, c.BaseURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
return nil, err
}
req.Close = true
req.Header.Set("content-type", "application/json")
req.Header.Set("User-Agent", acme.UserAgent)
resp, err := c.client.Do(req)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
return nil, err
}
if resp.StatusCode > 299 {
return nil, fmt.Errorf("netcup: API request failed with HTTP Status code %d", resp.StatusCode)
return nil, fmt.Errorf("API request failed with HTTP Status code %d", resp.StatusCode)
}
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("netcup: read of response body failed, %v", err)
return nil, fmt.Errorf("read of response body failed, %v", err)
}
defer resp.Body.Close()
@ -310,11 +311,11 @@ func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
return index, nil
}
}
return -1, fmt.Errorf("netcup: no DNS Record found")
return -1, fmt.Errorf("no DNS Record found")
}
// CreateTxtRecord uses the supplied values to return a DNSRecord of type TXT for the dns-01 challenge
func CreateTxtRecord(hostname, value string) DNSRecord {
func CreateTxtRecord(hostname, value string, ttl int) DNSRecord {
return DNSRecord{
ID: 0,
Hostname: hostname,
@ -323,5 +324,6 @@ func CreateTxtRecord(hostname, value string) DNSRecord {
Destination: value,
DeleteRecord: false,
State: "",
TTL: ttl,
}
}

View file

@ -2,10 +2,8 @@ package netcup
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/xenolf/lego/acme"
@ -17,10 +15,7 @@ func TestClientAuth(t *testing.T) {
}
// Setup
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
client := NewClient(httpClient, testCustomerNumber, testAPIKey, testAPIPassword)
client := NewClient(testCustomerNumber, testAPIKey, testAPIPassword)
for i := 1; i < 4; i++ {
i := i
@ -42,10 +37,7 @@ func TestClientGetDnsRecords(t *testing.T) {
t.Skip("skipping live test")
}
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
client := NewClient(httpClient, testCustomerNumber, testAPIKey, testAPIPassword)
client := NewClient(testCustomerNumber, testAPIKey, testAPIPassword)
// Setup
sessionID, err := client.Login()
@ -73,10 +65,7 @@ func TestClientUpdateDnsRecord(t *testing.T) {
}
// Setup
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
client := NewClient(httpClient, testCustomerNumber, testAPIKey, testAPIPassword)
client := NewClient(testCustomerNumber, testAPIKey, testAPIPassword)
sessionID, err := client.Login()
assert.NoError(t, err)
@ -88,7 +77,7 @@ func TestClientUpdateDnsRecord(t *testing.T) {
hostname := strings.Replace(fqdn, "."+zone, "", 1)
record := CreateTxtRecord(hostname, "asdf5678")
record := CreateTxtRecord(hostname, "asdf5678", 120)
// test
zone = acme.UnFqdn(zone)

View file

@ -2,6 +2,7 @@
package netcup
import (
"errors"
"fmt"
"net/http"
"strings"
@ -11,37 +12,78 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Key string
Password string
Customer string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NETCUP_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("NETCUP_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NETCUP_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NETCUP_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
client *Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for netcup.
// Credentials must be passed in the environment variables: NETCUP_CUSTOMER_NUMBER,
// NETCUP_API_KEY, NETCUP_API_PASSWORD
// Credentials must be passed in the environment variables:
// NETCUP_CUSTOMER_NUMBER, NETCUP_API_KEY, NETCUP_API_PASSWORD
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NETCUP_CUSTOMER_NUMBER", "NETCUP_API_KEY", "NETCUP_API_PASSWORD")
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
return NewDNSProviderCredentials(values["NETCUP_CUSTOMER_NUMBER"], values["NETCUP_API_KEY"], values["NETCUP_API_PASSWORD"])
config := NewDefaultConfig()
config.Customer = values["NETCUP_CUSTOMER_NUMBER"]
config.Key = values["NETCUP_API_KEY"]
config.Password = values["NETCUP_API_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for netcup.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for netcup.
// Deprecated
func NewDNSProviderCredentials(customer, key, password string) (*DNSProvider, error) {
if customer == "" || key == "" || password == "" {
config := NewDefaultConfig()
config.Customer = customer
config.Key = key
config.Password = password
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for netcup.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("netcup: the configuration of the DNS provider is nil")
}
if config.Customer == "" || config.Key == "" || config.Password == "" {
return nil, fmt.Errorf("netcup: netcup credentials missing")
}
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
client := NewClient(config.Customer, config.Key, config.Password)
client.HTTPClient = config.HTTPClient
return &DNSProvider{
client: NewClient(httpClient, customer, key, password),
}, nil
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge
@ -55,21 +97,25 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
sessionID, err := d.client.Login()
if err != nil {
return err
return fmt.Errorf("netcup: %v", err)
}
hostname := strings.Replace(fqdn, "."+zone, "", 1)
record := CreateTxtRecord(hostname, value)
record := CreateTxtRecord(hostname, value, d.config.TTL)
err = d.client.UpdateDNSRecord(sessionID, acme.UnFqdn(zone), record)
if err != nil {
if errLogout := d.client.Logout(sessionID); errLogout != nil {
return fmt.Errorf("failed to add TXT-Record: %v; %v", err, errLogout)
return fmt.Errorf("netcup: failed to add TXT-Record: %v; %v", err, errLogout)
}
return fmt.Errorf("failed to add TXT-Record: %v", err)
return fmt.Errorf("netcup: failed to add TXT-Record: %v", err)
}
return d.client.Logout(sessionID)
err = d.client.Logout(sessionID)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
@ -78,12 +124,12 @@ func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("failed to find DNSZone, %v", err)
return fmt.Errorf("netcup: failed to find DNSZone, %v", err)
}
sessionID, err := d.client.Login()
if err != nil {
return err
return fmt.Errorf("netcup: %v", err)
}
hostname := strings.Replace(fqdn, "."+zone, "", 1)
@ -92,14 +138,14 @@ func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
records, err := d.client.GetDNSRecords(zone, sessionID)
if err != nil {
return err
return fmt.Errorf("netcup: %v", err)
}
record := CreateTxtRecord(hostname, value)
record := CreateTxtRecord(hostname, value, 0)
idx, err := GetDNSRecordIdx(records, record)
if err != nil {
return err
return fmt.Errorf("netcup: %v", err)
}
records[idx].DeleteRecord = true
@ -107,10 +153,20 @@ func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
err = d.client.UpdateDNSRecord(sessionID, zone, records[idx])
if err != nil {
if errLogout := d.client.Logout(sessionID); errLogout != nil {
return fmt.Errorf("%v; %v", err, errLogout)
return fmt.Errorf("netcup: %v; %v", err, errLogout)
}
return err
return fmt.Errorf("netcup: %v", err)
}
return d.client.Logout(sessionID)
err = d.client.Logout(sessionID)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -15,7 +15,7 @@ import (
)
const (
defaultEndpoint = "https://dns.api.cloud.nifty.com"
defaultBaseURL = "https://dns.api.cloud.nifty.com"
apiVersion = "2012-12-12N2013-12-16"
xmlNs = "https://route53.amazonaws.com/doc/2012-12-12/"
)
@ -88,17 +88,13 @@ type ChangeInfo struct {
SubmittedAt string `xml:"SubmittedAt"`
}
func newClient(httpClient *http.Client, accessKey string, secretKey string, endpoint string) *Client {
client := http.DefaultClient
if httpClient != nil {
client = httpClient
}
// NewClient Creates a new client of NIFCLOUD DNS
func NewClient(accessKey string, secretKey string) *Client {
return &Client{
accessKey: accessKey,
secretKey: secretKey,
endpoint: endpoint,
client: client,
BaseURL: defaultBaseURL,
HTTPClient: &http.Client{},
}
}
@ -106,13 +102,13 @@ func newClient(httpClient *http.Client, accessKey string, secretKey string, endp
type Client struct {
accessKey string
secretKey string
endpoint string
client *http.Client
BaseURL string
HTTPClient *http.Client
}
// ChangeResourceRecordSets Call ChangeResourceRecordSets API and return response.
func (c *Client) ChangeResourceRecordSets(hostedZoneID string, input ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) {
requestURL := fmt.Sprintf("%s/%s/hostedzone/%s/rrset", c.endpoint, apiVersion, hostedZoneID)
requestURL := fmt.Sprintf("%s/%s/hostedzone/%s/rrset", c.BaseURL, apiVersion, hostedZoneID)
body := &bytes.Buffer{}
body.Write([]byte(xml.Header))
@ -133,7 +129,7 @@ func (c *Client) ChangeResourceRecordSets(hostedZoneID string, input ChangeResou
return nil, fmt.Errorf("an error occurred during the creation of the signature: %v", err)
}
res, err := c.client.Do(req)
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
@ -164,7 +160,7 @@ func (c *Client) ChangeResourceRecordSets(hostedZoneID string, input ChangeResou
// GetChange Call GetChange API and return response.
func (c *Client) GetChange(statusID string) (*GetChangeResponse, error) {
requestURL := fmt.Sprintf("%s/%s/change/%s", c.endpoint, apiVersion, statusID)
requestURL := fmt.Sprintf("%s/%s/change/%s", c.BaseURL, apiVersion, statusID)
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
@ -176,7 +172,7 @@ func (c *Client) GetChange(statusID string) (*GetChangeResponse, error) {
return nil, fmt.Errorf("an error occurred during the creation of the signature: %v", err)
}
res, err := c.client.Do(req)
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}

View file

@ -31,7 +31,8 @@ func TestChangeResourceRecordSets(t *testing.T) {
server := runTestServer(responseBody, http.StatusOK)
defer server.Close()
client := newClient(nil, "", "", server.URL)
client := NewClient("", "")
client.BaseURL = server.URL
res, err := client.ChangeResourceRecordSets("example.com", ChangeResourceRecordSetsRequest{})
require.NoError(t, err)
@ -82,7 +83,8 @@ func TestChangeResourceRecordSetsErrors(t *testing.T) {
server := runTestServer(test.responseBody, test.statusCode)
defer server.Close()
client := newClient(nil, "", "", server.URL)
client := NewClient("", "")
client.BaseURL = server.URL
res, err := client.ChangeResourceRecordSets("example.com", ChangeResourceRecordSetsRequest{})
assert.Nil(t, res)
@ -105,7 +107,8 @@ func TestGetChange(t *testing.T) {
server := runTestServer(responseBody, http.StatusOK)
defer server.Close()
client := newClient(nil, "", "", server.URL)
client := NewClient("", "")
client.BaseURL = server.URL
res, err := client.GetChange("12345")
require.NoError(t, err)
@ -156,7 +159,8 @@ func TestGetChangeErrors(t *testing.T) {
server := runTestServer(test.responseBody, test.statusCode)
defer server.Close()
client := newClient(nil, "", "", server.URL)
client := NewClient("", "")
client.BaseURL = server.URL
res, err := client.GetChange("12345")
assert.Nil(t, res)

View file

@ -3,6 +3,7 @@
package nifcloud
import (
"errors"
"fmt"
"net/http"
"os"
@ -12,49 +13,110 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
AccessKey string
SecretKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NIFCLOUD_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("NIFCLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NIFCLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NIFCLOUD_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for the NIFCLOUD DNS service.
// Credentials must be passed in the environment variables: NIFCLOUD_ACCESS_KEY_ID and NIFCLOUD_SECRET_ACCESS_KEY.
// Credentials must be passed in the environment variables:
// NIFCLOUD_ACCESS_KEY_ID and NIFCLOUD_SECRET_ACCESS_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NIFCLOUD_ACCESS_KEY_ID", "NIFCLOUD_SECRET_ACCESS_KEY")
if err != nil {
return nil, fmt.Errorf("NIFCLOUD: %v", err)
return nil, fmt.Errorf("nifcloud: %v", err)
}
endpoint := os.Getenv("NIFCLOUD_DNS_ENDPOINT")
if endpoint == "" {
endpoint = defaultEndpoint
config := NewDefaultConfig()
config.BaseURL = os.Getenv("NIFCLOUD_DNS_ENDPOINT")
config.AccessKey = values["NIFCLOUD_ACCESS_KEY_ID"]
config.SecretKey = values["NIFCLOUD_SECRET_ACCESS_KEY"]
return NewDNSProviderConfig(config)
}
httpClient := &http.Client{Timeout: 30 * time.Second}
return NewDNSProviderCredentials(httpClient, endpoint, values["NIFCLOUD_ACCESS_KEY_ID"], values["NIFCLOUD_SECRET_ACCESS_KEY"])
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for NIFCLOUD.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for NIFCLOUD.
// Deprecated
func NewDNSProviderCredentials(httpClient *http.Client, endpoint, accessKey, secretKey string) (*DNSProvider, error) {
client := newClient(httpClient, accessKey, secretKey, endpoint)
config := NewDefaultConfig()
config.HTTPClient = httpClient
config.BaseURL = endpoint
config.AccessKey = accessKey
config.SecretKey = secretKey
return &DNSProvider{
client: client,
}, nil
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for NIFCLOUD.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("nifcloud: the configuration of the DNS provider is nil")
}
client := NewClient(config.AccessKey, config.SecretKey)
if config.HTTPClient != nil {
client.HTTPClient = config.HTTPClient
}
if len(config.BaseURL) > 0 {
client.BaseURL = config.BaseURL
}
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
return d.changeRecord("CREATE", fqdn, value, domain, ttl)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
err := d.changeRecord("CREATE", fqdn, value, domain, d.config.TTL)
if err != nil {
return fmt.Errorf("nifcloud: %v", err)
}
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
return d.changeRecord("DELETE", fqdn, value, domain, ttl)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
err := d.changeRecord("DELETE", fqdn, value, domain, d.config.TTL)
if err != nil {
return fmt.Errorf("nifcloud: %v", err)
}
return err
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) changeRecord(action, fqdn, value, domain string, ttl int) error {

View file

@ -3,6 +3,7 @@
package ns1
import (
"errors"
"fmt"
"net/http"
"strings"
@ -14,9 +15,31 @@ import (
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NS1_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("NS1_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NS1_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NS1_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *rest.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for NS1.
@ -24,38 +47,53 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NS1_API_KEY")
if err != nil {
return nil, fmt.Errorf("NS1: %v", err)
return nil, fmt.Errorf("ns1: %v", err)
}
return NewDNSProviderCredentials(values["NS1_API_KEY"])
config := NewDefaultConfig()
config.APIKey = values["NS1_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for NS1.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for NS1.
// Deprecated
func NewDNSProviderCredentials(key string) (*DNSProvider, error) {
if key == "" {
return nil, fmt.Errorf("NS1 credentials missing")
config := NewDefaultConfig()
config.APIKey = key
return NewDNSProviderConfig(config)
}
httpClient := &http.Client{Timeout: time.Second * 10}
client := rest.NewClient(httpClient, rest.SetAPIKey(key))
// NewDNSProviderConfig return a DNSProvider instance configured for NS1.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ns1: the configuration of the DNS provider is nil")
}
return &DNSProvider{client}, nil
if config.APIKey == "" {
return nil, fmt.Errorf("ns1: credentials missing")
}
client := rest.NewClient(config.HTTPClient, rest.SetAPIKey(config.APIKey))
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("ns1: %v", err)
}
record := d.newTxtRecord(zone, fqdn, value, ttl)
record := d.newTxtRecord(zone, fqdn, value, d.config.TTL)
_, err = d.client.Records.Create(record)
if err != nil && err != rest.ErrRecordExists {
return err
return fmt.Errorf("ns1: %v", err)
}
return nil
@ -67,23 +105,29 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
zone, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("ns1: %v", err)
}
name := acme.UnFqdn(fqdn)
_, err = d.client.Records.Delete(zone.Zone, name, "TXT")
return err
return fmt.Errorf("ns1: %v", err)
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) {
authZone, err := getAuthZone(domain)
if err != nil {
return nil, err
return nil, fmt.Errorf("ns1: %v", err)
}
zone, _, err := d.client.Zones.Get(authZone)
if err != nil {
return nil, err
return nil, fmt.Errorf("ns1: %v", err)
}
return zone, nil

View file

@ -30,7 +30,10 @@ func TestNewDNSProviderValid(t *testing.T) {
defer restoreEnv()
os.Setenv("NS1_API_KEY", "")
_, err := NewDNSProviderCredentials("123")
config := NewDefaultConfig()
config.APIKey = "123"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
@ -39,7 +42,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("NS1_API_KEY", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "NS1: some credentials information are missing: NS1_API_KEY")
assert.EqualError(t, err, "ns1: some credentials information are missing: NS1_API_KEY")
}
func TestLivePresent(t *testing.T) {
@ -47,7 +50,10 @@ func TestLivePresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(apiKey)
config := NewDefaultConfig()
config.APIKey = apiKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(domain, "", "123d==")
@ -61,7 +67,10 @@ func TestLiveCleanUp(t *testing.T) {
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderCredentials(apiKey)
config := NewDefaultConfig()
config.APIKey = apiKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(domain, "", "123d==")

View file

@ -0,0 +1,68 @@
package otc
type recordset struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
TTL int `json:"ttl"`
Records []string `json:"records"`
}
type nameResponse struct {
Name string `json:"name"`
}
type userResponse struct {
Name string `json:"name"`
Password string `json:"password"`
Domain nameResponse `json:"domain"`
}
type passwordResponse struct {
User userResponse `json:"user"`
}
type identityResponse struct {
Methods []string `json:"methods"`
Password passwordResponse `json:"password"`
}
type scopeResponse struct {
Project nameResponse `json:"project"`
}
type authResponse struct {
Identity identityResponse `json:"identity"`
Scope scopeResponse `json:"scope"`
}
type loginResponse struct {
Auth authResponse `json:"auth"`
}
type endpointResponse struct {
Token struct {
Catalog []struct {
Type string `json:"type"`
Endpoints []struct {
URL string `json:"url"`
} `json:"endpoints"`
} `json:"catalog"`
} `json:"token"`
}
type zoneItem struct {
ID string `json:"id"`
}
type zonesResponse struct {
Zones []zoneItem `json:"zones"`
}
type recordSet struct {
ID string `json:"id"`
}
type recordSetsResponse struct {
RecordSets []recordSet `json:"recordsets"`
}

View file

@ -5,26 +5,69 @@ package otc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const defaultIdentityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens"
// minTTL 300 is otc minimum value for ttl
const minTTL = 300
// Config is used to configure the creation of the DNSProvider
type Config struct {
IdentityEndpoint string
DomainName string
ProjectName string
UserName string
Password string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
IdentityEndpoint: env.GetOrDefaultString("OTC_IDENTITY_ENDPOINT", defaultIdentityEndpoint),
PropagationTimeout: env.GetOrDefaultSecond("OTC_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("OTC_POLLING_INTERVAL", acme.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("OTC_TTL", minTTL),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("OTC_HTTP_TIMEOUT", 10*time.Second),
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// Workaround for keep alive bug in otc api
DisableKeepAlives: true,
},
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// OTC's Managed DNS API to manage TXT records for a domain.
type DNSProvider struct {
identityEndpoint string
otcBaseURL string
domainName string
projectName string
userName string
password string
config *Config
baseURL string
token string
}
@ -34,41 +77,129 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("OTC_DOMAIN_NAME", "OTC_USER_NAME", "OTC_PASSWORD", "OTC_PROJECT_NAME")
if err != nil {
return nil, fmt.Errorf("OTC: %v", err)
return nil, fmt.Errorf("otc: %v", err)
}
return NewDNSProviderCredentials(
values["OTC_DOMAIN_NAME"],
values["OTC_USER_NAME"],
values["OTC_PASSWORD"],
values["OTC_PROJECT_NAME"],
os.Getenv("OTC_IDENTITY_ENDPOINT"),
)
config := NewDefaultConfig()
config.DomainName = values["OTC_DOMAIN_NAME"]
config.UserName = values["OTC_USER_NAME"]
config.Password = values["OTC_PASSWORD"]
config.ProjectName = values["OTC_PROJECT_NAME"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for OTC DNS.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for OTC DNS.
// Deprecated
func NewDNSProviderCredentials(domainName, userName, password, projectName, identityEndpoint string) (*DNSProvider, error) {
if domainName == "" || userName == "" || password == "" || projectName == "" {
return nil, fmt.Errorf("OTC credentials missing")
config := NewDefaultConfig()
config.IdentityEndpoint = identityEndpoint
config.DomainName = domainName
config.UserName = userName
config.Password = password
config.ProjectName = projectName
return NewDNSProviderConfig(config)
}
if identityEndpoint == "" {
identityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens"
// NewDNSProviderConfig return a DNSProvider instance configured for OTC DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("otc: the configuration of the DNS provider is nil")
}
return &DNSProvider{
identityEndpoint: identityEndpoint,
domainName: domainName,
userName: userName,
password: password,
projectName: projectName,
}, nil
if config.DomainName == "" || config.UserName == "" || config.Password == "" || config.ProjectName == "" {
return nil, fmt.Errorf("otc: credentials missing")
}
// SendRequest send request
func (d *DNSProvider) SendRequest(method, resource string, payload interface{}) (io.Reader, error) {
url := fmt.Sprintf("%s/%s", d.otcBaseURL, resource)
if config.IdentityEndpoint == "" {
config.IdentityEndpoint = defaultIdentityEndpoint
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
if d.config.TTL < minTTL {
d.config.TTL = minTTL
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
err = d.login()
if err != nil {
return fmt.Errorf("otc: %v", err)
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("otc: unable to get zone: %s", err)
}
resource := fmt.Sprintf("zones/%s/recordsets", zoneID)
r1 := &recordset{
Name: fqdn,
Description: "Added TXT record for ACME dns-01 challenge using lego client",
Type: "TXT",
TTL: d.config.TTL,
Records: []string{fmt.Sprintf("\"%s\"", value)},
}
_, err = d.sendRequest(http.MethodPost, resource, r1)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
err = d.login()
if err != nil {
return fmt.Errorf("otc: %v", err)
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
recordID, err := d.getRecordSetID(zoneID, fqdn)
if err != nil {
return fmt.Errorf("otc: unable go get record %s for zone %s: %s", fqdn, domain, err)
}
err = d.deleteRecordSet(zoneID, recordID)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// sendRequest send request
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (io.Reader, error) {
url := fmt.Sprintf("%s/%s", d.baseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
@ -84,15 +215,7 @@ func (d *DNSProvider) SendRequest(method, resource string, payload interface{})
req.Header.Set("X-Auth-Token", d.token)
}
// Workaround for keep alive bug in otc api
tr := http.DefaultTransport.(*http.Transport)
tr.DisableKeepAlives = true
client := &http.Client{
Timeout: 10 * time.Second,
Transport: tr,
}
resp, err := client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
@ -111,42 +234,11 @@ func (d *DNSProvider) SendRequest(method, resource string, payload interface{})
}
func (d *DNSProvider) loginRequest() error {
type nameResponse struct {
Name string `json:"name"`
}
type userResponse struct {
Name string `json:"name"`
Password string `json:"password"`
Domain nameResponse `json:"domain"`
}
type passwordResponse struct {
User userResponse `json:"user"`
}
type identityResponse struct {
Methods []string `json:"methods"`
Password passwordResponse `json:"password"`
}
type scopeResponse struct {
Project nameResponse `json:"project"`
}
type authResponse struct {
Identity identityResponse `json:"identity"`
Scope scopeResponse `json:"scope"`
}
type loginResponse struct {
Auth authResponse `json:"auth"`
}
userResp := userResponse{
Name: d.userName,
Password: d.password,
Name: d.config.UserName,
Password: d.config.Password,
Domain: nameResponse{
Name: d.domainName,
Name: d.config.DomainName,
},
}
@ -160,7 +252,7 @@ func (d *DNSProvider) loginRequest() error {
},
Scope: scopeResponse{
Project: nameResponse{
Name: d.projectName,
Name: d.config.ProjectName,
},
},
},
@ -170,13 +262,14 @@ func (d *DNSProvider) loginRequest() error {
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, d.identityEndpoint, bytes.NewReader(body))
req, err := http.NewRequest(http.MethodPost, d.config.IdentityEndpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
client := &http.Client{Timeout: d.config.HTTPClient.Timeout}
resp, err := client.Do(req)
if err != nil {
return err
@ -193,16 +286,6 @@ func (d *DNSProvider) loginRequest() error {
return fmt.Errorf("unable to get auth token")
}
type endpointResponse struct {
Token struct {
Catalog []struct {
Type string `json:"type"`
Endpoints []struct {
URL string `json:"url"`
} `json:"endpoints"`
} `json:"catalog"`
} `json:"token"`
}
var endpointResp endpointResponse
err = json.NewDecoder(resp.Body).Decode(&endpointResp)
@ -213,13 +296,13 @@ func (d *DNSProvider) loginRequest() error {
for _, v := range endpointResp.Token.Catalog {
if v.Type == "dns" {
for _, endpoint := range v.Endpoints {
d.otcBaseURL = fmt.Sprintf("%s/v2", endpoint.URL)
d.baseURL = fmt.Sprintf("%s/v2", endpoint.URL)
continue
}
}
}
if d.otcBaseURL == "" {
if d.baseURL == "" {
return fmt.Errorf("unable to get dns endpoint")
}
@ -233,16 +316,8 @@ func (d *DNSProvider) login() error {
}
func (d *DNSProvider) getZoneID(zone string) (string, error) {
type zoneItem struct {
ID string `json:"id"`
}
type zonesResponse struct {
Zones []zoneItem `json:"zones"`
}
resource := fmt.Sprintf("zones?name=%s", zone)
resp, err := d.SendRequest(http.MethodGet, resource, nil)
resp, err := d.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return "", err
}
@ -269,16 +344,8 @@ func (d *DNSProvider) getZoneID(zone string) (string, error) {
}
func (d *DNSProvider) getRecordSetID(zoneID string, fqdn string) (string, error) {
type recordSet struct {
ID string `json:"id"`
}
type recordSetsResponse struct {
RecordSets []recordSet `json:"recordsets"`
}
resource := fmt.Sprintf("zones/%s/recordsets?type=TXT&name=%s", zoneID, fqdn)
resp, err := d.SendRequest(http.MethodGet, resource, nil)
resp, err := d.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return "", err
}
@ -307,77 +374,6 @@ func (d *DNSProvider) getRecordSetID(zoneID string, fqdn string) (string, error)
func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error {
resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID)
_, err := d.SendRequest(http.MethodDelete, resource, nil)
_, err := d.sendRequest(http.MethodDelete, resource, nil)
return err
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
if ttl < 300 {
ttl = 300 // 300 is otc minimum value for ttl
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return err
}
err = d.login()
if err != nil {
return err
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("unable to get zone: %s", err)
}
resource := fmt.Sprintf("zones/%s/recordsets", zoneID)
type recordset struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
TTL int `json:"ttl"`
Records []string `json:"records"`
}
r1 := &recordset{
Name: fqdn,
Description: "Added TXT record for ACME dns-01 challenge using lego client",
Type: "TXT",
TTL: ttl,
Records: []string{fmt.Sprintf("\"%s\"", value)},
}
_, err = d.SendRequest(http.MethodPost, resource, r1)
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return err
}
err = d.login()
if err != nil {
return err
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return err
}
recordID, err := d.getRecordSetID(zoneID, fqdn)
if err != nil {
return fmt.Errorf("unable go get record %s for zone %s: %s", fqdn, domain, err)
}
return d.deleteRecordSet(zoneID, recordID)
}

View file

@ -31,7 +31,15 @@ func TestOTCDNSTestSuite(t *testing.T) {
func (s *OTCDNSTestSuite) createDNSProvider() (*DNSProvider, error) {
url := fmt.Sprintf("%s/v3/auth/token", s.Mock.Server.URL)
return NewDNSProviderCredentials(fakeOTCUserName, fakeOTCPassword, fakeOTCDomainName, fakeOTCProjectName, url)
config := NewDefaultConfig()
config.UserName = fakeOTCUserName
config.Password = fakeOTCPassword
config.DomainName = fakeOTCDomainName
config.ProjectName = fakeOTCProjectName
config.IdentityEndpoint = url
return NewDNSProviderConfig(config)
}
func (s *OTCDNSTestSuite) TestOTCDNSLoginEnv() {
@ -45,24 +53,24 @@ func (s *OTCDNSTestSuite) TestOTCDNSLoginEnv() {
provider, err := NewDNSProvider()
assert.Nil(s.T(), err)
assert.Equal(s.T(), provider.domainName, "unittest1")
assert.Equal(s.T(), provider.userName, "unittest2")
assert.Equal(s.T(), provider.password, "unittest3")
assert.Equal(s.T(), provider.projectName, "unittest4")
assert.Equal(s.T(), provider.identityEndpoint, "unittest5")
assert.Equal(s.T(), provider.config.DomainName, "unittest1")
assert.Equal(s.T(), provider.config.UserName, "unittest2")
assert.Equal(s.T(), provider.config.Password, "unittest3")
assert.Equal(s.T(), provider.config.ProjectName, "unittest4")
assert.Equal(s.T(), provider.config.IdentityEndpoint, "unittest5")
os.Setenv("OTC_IDENTITY_ENDPOINT", "")
provider, err = NewDNSProvider()
assert.Nil(s.T(), err)
assert.Equal(s.T(), provider.identityEndpoint, "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens")
assert.Equal(s.T(), provider.config.IdentityEndpoint, "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens")
}
func (s *OTCDNSTestSuite) TestOTCDNSLoginEnvEmpty() {
defer os.Clearenv()
_, err := NewDNSProvider()
assert.EqualError(s.T(), err, "OTC: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME")
assert.EqualError(s.T(), err, "otc: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME")
}
func (s *OTCDNSTestSuite) TestOTCDNSLogin() {
@ -71,7 +79,7 @@ func (s *OTCDNSTestSuite) TestOTCDNSLogin() {
assert.Nil(s.T(), err)
err = otcProvider.loginRequest()
assert.Nil(s.T(), err)
assert.Equal(s.T(), otcProvider.otcBaseURL, fmt.Sprintf("%s/v2", s.Mock.Server.URL))
assert.Equal(s.T(), otcProvider.baseURL, fmt.Sprintf("%s/v2", s.Mock.Server.URL))
assert.Equal(s.T(), fakeOTCToken, otcProvider.token)
}

View file

@ -3,9 +3,12 @@
package ovh
import (
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/ovh/go-ovh/ovh"
"github.com/xenolf/lego/acme"
@ -15,9 +18,34 @@ import (
// OVH API reference: https://eu.api.ovh.com/
// Create a Token: https://eu.api.ovh.com/createToken/
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIEndpoint string
ApplicationKey string
ApplicationSecret string
ConsumerKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("OVH_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("OVH_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("OVH_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("OVH_HTTP_TIMEOUT", ovh.DefaultTimeout),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// that uses OVH's REST API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
client *ovh.Client
recordIDs map[string]int
recordIDsMu sync.Mutex
@ -32,69 +60,88 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("OVH_ENDPOINT", "OVH_APPLICATION_KEY", "OVH_APPLICATION_SECRET", "OVH_CONSUMER_KEY")
if err != nil {
return nil, fmt.Errorf("OVH: %v", err)
return nil, fmt.Errorf("ovh: %v", err)
}
return NewDNSProviderCredentials(
values["OVH_ENDPOINT"],
values["OVH_APPLICATION_KEY"],
values["OVH_APPLICATION_SECRET"],
values["OVH_CONSUMER_KEY"],
)
config := NewDefaultConfig()
config.APIEndpoint = values["OVH_ENDPOINT"]
config.ApplicationKey = values["OVH_APPLICATION_KEY"]
config.ApplicationSecret = values["OVH_APPLICATION_SECRET"]
config.ConsumerKey = values["OVH_CONSUMER_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for OVH.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for OVH.
// Deprecated
func NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, consumerKey string) (*DNSProvider, error) {
if apiEndpoint == "" || applicationKey == "" || applicationSecret == "" || consumerKey == "" {
return nil, fmt.Errorf("OVH credentials missing")
config := NewDefaultConfig()
config.APIEndpoint = apiEndpoint
config.ApplicationKey = applicationKey
config.ApplicationSecret = applicationSecret
config.ConsumerKey = consumerKey
return NewDNSProviderConfig(config)
}
ovhClient, err := ovh.NewClient(
apiEndpoint,
applicationKey,
applicationSecret,
consumerKey,
// NewDNSProviderConfig return a DNSProvider instance configured for OVH.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ovh: the configuration of the DNS provider is nil")
}
if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
return nil, fmt.Errorf("ovh: credentials missing")
}
client, err := ovh.NewClient(
config.APIEndpoint,
config.ApplicationKey,
config.ApplicationSecret,
config.ConsumerKey,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("ovh: %v", err)
}
client.Client = config.HTTPClient
return &DNSProvider{
client: ovhClient,
config: config,
client: client,
recordIDs: make(map[string]int),
}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
// Parse domain name
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
return fmt.Errorf("ovh: could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
subDomain := d.extractRecordName(fqdn, authZone)
reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone)
reqData := txtRecordRequest{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: ttl}
reqData := txtRecordRequest{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: d.config.TTL}
var respData txtRecordResponse
// Create TXT record
err = d.client.Post(reqURL, reqData, &respData)
if err != nil {
return fmt.Errorf("error when call OVH api to add record: %v", err)
return fmt.Errorf("ovh: error when call api to add record: %v", err)
}
// Apply the change
reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone)
err = d.client.Post(reqURL, nil, nil)
if err != nil {
return fmt.Errorf("error when call OVH api to refresh zone: %v", err)
return fmt.Errorf("ovh: error when call api to refresh zone: %v", err)
}
d.recordIDsMu.Lock()
@ -113,12 +160,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
recordID, ok := d.recordIDs[fqdn]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("unknown record ID for '%s'", fqdn)
return fmt.Errorf("ovh: unknown record ID for '%s'", fqdn)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
return fmt.Errorf("ovh: could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
@ -127,7 +174,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
err = d.client.Delete(reqURL, nil)
if err != nil {
return fmt.Errorf("error when call OVH api to delete challenge record: %v", err)
return fmt.Errorf("ovh: error when call OVH api to delete challenge record: %v", err)
}
// Delete record ID from map
@ -138,6 +185,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {

View file

@ -59,7 +59,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
"OVH_APPLICATION_SECRET": "5678",
"OVH_CONSUMER_KEY": "abcde",
},
expected: "OVH: some credentials information are missing: OVH_ENDPOINT",
expected: "ovh: some credentials information are missing: OVH_ENDPOINT",
},
{
desc: "missing OVH_APPLICATION_KEY",
@ -69,7 +69,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
"OVH_APPLICATION_SECRET": "5678",
"OVH_CONSUMER_KEY": "abcde",
},
expected: "OVH: some credentials information are missing: OVH_APPLICATION_KEY",
expected: "ovh: some credentials information are missing: OVH_APPLICATION_KEY",
},
{
desc: "missing OVH_APPLICATION_SECRET",
@ -79,7 +79,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
"OVH_APPLICATION_SECRET": "",
"OVH_CONSUMER_KEY": "abcde",
},
expected: "OVH: some credentials information are missing: OVH_APPLICATION_SECRET",
expected: "ovh: some credentials information are missing: OVH_APPLICATION_SECRET",
},
{
desc: "missing OVH_CONSUMER_KEY",
@ -89,7 +89,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
"OVH_APPLICATION_SECRET": "5678",
"OVH_CONSUMER_KEY": "",
},
expected: "OVH: some credentials information are missing: OVH_CONSUMER_KEY",
expected: "ovh: some credentials information are missing: OVH_CONSUMER_KEY",
},
{
desc: "all missing",
@ -99,7 +99,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
"OVH_APPLICATION_SECRET": "",
"OVH_CONSUMER_KEY": "",
},
expected: "OVH: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY",
expected: "ovh: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY",
},
}

View file

@ -5,6 +5,7 @@ package pdns
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -18,12 +19,32 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
Host *url.URL
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("PDNS_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("PDNS_PROPAGATION_TIMEOUT", 120*time.Second),
PollingInterval: env.GetOrDefaultSecond("PDNS_POLLING_INTERVAL", 2*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("PDNS_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
apiKey string
host *url.URL
apiVersion int
client *http.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for pdns.
@ -32,37 +53,51 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("PDNS_API_KEY", "PDNS_API_URL")
if err != nil {
return nil, fmt.Errorf("PDNS: %v", err)
return nil, fmt.Errorf("pdns: %v", err)
}
hostURL, err := url.Parse(values["PDNS_API_URL"])
if err != nil {
return nil, fmt.Errorf("PDNS: %v", err)
return nil, fmt.Errorf("pdns: %v", err)
}
return NewDNSProviderCredentials(hostURL, values["PDNS_API_KEY"])
config := NewDefaultConfig()
config.Host = hostURL
config.APIKey = values["PDNS_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for pdns.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for pdns.
// Deprecated
func NewDNSProviderCredentials(host *url.URL, key string) (*DNSProvider, error) {
if key == "" {
return nil, fmt.Errorf("PDNS API key missing")
config := NewDefaultConfig()
config.Host = host
config.APIKey = key
return NewDNSProviderConfig(config)
}
if host == nil || host.Host == "" {
return nil, fmt.Errorf("PDNS API URL missing")
// NewDNSProviderConfig return a DNSProvider instance configured for pdns.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("pdns: the configuration of the DNS provider is nil")
}
d := &DNSProvider{
host: host,
apiKey: key,
client: &http.Client{Timeout: 30 * time.Second},
if config.APIKey == "" {
return nil, fmt.Errorf("pdns: API key missing")
}
if config.Host == nil || config.Host.Host == "" {
return nil, fmt.Errorf("pdns: API URL missing")
}
d := &DNSProvider{config: config}
apiVersion, err := d.getAPIVersion()
if err != nil {
log.Warnf("PDNS: failed to get API version %v", err)
log.Warnf("pdns: failed to get API version %v", err)
}
d.apiVersion = apiVersion
@ -72,7 +107,7 @@ func NewDNSProviderCredentials(host *url.URL, key string) (*DNSProvider, error)
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 120 * time.Second, 2 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
@ -80,7 +115,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZone(fqdn)
if err != nil {
return err
return fmt.Errorf("pdns: %v", err)
}
name := fqdn
@ -97,7 +132,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// pre-v1 API
Type: "TXT",
Name: name,
TTL: 120,
TTL: d.config.TTL,
}
rrsets := rrSets{
@ -107,7 +142,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
ChangeType: "REPLACE",
Type: "TXT",
Kind: "Master",
TTL: 120,
TTL: d.config.TTL,
Records: []pdnsRecord{rec},
},
},
@ -115,11 +150,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
body, err := json.Marshal(rrsets)
if err != nil {
return err
return fmt.Errorf("pdns: %v", err)
}
_, err = d.makeRequest(http.MethodPatch, zone.URL, bytes.NewReader(body))
return err
if err != nil {
return fmt.Errorf("pdns: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
@ -128,12 +166,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
zone, err := d.getHostedZone(fqdn)
if err != nil {
return err
return fmt.Errorf("pdns: %v", err)
}
set, err := d.findTxtRecord(fqdn)
if err != nil {
return err
return fmt.Errorf("pdns: %v", err)
}
rrsets := rrSets{
@ -147,11 +185,14 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
body, err := json.Marshal(rrsets)
if err != nil {
return err
return fmt.Errorf("pdns: %v", err)
}
_, err = d.makeRequest(http.MethodPatch, zone.URL, bytes.NewReader(body))
return err
if err != nil {
return fmt.Errorf("pdns: %v", err)
}
return nil
}
func (d *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
@ -161,8 +202,8 @@ func (d *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
return nil, err
}
url := "/servers/localhost/zones"
result, err := d.makeRequest(http.MethodGet, url, nil)
u := "/servers/localhost/zones"
result, err := d.makeRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
@ -173,14 +214,14 @@ func (d *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
return nil, err
}
url = ""
u = ""
for _, zone := range zones {
if acme.UnFqdn(zone.Name) == acme.UnFqdn(authZone) {
url = zone.URL
u = zone.URL
}
}
result, err = d.makeRequest(http.MethodGet, url, nil)
result, err = d.makeRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
@ -259,8 +300,8 @@ func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
}
var path = ""
if d.host.Path != "/" {
path = d.host.Path
if d.config.Host.Path != "/" {
path = d.config.Host.Path
}
if !strings.HasPrefix(uri, "/") {
@ -271,15 +312,15 @@ func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
uri = "/api/v" + strconv.Itoa(d.apiVersion) + uri
}
url := d.host.Scheme + "://" + d.host.Host + path + uri
req, err := http.NewRequest(method, url, body)
u := d.config.Host.Scheme + "://" + d.config.Host.Host + path + uri
req, err := http.NewRequest(method, u, body)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", d.apiKey)
req.Header.Set("X-API-Key", d.config.APIKey)
resp, err := d.client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error talking to PDNS API -> %v", err)
}
@ -287,7 +328,7 @@ func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnprocessableEntity && (resp.StatusCode < 200 || resp.StatusCode >= 300) {
return nil, fmt.Errorf("unexpected HTTP status code %d when fetching '%s'", resp.StatusCode, url)
return nil, fmt.Errorf("unexpected HTTP status code %d when fetching '%s'", resp.StatusCode, u)
}
var msg json.RawMessage

View file

@ -37,7 +37,12 @@ func TestNewDNSProviderValid(t *testing.T) {
os.Setenv("PDNS_API_KEY", "")
tmpURL, _ := url.Parse("http://localhost:8081")
_, err := NewDNSProviderCredentials(tmpURL, "123")
config := NewDefaultConfig()
config.Host = tmpURL
config.APIKey = "123"
_, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
}
@ -56,7 +61,7 @@ func TestNewDNSProviderMissingHostErr(t *testing.T) {
os.Setenv("PDNS_API_KEY", "123")
_, err := NewDNSProvider()
assert.EqualError(t, err, "PDNS: some credentials information are missing: PDNS_API_URL")
assert.EqualError(t, err, "pdns: some credentials information are missing: PDNS_API_URL")
}
func TestNewDNSProviderMissingKeyErr(t *testing.T) {
@ -65,7 +70,7 @@ func TestNewDNSProviderMissingKeyErr(t *testing.T) {
os.Setenv("PDNS_API_KEY", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "PDNS: some credentials information are missing: PDNS_API_KEY,PDNS_API_URL")
assert.EqualError(t, err, "pdns: some credentials information are missing: PDNS_API_KEY,PDNS_API_URL")
}
func TestPdnsPresentAndCleanup(t *testing.T) {
@ -73,7 +78,11 @@ func TestPdnsPresentAndCleanup(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(pdnsURL, pdnsAPIKey)
config := NewDefaultConfig()
config.Host = pdnsURL
config.APIKey = pdnsAPIKey
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(pdnsDomain, "", "123d==")

View file

@ -0,0 +1,47 @@
package rackspace
// APIKeyCredentials API credential
type APIKeyCredentials struct {
Username string `json:"username"`
APIKey string `json:"apiKey"`
}
// Auth auth credentials
type Auth struct {
APIKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials"`
}
// AuthData Auth data
type AuthData struct {
Auth `json:"auth"`
}
// Identity Identity
type Identity struct {
Access struct {
ServiceCatalog []struct {
Endpoints []struct {
PublicURL string `json:"publicURL"`
TenantID string `json:"tenantId"`
} `json:"endpoints"`
Name string `json:"name"`
} `json:"serviceCatalog"`
Token struct {
ID string `json:"id"`
} `json:"token"`
} `json:"access"`
}
// Records is the list of records sent/received from the DNS API
type Records struct {
Record []Record `json:"records"`
}
// Record represents a Rackspace DNS record
type Record struct {
Name string `json:"name"`
Type string `json:"type"`
Data string `json:"data"`
TTL int `json:"ttl,omitempty"`
ID string `json:"id,omitempty"`
}

View file

@ -5,6 +5,7 @@ package rackspace
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -14,42 +15,85 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// rackspaceAPIURL represents the Identity API endpoint to call
var rackspaceAPIURL = "https://identity.api.rackspacecloud.com/v2.0/tokens"
// defaultBaseURL represents the Identity API endpoint to call
const defaultBaseURL = "https://identity.api.rackspacecloud.com/v2.0/tokens"
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIUser string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
BaseURL: defaultBaseURL,
TTL: env.GetOrDefaultInt("RACKSPACE_TTL", 300),
PropagationTimeout: env.GetOrDefaultSecond("RACKSPACE_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("RACKSPACE_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("RACKSPACE_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// used to store the reusable token and DNS API endpoint
type DNSProvider struct {
config *Config
token string
cloudDNSEndpoint string
client *http.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Rackspace.
// Credentials must be passed in the environment variables: RACKSPACE_USER
// and RACKSPACE_API_KEY.
// Credentials must be passed in the environment variables:
// RACKSPACE_USER and RACKSPACE_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("RACKSPACE_USER", "RACKSPACE_API_KEY")
if err != nil {
return nil, fmt.Errorf("Rackspace: %v", err)
return nil, fmt.Errorf("rackspace: %v", err)
}
return NewDNSProviderCredentials(values["RACKSPACE_USER"], values["RACKSPACE_API_KEY"])
config := NewDefaultConfig()
config.APIUser = values["RACKSPACE_USER"]
config.APIKey = values["RACKSPACE_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Rackspace. It authenticates against
// the API, also grabbing the DNS Endpoint.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Rackspace.
// It authenticates against the API, also grabbing the DNS Endpoint.
// Deprecated
func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) {
if user == "" || key == "" {
return nil, fmt.Errorf("Rackspace credentials missing")
config := NewDefaultConfig()
config.APIUser = user
config.APIKey = key
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Rackspace.
// It authenticates against the API, also grabbing the DNS Endpoint.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("rackspace: the configuration of the DNS provider is nil")
}
if config.APIUser == "" || config.APIKey == "" {
return nil, fmt.Errorf("rackspace: credentials missing")
}
authData := AuthData{
Auth: Auth{
APIKeyCredentials: APIKeyCredentials{
Username: user,
APIKey: key,
Username: config.APIUser,
APIKey: config.APIKey,
},
},
}
@ -59,46 +103,47 @@ func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, rackspaceAPIURL, bytes.NewReader(body))
req, err := http.NewRequest(http.MethodPost, config.BaseURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
// client := &http.Client{Timeout: 30 * time.Second}
resp, err := config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error querying Rackspace Identity API: %v", err)
return nil, fmt.Errorf("rackspace: error querying Identity API: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Rackspace Authentication failed. Response code: %d", resp.StatusCode)
return nil, fmt.Errorf("rackspace: authentication failed: response code: %d", resp.StatusCode)
}
var rackspaceIdentity Identity
err = json.NewDecoder(resp.Body).Decode(&rackspaceIdentity)
var identity Identity
err = json.NewDecoder(resp.Body).Decode(&identity)
if err != nil {
return nil, err
return nil, fmt.Errorf("rackspace: %v", err)
}
// Iterate through the Service Catalog to get the DNS Endpoint
var dnsEndpoint string
for _, service := range rackspaceIdentity.Access.ServiceCatalog {
for _, service := range identity.Access.ServiceCatalog {
if service.Name == "cloudDNS" {
dnsEndpoint = service.Endpoints[0].PublicURL
break
}
}
if dnsEndpoint == "" {
return nil, fmt.Errorf("failed to populate DNS endpoint, check Rackspace API for changes")
return nil, fmt.Errorf("rackspace: failed to populate DNS endpoint, check Rackspace API for changes")
}
return &DNSProvider{
token: rackspaceIdentity.Access.Token.ID,
config: config,
token: identity.Access.Token.ID,
cloudDNSEndpoint: dnsEndpoint,
client: client,
}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge
@ -106,7 +151,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneID, err := d.getHostedZoneID(fqdn)
if err != nil {
return err
return fmt.Errorf("rackspace: %v", err)
}
rec := Records{
@ -114,17 +159,20 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
Name: acme.UnFqdn(fqdn),
Type: "TXT",
Data: value,
TTL: 300,
TTL: d.config.TTL,
}},
}
body, err := json.Marshal(rec)
if err != nil {
return err
return fmt.Errorf("rackspace: %v", err)
}
_, err = d.makeRequest(http.MethodPost, fmt.Sprintf("/domains/%d/records", zoneID), bytes.NewReader(body))
return err
if err != nil {
return fmt.Errorf("rackspace: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
@ -132,16 +180,25 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
zoneID, err := d.getHostedZoneID(fqdn)
if err != nil {
return err
return fmt.Errorf("rackspace: %v", err)
}
record, err := d.findTxtRecord(fqdn, zoneID)
if err != nil {
return err
return fmt.Errorf("rackspace: %v", err)
}
_, err = d.makeRequest(http.MethodDelete, fmt.Sprintf("/domains/%d/records?id=%s", zoneID, record.ID), nil)
return err
if err != nil {
return fmt.Errorf("rackspace: %v", err)
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// getHostedZoneID performs a lookup to get the DNS zone which needs
@ -216,8 +273,7 @@ func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
req.Header.Set("X-Auth-Token", d.token)
req.Header.Set("Content-Type", "application/json")
client := http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error querying DNS API: %v", err)
}
@ -236,49 +292,3 @@ func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
return r, nil
}
// APIKeyCredentials API credential
type APIKeyCredentials struct {
Username string `json:"username"`
APIKey string `json:"apiKey"`
}
// Auth auth credentials
type Auth struct {
APIKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials"`
}
// AuthData Auth data
type AuthData struct {
Auth `json:"auth"`
}
// Identity Identity
type Identity struct {
Access struct {
ServiceCatalog []struct {
Endpoints []struct {
PublicURL string `json:"publicURL"`
TenantID string `json:"tenantId"`
} `json:"endpoints"`
Name string `json:"name"`
} `json:"serviceCatalog"`
Token struct {
ID string `json:"id"`
} `json:"token"`
} `json:"access"`
}
// Records is the list of records sent/received from the DNS API
type Records struct {
Record []Record `json:"records"`
}
// Record represents a Rackspace DNS record
type Record struct {
Name string `json:"name"`
Type string `json:"type"`
Data string `json:"data"`
TTL int `json:"ttl,omitempty"`
ID string `json:"id,omitempty"`
}

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
@ -31,13 +32,11 @@ func init() {
}
func testRackspaceEnv() {
rackspaceAPIURL = testAPIURL
os.Setenv("RACKSPACE_USER", "testUser")
os.Setenv("RACKSPACE_API_KEY", "testKey")
}
func liveRackspaceEnv() {
rackspaceAPIURL = "https://identity.api.rackspacecloud.com/v2.0/tokens"
os.Setenv("RACKSPACE_USER", rackspaceUser)
os.Setenv("RACKSPACE_API_KEY", rackspaceAPIKey)
}
@ -134,31 +133,50 @@ func dnsMux() *http.ServeMux {
func TestNewDNSProviderMissingCredErr(t *testing.T) {
testRackspaceEnv()
_, err := NewDNSProviderCredentials("", "")
assert.EqualError(t, err, "Rackspace credentials missing")
_, err := NewDNSProviderConfig(&Config{})
assert.EqualError(t, err, "rackspace: credentials missing")
}
func TestOfflineRackspaceValid(t *testing.T) {
testRackspaceEnv()
provider, err := NewDNSProviderCredentials(os.Getenv("RACKSPACE_USER"), os.Getenv("RACKSPACE_API_KEY"))
assert.NoError(t, err)
config := NewDefaultConfig()
config.BaseURL = testAPIURL
config.APIKey = os.Getenv("RACKSPACE_API_KEY")
config.APIUser = os.Getenv("RACKSPACE_USER")
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
assert.Equal(t, provider.token, "testToken", "The token should match")
}
func TestOfflineRackspacePresent(t *testing.T) {
testRackspaceEnv()
provider, err := NewDNSProvider()
config := NewDefaultConfig()
config.APIUser = os.Getenv("RACKSPACE_USER")
config.APIKey = os.Getenv("RACKSPACE_API_KEY")
config.BaseURL = testAPIURL
provider, err := NewDNSProviderConfig(config)
if assert.NoError(t, err) {
err = provider.Present("example.com", "token", "keyAuth")
assert.NoError(t, err)
require.NoError(t, err)
}
}
func TestOfflineRackspaceCleanUp(t *testing.T) {
testRackspaceEnv()
provider, err := NewDNSProvider()
config := NewDefaultConfig()
config.APIUser = os.Getenv("RACKSPACE_USER")
config.APIKey = os.Getenv("RACKSPACE_API_KEY")
config.BaseURL = testAPIURL
provider, err := NewDNSProviderConfig(config)
if assert.NoError(t, err) {
err = provider.CleanUp("example.com", "token", "keyAuth")

View file

@ -3,6 +3,7 @@
package rfc2136
import (
"errors"
"fmt"
"net"
"os"
@ -11,16 +12,37 @@ import (
"github.com/miekg/dns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const defaultTimeout = 60 * time.Second
// Config is used to configure the creation of the DNSProvider
type Config struct {
Nameserver string
TSIGAlgorithm string
TSIGKey string
TSIGSecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TSIGAlgorithm: env.GetOrDefaultString("RFC2136_TSIG_ALGORITHM", dns.HmacMD5),
TTL: env.GetOrDefaultInt("RFC2136_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("RFC2136_PROPAGATION_TIMEOUT",
env.GetOrDefaultSecond("RFC2136_TIMEOUT", 60*time.Second)),
PollingInterval: env.GetOrDefaultSecond("RFC2136_POLLING_INTERVAL", 2*time.Second),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that
// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
type DNSProvider struct {
nameserver string
tsigAlgorithm string
tsigKey string
tsigSecret string
timeout time.Duration
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for rfc2136
@ -33,81 +55,110 @@ type DNSProvider struct {
// RFC2136_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s)
// To disable TSIG authentication, leave the RFC2136_TSIG* variables unset.
func NewDNSProvider() (*DNSProvider, error) {
nameserver := os.Getenv("RFC2136_NAMESERVER")
tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM")
tsigKey := os.Getenv("RFC2136_TSIG_KEY")
tsigSecret := os.Getenv("RFC2136_TSIG_SECRET")
timeout := os.Getenv("RFC2136_TIMEOUT")
return NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, timeout)
values, err := env.Get("RFC2136_NAMESERVER")
if err != nil {
return nil, fmt.Errorf("rfc2136: %v", err)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for rfc2136 dynamic update. To disable TSIG
// authentication, leave the TSIG parameters as empty strings.
config := NewDefaultConfig()
config.Nameserver = values["RFC2136_NAMESERVER"]
config.TSIGKey = os.Getenv("RFC2136_TSIG_KEY")
config.TSIGSecret = os.Getenv("RFC2136_TSIG_SECRET")
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for rfc2136 dynamic update.
// To disable TSIG authentication, leave the TSIG parameters as empty strings.
// nameserver must be a network address in the form "host" or "host:port".
func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, timeout string) (*DNSProvider, error) {
if nameserver == "" {
return nil, fmt.Errorf("RFC2136 nameserver missing")
}
// Deprecated
func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, rawTimeout string) (*DNSProvider, error) {
config := NewDefaultConfig()
config.Nameserver = nameserver
config.TSIGAlgorithm = tsigAlgorithm
config.TSIGKey = tsigKey
config.TSIGSecret = tsigSecret
// Append the default DNS port if none is specified.
if _, _, err := net.SplitHostPort(nameserver); err != nil {
if strings.Contains(err.Error(), "missing port") {
nameserver = net.JoinHostPort(nameserver, "53")
} else {
return nil, err
}
}
d := &DNSProvider{nameserver: nameserver}
if tsigAlgorithm == "" {
tsigAlgorithm = dns.HmacMD5
}
d.tsigAlgorithm = tsigAlgorithm
if len(tsigKey) > 0 && len(tsigSecret) > 0 {
d.tsigKey = tsigKey
d.tsigSecret = tsigSecret
}
if timeout == "" {
d.timeout = 60 * time.Second
} else {
t, err := time.ParseDuration(timeout)
timeout := defaultTimeout
if rawTimeout != "" {
t, err := time.ParseDuration(rawTimeout)
if err != nil {
return nil, err
} else if t < 0 {
return nil, fmt.Errorf("invalid/negative RFC2136_TIMEOUT: %v", timeout)
return nil, fmt.Errorf("rfc2136: invalid/negative RFC2136_TIMEOUT: %v", rawTimeout)
} else {
d.timeout = t
timeout = t
}
}
config.PropagationTimeout = timeout
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for rfc2136.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("rfc2136: the configuration of the DNS provider is nil")
}
if config.Nameserver == "" {
return nil, fmt.Errorf("rfc2136: nameserver missing")
}
if config.TSIGAlgorithm == "" {
config.TSIGAlgorithm = dns.HmacMD5
}
// Append the default DNS port if none is specified.
if _, _, err := net.SplitHostPort(config.Nameserver); err != nil {
if strings.Contains(err.Error(), "missing port") {
config.Nameserver = net.JoinHostPort(config.Nameserver, "53")
} else {
return nil, fmt.Errorf("rfc2136: %v", err)
}
}
return d, nil
if len(config.TSIGKey) == 0 && len(config.TSIGSecret) > 0 ||
len(config.TSIGKey) > 0 && len(config.TSIGSecret) == 0 {
config.TSIGKey = ""
config.TSIGSecret = ""
}
// Timeout Returns the timeout configured with RFC2136_TIMEOUT, or 60s.
return &DNSProvider{config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.timeout, 2 * time.Second
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
return d.changeRecord("INSERT", fqdn, value, ttl)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
err := d.changeRecord("INSERT", fqdn, value, d.config.TTL)
if err != nil {
return fmt.Errorf("rfc2136: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
return d.changeRecord("REMOVE", fqdn, value, ttl)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
err := d.changeRecord("REMOVE", fqdn, value, d.config.TTL)
if err != nil {
return fmt.Errorf("rfc2136: %v", err)
}
return nil
}
func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
// Find the zone for the given fqdn
zone, err := acme.FindZoneByFqdn(fqdn, []string{d.nameserver})
zone, err := acme.FindZoneByFqdn(fqdn, []string{d.config.Nameserver})
if err != nil {
return err
}
@ -135,14 +186,15 @@ func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
// Setup client
c := new(dns.Client)
c.SingleInflight = true
// TSIG authentication / msg signing
if len(d.tsigKey) > 0 && len(d.tsigSecret) > 0 {
m.SetTsig(dns.Fqdn(d.tsigKey), d.tsigAlgorithm, 300, time.Now().Unix())
c.TsigSecret = map[string]string{dns.Fqdn(d.tsigKey): d.tsigSecret}
if len(d.config.TSIGKey) > 0 && len(d.config.TSIGSecret) > 0 {
m.SetTsig(dns.Fqdn(d.config.TSIGKey), d.config.TSIGAlgorithm, 300, time.Now().Unix())
c.TsigSecret = map[string]string{dns.Fqdn(d.config.TSIGKey): d.config.TSIGSecret}
}
// Send the query
reply, _, err := c.Exchange(m, d.nameserver)
reply, _, err := c.Exchange(m, d.config.Nameserver)
if err != nil {
return fmt.Errorf("DNS update failed: %v", err)
}

View file

@ -59,7 +59,10 @@ func TestRFC2136ServerSuccess(t *testing.T) {
require.NoError(t, err, "Failed to start test server")
defer server.Shutdown()
provider, err := NewDNSProviderCredentials(addrstr, "", "", "", "")
config := NewDefaultConfig()
config.Nameserver = addrstr
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
err = provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth)
@ -75,7 +78,10 @@ func TestRFC2136ServerError(t *testing.T) {
require.NoError(t, err, "Failed to start test server")
defer server.Shutdown()
provider, err := NewDNSProviderCredentials(addrstr, "", "", "", "")
config := NewDefaultConfig()
config.Nameserver = addrstr
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
err = provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth)
@ -94,7 +100,12 @@ func TestRFC2136TsigClient(t *testing.T) {
require.NoError(t, err, "Failed to start test server")
defer server.Shutdown()
provider, err := NewDNSProviderCredentials(addrstr, "", rfc2136TestTsigKey, rfc2136TestTsigSecret, "")
config := NewDefaultConfig()
config.Nameserver = addrstr
config.TSIGKey = rfc2136TestTsigKey
config.TSIGSecret = rfc2136TestTsigSecret
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
err = provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth)
@ -121,7 +132,10 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) {
expect, err := m.Pack()
require.NoError(t, err, "error packing")
provider, err := NewDNSProviderCredentials(addrstr, "", "", "", "")
config := NewDefaultConfig()
config.Nameserver = addrstr
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
err = provider.Present(rfc2136TestDomain, "", "1234d==")

View file

@ -30,13 +30,11 @@ type Config struct {
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
propagationMins := env.GetOrDefaultInt("AWS_PROPAGATION_TIMEOUT", 2)
intervalSecs := env.GetOrDefaultInt("AWS_POLLING_INTERVAL", 4)
return &Config{
MaxRetries: env.GetOrDefaultInt("AWS_MAX_RETRIES", 5),
TTL: env.GetOrDefaultInt("AWS_TTL", 10),
PropagationTimeout: time.Second * time.Duration(propagationMins),
PollingInterval: time.Second * time.Duration(intervalSecs),
PropagationTimeout: env.GetOrDefaultSecond("AWS_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("AWS_POLLING_INTERVAL", 4*time.Second),
HostedZoneID: os.Getenv("AWS_HOSTED_ZONE_ID"),
}
}
@ -91,20 +89,20 @@ func NewDNSProvider() (*DNSProvider, error) {
// DNSProvider instance
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the Route53 DNS provider is nil")
return nil, errors.New("route53: the configuration of the Route53 DNS provider is nil")
}
r := customRetryer{}
r.NumMaxRetries = config.MaxRetries
sessionCfg := request.WithRetryer(aws.NewConfig(), r)
session, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg})
sess, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg})
if err != nil {
return nil, err
}
client := route53.New(session)
cl := route53.New(sess)
return &DNSProvider{
client: client,
client: cl,
config: config,
}, nil
}
@ -118,15 +116,23 @@ func (r *DNSProvider) Timeout() (timeout, interval time.Duration) {
// Present creates a TXT record using the specified parameters
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("UPSERT", fqdn, value, r.config.TTL)
err := r.changeRecord("UPSERT", fqdn, `"`+value+`"`, r.config.TTL)
if err != nil {
return fmt.Errorf("route53: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("DELETE", fqdn, value, r.config.TTL)
err := r.changeRecord("DELETE", fqdn, `"`+value+`"`, r.config.TTL)
if err != nil {
return fmt.Errorf("route53: %v", err)
}
return nil
}
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
@ -151,7 +157,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
resp, err := r.client.ChangeResourceRecordSets(reqParams)
if err != nil {
return fmt.Errorf("failed to change Route 53 record set: %v", err)
return fmt.Errorf("failed to change record set: %v", err)
}
statusID := resp.ChangeInfo.Id
@ -162,7 +168,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
}
resp, err := r.client.GetChange(reqParams)
if err != nil {
return false, fmt.Errorf("failed to query Route 53 change status: %v", err)
return false, fmt.Errorf("failed to query change status: %v", err)
}
if aws.StringValue(resp.ChangeInfo.Status) == route53.ChangeStatusInsync {
return true, nil
@ -200,7 +206,7 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
}
if len(hostedZoneID) == 0 {
return "", fmt.Errorf("zone %s not found in Route 53 for domain %s", authZone, fqdn)
return "", fmt.Errorf("zone %s not found for domain %s", authZone, fqdn)
}
if strings.HasPrefix(hostedZoneID, "/hostedzone/") {

View file

@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/sacloud/libsacloud/api"
"github.com/sacloud/libsacloud/sacloud"
@ -14,8 +15,27 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Token string
Secret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("SAKURACLOUD_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("SAKURACLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("SAKURACLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *api.Client
}
@ -24,23 +44,42 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("SAKURACLOUD_ACCESS_TOKEN", "SAKURACLOUD_ACCESS_TOKEN_SECRET")
if err != nil {
return nil, fmt.Errorf("SakuraCloud: %v", err)
return nil, fmt.Errorf("sakuracloud: %v", err)
}
return NewDNSProviderCredentials(values["SAKURACLOUD_ACCESS_TOKEN"], values["SAKURACLOUD_ACCESS_TOKEN_SECRET"])
config := NewDefaultConfig()
config.Token = values["SAKURACLOUD_ACCESS_TOKEN"]
config.Secret = values["SAKURACLOUD_ACCESS_TOKEN_SECRET"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for sakuracloud.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for sakuracloud.
// Deprecated
func NewDNSProviderCredentials(token, secret string) (*DNSProvider, error) {
if token == "" {
return nil, errors.New("SakuraCloud AccessToken is missing")
}
if secret == "" {
return nil, errors.New("SakuraCloud AccessSecret is missing")
config := NewDefaultConfig()
config.Token = token
config.Secret = secret
return NewDNSProviderConfig(config)
}
client := api.NewClient(token, secret, "tk1a")
// NewDNSProviderConfig return a DNSProvider instance configured for GleSYS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("sakuracloud: the configuration of the DNS provider is nil")
}
if config.Token == "" {
return nil, errors.New("sakuracloud: AccessToken is missing")
}
if config.Secret == "" {
return nil, errors.New("sakuracloud: AccessSecret is missing")
}
client := api.NewClient(config.Token, config.Secret, "tk1a")
client.UserAgent = acme.UserAgent
return &DNSProvider{client: client}, nil
@ -48,19 +87,19 @@ func NewDNSProviderCredentials(token, secret string) (*DNSProvider, error) {
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("sakuracloud: %v", err)
}
name := d.extractRecordName(fqdn, zone.Name)
zone.AddRecord(zone.CreateNewRecord(name, "TXT", value, ttl))
zone.AddRecord(zone.CreateNewRecord(name, "TXT", value, d.config.TTL))
_, err = d.client.GetDNSAPI().Update(zone.ID, zone)
if err != nil {
return fmt.Errorf("SakuraCloud API call failed: %v", err)
return fmt.Errorf("sakuracloud: API call failed: %v", err)
}
return nil
@ -72,12 +111,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
zone, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("sakuracloud: %v", err)
}
records, err := d.findTxtRecords(fqdn, zone)
if err != nil {
return err
return fmt.Errorf("sakuracloud: %v", err)
}
for _, record := range records {
@ -92,12 +131,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
_, err = d.client.GetDNSAPI().Update(zone.ID, zone)
if err != nil {
return fmt.Errorf("SakuraCloud API call failed: %v", err)
return fmt.Errorf("sakuracloud: API call failed: %v", err)
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(domain string) (*sacloud.DNS, error) {
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
@ -111,7 +156,7 @@ func (d *DNSProvider) getHostedZone(domain string) (*sacloud.DNS, error) {
if notFound, ok := err.(api.Error); ok && notFound.ResponseCode() == http.StatusNotFound {
return nil, fmt.Errorf("zone %s not found on SakuraCloud DNS: %v", zoneName, err)
}
return nil, fmt.Errorf("SakuraCloud API call failed: %v", err)
return nil, fmt.Errorf("API call failed: %v", err)
}
for _, zone := range res.CommonServiceDNSItems {
@ -120,7 +165,7 @@ func (d *DNSProvider) getHostedZone(domain string) (*sacloud.DNS, error) {
}
}
return nil, fmt.Errorf("zone %s not found on SakuraCloud DNS", zoneName)
return nil, fmt.Errorf("zone %s not found", zoneName)
}
func (d *DNSProvider) findTxtRecords(fqdn string, zone *sacloud.DNS) ([]sacloud.DNSRecordSet, error) {

View file

@ -6,6 +6,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xenolf/lego/acme"
)
@ -54,7 +55,7 @@ func TestNewDNSProviderInvalidWithMissingAccessToken(t *testing.T) {
provider, err := NewDNSProvider()
assert.Nil(t, provider)
assert.EqualError(t, err, "SakuraCloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN,SAKURACLOUD_ACCESS_TOKEN_SECRET")
assert.EqualError(t, err, "sakuracloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN,SAKURACLOUD_ACCESS_TOKEN_SECRET")
}
//
@ -62,18 +63,23 @@ func TestNewDNSProviderInvalidWithMissingAccessToken(t *testing.T) {
//
func TestNewDNSProviderCredentialsValid(t *testing.T) {
provider, err := NewDNSProviderCredentials("123", "456")
config := NewDefaultConfig()
config.Token = "123"
config.Secret = "456"
provider, err := NewDNSProviderConfig(config)
require.NoError(t, err)
assert.NotNil(t, provider)
assert.Equal(t, acme.UserAgent, provider.client.UserAgent)
assert.NoError(t, err)
}
func TestNewDNSProviderCredentialsInvalidWithMissingAccessToken(t *testing.T) {
provider, err := NewDNSProviderCredentials("", "")
config := NewDefaultConfig()
provider, err := NewDNSProviderConfig(config)
assert.Nil(t, provider)
assert.EqualError(t, err, "SakuraCloud AccessToken is missing")
assert.EqualError(t, err, "sakuracloud: AccessToken is missing")
}
//
@ -85,7 +91,11 @@ func TestLiveSakuraCloudPresent(t *testing.T) {
t.Skip("skipping live test")
}
provider, err := NewDNSProviderCredentials(sakuracloudAccessToken, sakuracloudAccessSecret)
config := NewDefaultConfig()
config.Token = sakuracloudAccessToken
config.Secret = sakuracloudAccessSecret
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.Present(sakuracloudDomain, "", "123d==")
@ -103,7 +113,11 @@ func TestLiveSakuraCloudCleanUp(t *testing.T) {
time.Sleep(time.Second * 1)
provider, err := NewDNSProviderCredentials(sakuracloudAccessToken, sakuracloudAccessSecret)
config := NewDefaultConfig()
config.Token = sakuracloudAccessToken
config.Secret = sakuracloudAccessSecret
provider, err := NewDNSProviderConfig(config)
assert.NoError(t, err)
err = provider.CleanUp(sakuracloudDomain, "", "123d==")

View file

@ -3,6 +3,7 @@
package vegadns
import (
"errors"
"fmt"
"os"
"strings"
@ -13,8 +14,28 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
APISecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("VEGADNS_TTL", 10),
PropagationTimeout: env.GetOrDefaultSecond("VEGADNS_PROPAGATION_TIMEOUT", 12*time.Minute),
PollingInterval: env.GetOrDefaultSecond("VEGADNS_POLLING_INTERVAL", 1*time.Minute),
}
}
// DNSProvider describes a provider for VegaDNS
type DNSProvider struct {
config *Config
client vegaClient.VegaDNSClient
}
@ -24,62 +45,83 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("VEGADNS_URL")
if err != nil {
return nil, fmt.Errorf("VegaDNS: %v", err)
return nil, fmt.Errorf("vegadns: %v", err)
}
key := os.Getenv("SECRET_VEGADNS_KEY")
secret := os.Getenv("SECRET_VEGADNS_SECRET")
config := NewDefaultConfig()
config.BaseURL = values["VEGADNS_URL"]
config.APIKey = os.Getenv("SECRET_VEGADNS_KEY")
config.APISecret = os.Getenv("SECRET_VEGADNS_SECRET")
return NewDNSProviderCredentials(values["VEGADNS_URL"], key, secret)
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for VegaDNS.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for VegaDNS.
// Deprecated
func NewDNSProviderCredentials(vegaDNSURL string, key string, secret string) (*DNSProvider, error) {
vega := vegaClient.NewVegaDNSClient(vegaDNSURL)
vega.APIKey = key
vega.APISecret = secret
config := NewDefaultConfig()
config.BaseURL = vegaDNSURL
config.APIKey = key
config.APISecret = secret
return &DNSProvider{
client: vega,
}, nil
return NewDNSProviderConfig(config)
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (r *DNSProvider) Timeout() (timeout, interval time.Duration) {
timeout = 12 * time.Minute
interval = 1 * time.Minute
return
// NewDNSProviderConfig return a DNSProvider instance configured for VegaDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("vegadns: the configuration of the DNS provider is nil")
}
vega := vegaClient.NewVegaDNSClient(config.BaseURL)
vega.APIKey = config.APIKey
vega.APISecret = config.APISecret
return &DNSProvider{client: vega, config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
_, domainID, err := r.client.GetAuthZone(fqdn)
_, domainID, err := d.client.GetAuthZone(fqdn)
if err != nil {
return fmt.Errorf("can't find Authoritative Zone for %s in Present: %v", fqdn, err)
return fmt.Errorf("vegadns: can't find Authoritative Zone for %s in Present: %v", fqdn, err)
}
return r.client.CreateTXT(domainID, fqdn, value, 10)
err = d.client.CreateTXT(domainID, fqdn, value, d.config.TTL)
if err != nil {
return fmt.Errorf("vegadns: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
_, domainID, err := r.client.GetAuthZone(fqdn)
_, domainID, err := d.client.GetAuthZone(fqdn)
if err != nil {
return fmt.Errorf("can't find Authoritative Zone for %s in CleanUp: %v", fqdn, err)
return fmt.Errorf("vegadns: can't find Authoritative Zone for %s in CleanUp: %v", fqdn, err)
}
txt := strings.TrimSuffix(fqdn, ".")
recordID, err := r.client.GetRecordID(domainID, txt, "TXT")
recordID, err := d.client.GetRecordID(domainID, txt, "TXT")
if err != nil {
return fmt.Errorf("couldn't get Record ID in CleanUp: %s", err)
return fmt.Errorf("vegadns: couldn't get Record ID in CleanUp: %s", err)
}
return r.client.DeleteRecord(recordID)
err = d.client.DeleteRecord(recordID)
if err != nil {
return fmt.Errorf("vegadns: %v", err)
}
return nil
}

View file

@ -147,7 +147,7 @@ func TestVegaDNSPresentFailToFindZone(t *testing.T) {
require.NoError(t, err)
err = provider.Present("example.com", "token", "keyAuth")
assert.EqualError(t, err, "can't find Authoritative Zone for _acme-challenge.example.com. in Present: Unable to find auth zone for fqdn _acme-challenge.example.com")
assert.EqualError(t, err, "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in Present: Unable to find auth zone for fqdn _acme-challenge.example.com")
}
func TestVegaDNSPresentFailToCreateTXT(t *testing.T) {
@ -161,7 +161,7 @@ func TestVegaDNSPresentFailToCreateTXT(t *testing.T) {
require.NoError(t, err)
err = provider.Present("example.com", "token", "keyAuth")
assert.EqualError(t, err, "Got bad answer from VegaDNS on CreateTXT. Code: 400. Message: ")
assert.EqualError(t, err, "vegadns: Got bad answer from VegaDNS on CreateTXT. Code: 400. Message: ")
}
func TestVegaDNSCleanUpSuccess(t *testing.T) {
@ -189,7 +189,7 @@ func TestVegaDNSCleanUpFailToFindZone(t *testing.T) {
require.NoError(t, err)
err = provider.CleanUp("example.com", "token", "keyAuth")
assert.EqualError(t, err, "can't find Authoritative Zone for _acme-challenge.example.com. in CleanUp: Unable to find auth zone for fqdn _acme-challenge.example.com")
assert.EqualError(t, err, "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in CleanUp: Unable to find auth zone for fqdn _acme-challenge.example.com")
}
func TestVegaDNSCleanUpFailToGetRecordID(t *testing.T) {
@ -203,7 +203,7 @@ func TestVegaDNSCleanUpFailToGetRecordID(t *testing.T) {
require.NoError(t, err)
err = provider.CleanUp("example.com", "token", "keyAuth")
assert.EqualError(t, err, "couldn't get Record ID in CleanUp: Got bad answer from VegaDNS on GetRecordID. Code: 404. Message: ")
assert.EqualError(t, err, "vegadns: couldn't get Record ID in CleanUp: Got bad answer from VegaDNS on GetRecordID. Code: 404. Message: ")
}
func vegaDNSMuxSuccess() *http.ServeMux {

View file

@ -4,16 +4,46 @@
package vultr
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"strings"
"time"
vultr "github.com/JamesClonk/vultr/lib"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("VULTR_TTL", 120),
PropagationTimeout: env.GetOrDefaultSecond("VULTR_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("VULTR_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("VULTR_HTTP_TIMEOUT", 0),
// from Vultr Client
Transport: &http.Transport{
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
},
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *vultr.Client
}
@ -22,36 +52,58 @@ type DNSProvider struct {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("VULTR_API_KEY")
if err != nil {
return nil, fmt.Errorf("Vultr: %v", err)
return nil, fmt.Errorf("vultr: %v", err)
}
return NewDNSProviderCredentials(values["VULTR_API_KEY"])
config := NewDefaultConfig()
config.APIKey = values["VULTR_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderCredentials uses the supplied credentials to return a DNSProvider
// instance configured for Vultr.
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Vultr.
// Deprecated
func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
if apiKey == "" {
return nil, fmt.Errorf("Vultr credentials missing")
config := NewDefaultConfig()
config.APIKey = apiKey
return NewDNSProviderConfig(config)
}
return &DNSProvider{client: vultr.NewClient(apiKey, nil)}, nil
// NewDNSProviderConfig return a DNSProvider instance configured for Vultr.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("vultr: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, fmt.Errorf("vultr: credentials missing")
}
options := &vultr.Options{
HTTPClient: config.HTTPClient,
UserAgent: acme.UserAgent,
}
client := vultr.NewClient(config.APIKey, options)
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the DNS-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneDomain, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("vultr: %v", err)
}
name := d.extractRecordName(fqdn, zoneDomain)
err = d.client.CreateDNSRecord(zoneDomain, name, "TXT", `"`+value+`"`, 0, ttl)
err = d.client.CreateDNSRecord(zoneDomain, name, "TXT", `"`+value+`"`, 0, d.config.TTL)
if err != nil {
return fmt.Errorf("Vultr API call failed: %v", err)
return fmt.Errorf("vultr: API call failed: %v", err)
}
return nil
@ -63,22 +115,34 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
zoneDomain, records, err := d.findTxtRecords(domain, fqdn)
if err != nil {
return err
return fmt.Errorf("vultr: %v", err)
}
var allErr []string
for _, rec := range records {
err := d.client.DeleteDNSRecord(zoneDomain, rec.RecordID)
if err != nil {
return err
allErr = append(allErr, err.Error())
}
}
if len(allErr) > 0 {
return errors.New(strings.Join(allErr, ": "))
}
return nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(domain string) (string, error) {
domains, err := d.client.GetDNSDomains()
if err != nil {
return "", fmt.Errorf("Vultr API call failed: %v", err)
return "", fmt.Errorf("API call failed: %v", err)
}
var hostedDomain vultr.DNSDomain
@ -90,7 +154,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) {
}
}
if hostedDomain.Domain == "" {
return "", fmt.Errorf("No matching Vultr domain found for domain %s", domain)
return "", fmt.Errorf("no matching Vultr domain found for domain %s", domain)
}
return hostedDomain.Domain, nil
@ -105,7 +169,7 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) (string, []vultr.DNSRe
var records []vultr.DNSRecord
result, err := d.client.GetDNSRecords(zoneDomain)
if err != nil {
return "", records, fmt.Errorf("Vultr API call has failed: %v", err)
return "", records, fmt.Errorf("API call has failed: %v", err)
}
recordName := d.extractRecordName(fqdn, zoneDomain)

View file

@ -37,7 +37,7 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
os.Setenv("VULTR_API_KEY", "")
_, err := NewDNSProvider()
assert.EqualError(t, err, "Vultr: some credentials information are missing: VULTR_API_KEY")
assert.EqualError(t, err, "vultr: some credentials information are missing: VULTR_API_KEY")
}
func TestLivePresent(t *testing.T) {