Skip to content

Commit bacde13

Browse files
committed
Add DNS provider for hosting.nl
1 parent b83c1d5 commit bacde13

11 files changed

+613
-0
lines changed

providers/dns/hostingnl/hostingnl.go

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Package hostingnl implements a DNS provider for solving the DNS-01 challenge using hosting.nl.
2+
package hostingnl
3+
4+
import (
5+
"context"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"strconv"
10+
"sync"
11+
"time"
12+
13+
"github.com/go-acme/lego/v4/challenge"
14+
"github.com/go-acme/lego/v4/challenge/dns01"
15+
"github.com/go-acme/lego/v4/platform/config/env"
16+
"github.com/go-acme/lego/v4/providers/dns/hostingnl/internal"
17+
)
18+
19+
// Environment variables names.
20+
const (
21+
envNamespace = "HOSTINGNL_"
22+
23+
EnvAPIKey = envNamespace + "API_KEY"
24+
25+
EnvTTL = envNamespace + "TTL"
26+
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
27+
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
28+
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
29+
)
30+
31+
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
32+
33+
// Config is used to configure the creation of the DNSProvider.
34+
type Config struct {
35+
APIKey string
36+
HTTPClient *http.Client
37+
PropagationTimeout time.Duration
38+
PollingInterval time.Duration
39+
TTL int
40+
}
41+
42+
// NewDefaultConfig returns a default configuration for the DNSProvider.
43+
func NewDefaultConfig() *Config {
44+
return &Config{
45+
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
46+
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
47+
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
48+
HTTPClient: &http.Client{
49+
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
50+
},
51+
}
52+
}
53+
54+
// DNSProvider implements the challenge.Provider interface.
55+
type DNSProvider struct {
56+
config *Config
57+
client *internal.Client
58+
59+
recordIDs map[string]string
60+
recordIDsMu sync.Mutex
61+
}
62+
63+
// NewDNSProvider returns a DNSProvider instance configured for hosting.nl.
64+
// Credentials must be passed in the environment variables:
65+
// HOSTINGNL_APIKEY.
66+
func NewDNSProvider() (*DNSProvider, error) {
67+
values, err := env.Get(EnvAPIKey)
68+
if err != nil {
69+
return nil, fmt.Errorf("hostingnl: %w", err)
70+
}
71+
72+
config := NewDefaultConfig()
73+
config.APIKey = values[EnvAPIKey]
74+
75+
return NewDNSProviderConfig(config)
76+
}
77+
78+
// NewDNSProviderConfig return a DNSProvider instance configured for hosting.nl.
79+
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
80+
if config == nil {
81+
return nil, errors.New("hostingnl: the configuration of the DNS provider is nil")
82+
}
83+
84+
if config.APIKey == "" {
85+
return nil, errors.New("hostingnl: APIKey is missing")
86+
}
87+
88+
client := internal.NewClient(config.APIKey)
89+
90+
if config.HTTPClient != nil {
91+
client.HTTPClient = config.HTTPClient
92+
}
93+
94+
return &DNSProvider{
95+
config: config,
96+
client: client,
97+
recordIDs: make(map[string]string),
98+
}, nil
99+
}
100+
101+
// Present creates a TXT record using the specified parameters.
102+
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
103+
info := dns01.GetChallengeInfo(domain, keyAuth)
104+
105+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
106+
if err != nil {
107+
return fmt.Errorf("hostingnl: could not find zone for domain %q: %w", domain, err)
108+
}
109+
110+
record := internal.Record{
111+
Name: info.EffectiveFQDN,
112+
Type: "TXT",
113+
Content: strconv.Quote(info.Value),
114+
TTL: strconv.Itoa(d.config.TTL),
115+
Priority: "0",
116+
}
117+
118+
newRecord, err := d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record)
119+
if err != nil {
120+
return fmt.Errorf("hostingnl: failed to create TXT record, fqdn=%s: %w", info.EffectiveFQDN, err)
121+
}
122+
123+
d.recordIDsMu.Lock()
124+
d.recordIDs[token] = newRecord.ID
125+
d.recordIDsMu.Unlock()
126+
127+
return nil
128+
}
129+
130+
// CleanUp removes the TXT records matching the specified parameters.
131+
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
132+
info := dns01.GetChallengeInfo(domain, keyAuth)
133+
134+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
135+
if err != nil {
136+
return fmt.Errorf("hostingnl: could not find zone for domain %q: %w", domain, err)
137+
}
138+
139+
// gets the record's unique ID
140+
d.recordIDsMu.Lock()
141+
recordID, ok := d.recordIDs[token]
142+
d.recordIDsMu.Unlock()
143+
if !ok {
144+
return fmt.Errorf("hostingnl: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
145+
}
146+
147+
err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)
148+
if err != nil {
149+
return fmt.Errorf("hostingnl: failed to delete TXT record, id=%s: %w", recordID, err)
150+
}
151+
152+
// deletes record ID from map
153+
d.recordIDsMu.Lock()
154+
delete(d.recordIDs, token)
155+
d.recordIDsMu.Unlock()
156+
157+
return nil
158+
}
159+
160+
// Timeout returns the timeout and interval to use when checking for DNS propagation.
161+
// Adjusting here to cope with spikes in propagation times.
162+
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
163+
return d.config.PropagationTimeout, d.config.PollingInterval
164+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Name = "Hosting.nl"
2+
Description = ''''''
3+
URL = "https://hosting.nl"
4+
Code = "hostingnl"
5+
Since = "v4.21.0"
6+
7+
Example = '''
8+
HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
9+
lego --email [email protected] --dns hostingnl -d '*.example.com' -d example.com run
10+
'''
11+
12+
[Configuration]
13+
[Configuration.Credentials]
14+
HOSTINGNL_API_KEY = "The API key"
15+
[Configuration.Additional]
16+
HOSTINGNL_POLLING_INTERVAL = "Time between DNS propagation check"
17+
HOSTINGNL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
18+
HOSTINGNL_TTL = "The TTL of the TXT record used for the DNS challenge"
19+
HOSTINGNL_HTTP_TIMEOUT = "API request timeout"
20+
21+
[Links]
22+
API = "https://api.hosting.nl/api/documentation"
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package hostingnl
2+
3+
import (
4+
"testing"
5+
6+
"github.com/go-acme/lego/v4/platform/tester"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
const envDomain = envNamespace + "DOMAIN"
11+
12+
var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)
13+
14+
func TestNewDNSProvider(t *testing.T) {
15+
testCases := []struct {
16+
desc string
17+
envVars map[string]string
18+
expected string
19+
}{
20+
{
21+
desc: "success",
22+
envVars: map[string]string{
23+
EnvAPIKey: "key",
24+
},
25+
},
26+
{
27+
desc: "missing API key",
28+
envVars: map[string]string{},
29+
expected: "hostingnl: some credentials information are missing: HOSTINGNL_API_KEY",
30+
},
31+
}
32+
33+
for _, test := range testCases {
34+
t.Run(test.desc, func(t *testing.T) {
35+
defer envTest.RestoreEnv()
36+
envTest.ClearEnv()
37+
38+
envTest.Apply(test.envVars)
39+
40+
p, err := NewDNSProvider()
41+
42+
if test.expected == "" {
43+
require.NoError(t, err)
44+
require.NotNil(t, p)
45+
require.NotNil(t, p.config)
46+
require.NotNil(t, p.client)
47+
} else {
48+
require.EqualError(t, err, test.expected)
49+
}
50+
})
51+
}
52+
}
53+
54+
func TestNewDNSProviderConfig(t *testing.T) {
55+
testCases := []struct {
56+
desc string
57+
apiKey string
58+
expected string
59+
}{
60+
{
61+
desc: "success",
62+
apiKey: "key",
63+
},
64+
{
65+
desc: "missing API key",
66+
expected: "hostingnl: APIKey is missing",
67+
},
68+
}
69+
70+
for _, test := range testCases {
71+
t.Run(test.desc, func(t *testing.T) {
72+
config := NewDefaultConfig()
73+
config.APIKey = test.apiKey
74+
75+
p, err := NewDNSProviderConfig(config)
76+
77+
if test.expected == "" {
78+
require.NoError(t, err)
79+
require.NotNil(t, p)
80+
require.NotNil(t, p.config)
81+
require.NotNil(t, p.client)
82+
} else {
83+
require.EqualError(t, err, test.expected)
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestLivePresent(t *testing.T) {
90+
if !envTest.IsLiveTest() {
91+
t.Skip("skipping live test")
92+
}
93+
94+
envTest.RestoreEnv()
95+
provider, err := NewDNSProvider()
96+
require.NoError(t, err)
97+
98+
err = provider.Present(envTest.GetDomain(), "", "123d==")
99+
require.NoError(t, err)
100+
}
101+
102+
func TestLiveCleanUp(t *testing.T) {
103+
if !envTest.IsLiveTest() {
104+
t.Skip("skipping live test")
105+
}
106+
107+
envTest.RestoreEnv()
108+
provider, err := NewDNSProvider()
109+
require.NoError(t, err)
110+
111+
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
112+
require.NoError(t, err)
113+
}

0 commit comments

Comments
 (0)