Skip to content

Commit c51978d

Browse files
committed
Add DNS provider for hosting.nl
1 parent 83ff393 commit c51978d

11 files changed

+610
-0
lines changed

providers/dns/dns_providers.go

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import (
5757
"github.com/go-acme/lego/v4/providers/dns/googledomains"
5858
"github.com/go-acme/lego/v4/providers/dns/hetzner"
5959
"github.com/go-acme/lego/v4/providers/dns/hostingde"
60+
"github.com/go-acme/lego/v4/providers/dns/hostingnl"
6061
"github.com/go-acme/lego/v4/providers/dns/hosttech"
6162
"github.com/go-acme/lego/v4/providers/dns/httpreq"
6263
"github.com/go-acme/lego/v4/providers/dns/hurricane"
@@ -240,6 +241,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
240241
return hetzner.NewDNSProvider()
241242
case "hostingde":
242243
return hostingde.NewDNSProvider()
244+
case "hostingnl":
245+
return hostingnl.NewDNSProvider()
243246
case "hosttech":
244247
return hosttech.NewDNSProvider()
245248
case "httpreq":

providers/dns/hostingnl/hostingnl.go

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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/dns01"
14+
"github.com/go-acme/lego/v4/platform/config/env"
15+
"github.com/go-acme/lego/v4/providers/dns/hostingnl/internal"
16+
)
17+
18+
// Environment variables names.
19+
const (
20+
envNamespace = "HOSTINGNL_"
21+
22+
EnvAPIKey = envNamespace + "API_KEY"
23+
24+
EnvTTL = envNamespace + "TTL"
25+
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
26+
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
27+
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
28+
)
29+
30+
// Config is used to configure the creation of the DNSProvider.
31+
type Config struct {
32+
APIKey string
33+
HTTPClient *http.Client
34+
PropagationTimeout time.Duration
35+
PollingInterval time.Duration
36+
TTL int
37+
}
38+
39+
// NewDefaultConfig returns a default configuration for the DNSProvider.
40+
func NewDefaultConfig() *Config {
41+
return &Config{
42+
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
43+
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
44+
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
45+
HTTPClient: &http.Client{
46+
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
47+
},
48+
}
49+
}
50+
51+
// DNSProvider implements the challenge.Provider interface.
52+
type DNSProvider struct {
53+
config *Config
54+
client *internal.Client
55+
56+
recordIDs map[string]string
57+
recordIDsMu sync.Mutex
58+
}
59+
60+
// NewDNSProvider returns a DNSProvider instance configured for hosting.nl.
61+
// Credentials must be passed in the environment variables:
62+
// HOSTINGNL_APIKEY.
63+
func NewDNSProvider() (*DNSProvider, error) {
64+
values, err := env.Get(EnvAPIKey)
65+
if err != nil {
66+
return nil, fmt.Errorf("hostingnl: %w", err)
67+
}
68+
69+
config := NewDefaultConfig()
70+
config.APIKey = values[EnvAPIKey]
71+
72+
return NewDNSProviderConfig(config)
73+
}
74+
75+
// NewDNSProviderConfig return a DNSProvider instance configured for hosting.nl.
76+
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
77+
if config == nil {
78+
return nil, errors.New("hostingnl: the configuration of the DNS provider is nil")
79+
}
80+
81+
if config.APIKey == "" {
82+
return nil, errors.New("hostingnl: APIKey is missing")
83+
}
84+
85+
client := internal.NewClient(config.APIKey)
86+
87+
if config.HTTPClient != nil {
88+
client.HTTPClient = config.HTTPClient
89+
}
90+
91+
return &DNSProvider{
92+
config: config,
93+
client: client,
94+
recordIDs: make(map[string]string),
95+
}, nil
96+
}
97+
98+
// Present creates a TXT record using the specified parameters.
99+
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
100+
info := dns01.GetChallengeInfo(domain, keyAuth)
101+
102+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
103+
if err != nil {
104+
return fmt.Errorf("hostingnl: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err)
105+
}
106+
107+
record := internal.Record{
108+
Name: info.EffectiveFQDN,
109+
Type: "TXT",
110+
Content: strconv.Quote(info.Value),
111+
TTL: strconv.Itoa(d.config.TTL),
112+
Priority: "0",
113+
}
114+
115+
newRecord, err := d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record)
116+
if err != nil {
117+
return fmt.Errorf("hostingnl: failed to create TXT record, fqdn=%s: %w", info.EffectiveFQDN, err)
118+
}
119+
120+
d.recordIDsMu.Lock()
121+
d.recordIDs[token] = newRecord.ID
122+
d.recordIDsMu.Unlock()
123+
124+
return nil
125+
}
126+
127+
// CleanUp removes the TXT records matching the specified parameters.
128+
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
129+
info := dns01.GetChallengeInfo(domain, keyAuth)
130+
131+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
132+
if err != nil {
133+
return fmt.Errorf("hostingnl: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err)
134+
}
135+
136+
// gets the record's unique ID
137+
d.recordIDsMu.Lock()
138+
recordID, ok := d.recordIDs[token]
139+
d.recordIDsMu.Unlock()
140+
if !ok {
141+
return fmt.Errorf("hostingnl: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
142+
}
143+
144+
err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)
145+
if err != nil {
146+
return fmt.Errorf("hostingnl: failed to delete TXT record, id=%s: %w", recordID, err)
147+
}
148+
149+
// deletes record ID from map
150+
d.recordIDsMu.Lock()
151+
delete(d.recordIDs, token)
152+
d.recordIDsMu.Unlock()
153+
154+
return nil
155+
}
156+
157+
// Timeout returns the timeout and interval to use when checking for DNS propagation.
158+
// Adjusting here to cope with spikes in propagation times.
159+
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
160+
return d.config.PropagationTimeout, d.config.PollingInterval
161+
}
+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.16.0"
6+
7+
Example = '''
8+
HOSTINGNL_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
9+
lego --email [email protected] --dns hostingnl --domains my.example.org 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)