forked from TrueCloudLab/lego
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:
parent
ad34a85dad
commit
bba134ce87
82 changed files with 4800 additions and 2521 deletions
2
Gopkg.lock
generated
2
Gopkg.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
34
platform/config/env/env.go
vendored
34
platform/config/env/env.go
vendored
|
@ -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
|
||||
}
|
||||
|
|
124
platform/config/env/env_test.go
vendored
124
platform/config/env/env_test.go
vendored
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
if len(regionID) == 0 {
|
||||
regionID = defaultRegionID
|
||||
if config.APIKey == "" || config.SecretKey == "" {
|
||||
return nil, fmt.Errorf("alicloud: credentials missing")
|
||||
}
|
||||
|
||||
client, err := alidns.NewClientWithAccessKey(regionID, apiKey, secretKey)
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
client, err := auroradnsclient.NewAuroraDNSClient(baseURL, userID, key)
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
16
providers/dns/bluecat/client.go
Normal file
16
providers/dns/bluecat/client.go
Normal 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"`
|
||||
}
|
212
providers/dns/cloudflare/client.go
Normal file
212
providers/dns/cloudflare/client.go
Normal 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)
|
||||
}
|
188
providers/dns/cloudflare/client_test.go
Normal file
188
providers/dns/cloudflare/client_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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==")
|
||||
|
|
26
providers/dns/digitalocean/client.go
Normal file
26
providers/dns/digitalocean/client.go
Normal 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"`
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var digitalOceanBaseURL = "https://api.digitalocean.com"
|
||||
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)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(accessToken))
|
||||
client.UserAgent = "lego"
|
||||
if config.AccessToken == "" {
|
||||
return nil, fmt.Errorf("dnsimple: OAuth token is missing")
|
||||
}
|
||||
|
||||
if baseURL != "" {
|
||||
client.BaseURL = baseURL
|
||||
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
|
||||
|
|
|
@ -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==")
|
||||
|
|
168
providers/dns/dnsmadeeasy/client.go
Normal file
168
providers/dns/dnsmadeeasy/client.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
if config.BaseURL == "" {
|
||||
return nil, fmt.Errorf("dnsmadeeasy: base URL missing")
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 10 * time.Second,
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
params := dnspod.CommonParams{LoginToken: key, Format: "json"}
|
||||
return &DNSProvider{
|
||||
client: dnspod.NewClient(params),
|
||||
}, 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)
|
||||
|
|
|
@ -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==")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
return &DNSProvider{token: token}, 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
|
||||
|
|
|
@ -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) {
|
||||
|
|
35
providers/dns/dyn/client.go
Normal file
35
providers/dns/dyn/client.go
Normal 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"`
|
||||
}
|
|
@ -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 &DNSProvider{
|
||||
customerName: customerName,
|
||||
userName: userName,
|
||||
password: password,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
}, nil
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) {
|
||||
url := fmt.Sprintf("%s/%s", dynBaseURL, resource)
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 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")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if config.CustomerName == "" || config.UserName == "" || config.Password == "" {
|
||||
return nil, fmt.Errorf("dyn: credentials missing")
|
||||
}
|
||||
|
||||
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 fmt.Errorf("dyn: %v", err)
|
||||
}
|
||||
|
||||
err = d.login()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
endpoint = "https://api.exoscale.ch/dns"
|
||||
if config.APIKey == "" || config.APISecret == "" {
|
||||
return nil, fmt.Errorf("exoscale: credentials missing")
|
||||
}
|
||||
|
||||
return &DNSProvider{
|
||||
client: egoscale.NewClient(endpoint, key, secret),
|
||||
}, nil
|
||||
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) {
|
||||
|
|
|
@ -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==")
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderClient uses the supplied parameters to return a DNSProvider instance
|
||||
// configured for FastDNS.
|
||||
// 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 {
|
||||
|
|
|
@ -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==")
|
||||
|
|
94
providers/dns/gandi/client.go
Normal file
94
providers/dns/gandi/client.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
18
providers/dns/gandiv5/client.go
Normal file
18
providers/dns/gandiv5/client.go
Normal 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"`
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// types for JSON responses
|
||||
|
||||
type responseStruct struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
return nil, fmt.Errorf("googlecloud: 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
|
||||
|
||||
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.
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
return &DNSProvider{
|
||||
project: project,
|
||||
client: svc,
|
||||
}, 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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
24
providers/dns/glesys/client.go
Normal file
24
providers/dns/glesys/client.go
Normal 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"`
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
return &DNSProvider{
|
||||
apiKey: apiKey,
|
||||
apiSecret: apiSecret,
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
}, 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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
44
providers/dns/namecheap/client.go
Normal file
44
providers/dns/namecheap/client.go
Normal 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"`
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
if config.APIUser == "" || config.APIKey == "" {
|
||||
return nil, fmt.Errorf("namecheap: credentials missing")
|
||||
}
|
||||
|
||||
clientIP, err := getClientIP(client)
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// apierror describes an error record in a namecheap API response.
|
||||
type apierror struct {
|
||||
Number int `xml:",attr"`
|
||||
Description string `xml:",innerxml"`
|
||||
// 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) (addr string, err error) {
|
||||
// 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, >r); 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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
client := namecom.New(username, apiToken)
|
||||
|
||||
if server != "" {
|
||||
client.Server = server
|
||||
if config.Username == "" {
|
||||
return nil, fmt.Errorf("namedotcom: username is required")
|
||||
}
|
||||
|
||||
return &DNSProvider{client: client}, nil
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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==")
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
|
||||
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
return NewDNSProviderCredentials(httpClient, endpoint, values["NIFCLOUD_ACCESS_KEY_ID"], values["NIFCLOUD_SECRET_ACCESS_KEY"])
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Timeout: time.Second * 10}
|
||||
client := rest.NewClient(httpClient, rest.SetAPIKey(key))
|
||||
if config.APIKey == "" {
|
||||
return nil, fmt.Errorf("ns1: credentials missing")
|
||||
}
|
||||
|
||||
return &DNSProvider{client}, nil
|
||||
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
|
||||
|
|
|
@ -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==")
|
||||
|
|
68
providers/dns/otc/client.go
Normal file
68
providers/dns/otc/client.go
Normal 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"`
|
||||
}
|
|
@ -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
|
||||
|
||||
if identityEndpoint == "" {
|
||||
identityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens"
|
||||
}
|
||||
|
||||
return &DNSProvider{
|
||||
identityEndpoint: identityEndpoint,
|
||||
domainName: domainName,
|
||||
userName: userName,
|
||||
password: password,
|
||||
projectName: projectName,
|
||||
}, nil
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// SendRequest send request
|
||||
func (d *DNSProvider) SendRequest(method, resource string, payload interface{}) (io.Reader, error) {
|
||||
url := fmt.Sprintf("%s/%s", d.otcBaseURL, resource)
|
||||
// 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")
|
||||
}
|
||||
|
||||
if config.DomainName == "" || config.UserName == "" || config.Password == "" || config.ProjectName == "" {
|
||||
return nil, fmt.Errorf("otc: credentials missing")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
ovhClient, err := ovh.NewClient(
|
||||
apiEndpoint,
|
||||
applicationKey,
|
||||
applicationSecret,
|
||||
consumerKey,
|
||||
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 {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
if host == nil || host.Host == "" {
|
||||
return nil, fmt.Errorf("PDNS API URL missing")
|
||||
if config.APIKey == "" {
|
||||
return nil, fmt.Errorf("pdns: API key missing")
|
||||
}
|
||||
|
||||
d := &DNSProvider{
|
||||
host: host,
|
||||
apiKey: key,
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
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
|
||||
|
|
|
@ -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==")
|
||||
|
|
47
providers/dns/rackspace/client.go
Normal file
47
providers/dns/rackspace/client.go
Normal 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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
values, err := env.Get("RFC2136_NAMESERVER")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rfc2136: %v", err)
|
||||
}
|
||||
|
||||
return NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, timeout)
|
||||
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.
|
||||
// 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 d, nil
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// Timeout Returns the timeout configured with RFC2136_TIMEOUT, or 60s.
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.TSIGKey) == 0 && len(config.TSIGSecret) > 0 ||
|
||||
len(config.TSIGKey) > 0 && len(config.TSIGSecret) == 0 {
|
||||
config.TSIGKey = ""
|
||||
config.TSIGSecret = ""
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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==")
|
||||
|
|
|
@ -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/") {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
client := api.NewClient(token, secret, "tk1a")
|
||||
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) {
|
||||
|
|
|
@ -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==")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
return &DNSProvider{client: vultr.NewClient(apiKey, nil)}, 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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue