Skip to content

Commit f264a1c

Browse files
committed
Add DNS provider for BookMyName
1 parent a628db5 commit f264a1c

File tree

9 files changed

+547
-0
lines changed

9 files changed

+547
-0
lines changed
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Package bookmyname implements a DNS provider for solving the DNS-01 challenge using BookMyName.
2+
package bookmyname
3+
4+
import (
5+
"context"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"time"
10+
11+
"github.com/go-acme/lego/v4/challenge"
12+
"github.com/go-acme/lego/v4/challenge/dns01"
13+
"github.com/go-acme/lego/v4/platform/config/env"
14+
"github.com/go-acme/lego/v4/providers/dns/bookmyname/internal"
15+
)
16+
17+
// Environment variables names.
18+
const (
19+
envNamespace = "BOOKMYNAME_"
20+
21+
EnvUsername = envNamespace + "USERNAME"
22+
EnvPassword = envNamespace + "PASSWORD"
23+
24+
EnvTTL = envNamespace + "TTL"
25+
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
26+
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
27+
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
28+
)
29+
30+
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
31+
32+
// Config is used to configure the creation of the DNSProvider.
33+
type Config struct {
34+
Username string
35+
Password string
36+
37+
PropagationTimeout time.Duration
38+
PollingInterval time.Duration
39+
TTL int
40+
HTTPClient *http.Client
41+
}
42+
43+
// NewDefaultConfig returns a default configuration for the DNSProvider.
44+
func NewDefaultConfig() *Config {
45+
return &Config{
46+
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
47+
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
48+
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
49+
HTTPClient: &http.Client{
50+
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
51+
},
52+
}
53+
}
54+
55+
// DNSProvider implements the challenge.Provider interface.
56+
type DNSProvider struct {
57+
config *Config
58+
client *internal.Client
59+
}
60+
61+
// NewDNSProvider returns a DNSProvider instance configured for BookMyName.
62+
func NewDNSProvider() (*DNSProvider, error) {
63+
values, err := env.Get(EnvUsername, EnvPassword)
64+
if err != nil {
65+
return nil, fmt.Errorf("bookmyname: %w", err)
66+
}
67+
68+
config := NewDefaultConfig()
69+
config.Username = values[EnvUsername]
70+
config.Password = values[EnvPassword]
71+
72+
return NewDNSProviderConfig(config)
73+
}
74+
75+
// NewDNSProviderConfig return a DNSProvider instance configured for BookMyName.
76+
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
77+
if config == nil {
78+
return nil, errors.New("bookmyname: the configuration of the DNS provider is nil")
79+
}
80+
81+
client, err := internal.NewClient(config.Username, config.Password)
82+
if err != nil {
83+
return nil, fmt.Errorf("bookmyname: %w", err)
84+
}
85+
86+
if config.HTTPClient != nil {
87+
client.HTTPClient = config.HTTPClient
88+
}
89+
90+
return &DNSProvider{
91+
config: config,
92+
client: client,
93+
}, nil
94+
}
95+
96+
// Present creates a TXT record using the specified parameters.
97+
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
98+
info := dns01.GetChallengeInfo(domain, keyAuth)
99+
100+
record := internal.Record{
101+
Hostname: info.EffectiveFQDN,
102+
Type: "txt",
103+
TTL: d.config.TTL,
104+
Value: info.Value,
105+
}
106+
107+
err := d.client.AddRecord(context.Background(), record)
108+
if err != nil {
109+
return fmt.Errorf("bookmyname: add record: %w", err)
110+
}
111+
112+
return nil
113+
}
114+
115+
// CleanUp removes the TXT record matching the specified parameters.
116+
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
117+
info := dns01.GetChallengeInfo(domain, keyAuth)
118+
119+
record := internal.Record{
120+
Hostname: info.EffectiveFQDN,
121+
Type: "txt",
122+
TTL: d.config.TTL,
123+
Value: info.Value,
124+
}
125+
126+
err := d.client.RemoveRecord(context.Background(), record)
127+
if err != nil {
128+
return fmt.Errorf("bookmyname: add record: %w", err)
129+
}
130+
131+
return nil
132+
}
133+
134+
// Timeout returns the timeout and interval to use when checking for DNS propagation.
135+
// Adjusting here to cope with spikes in propagation times.
136+
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
137+
return d.config.PropagationTimeout, d.config.PollingInterval
138+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Name = "BookMyName"
2+
Description = ''''''
3+
URL = "https://www.bookmyname.com/"
4+
Code = "bookmyname"
5+
Since = "v4.21.0"
6+
7+
Example = '''
8+
BOOKMYNAME_USERNAME="xxx" \
9+
BOOKMYNAME_PASSWORD="yyy" \
10+
lego --email [email protected] --dns bookmyname -d '*.example.com' -d example.com run
11+
'''
12+
13+
[Configuration]
14+
[Configuration.Credentials]
15+
BOOKMYNAME_USERNAME = "Username"
16+
BOOKMYNAME_PASSWORD = "Password"
17+
[Configuration.Additional]
18+
BOOKMYNAME_POLLING_INTERVAL = "Time between DNS propagation check"
19+
BOOKMYNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
20+
BOOKMYNAME_TTL = "The TTL of the TXT record used for the DNS challenge"
21+
BOOKMYNAME_HTTP_TIMEOUT = "API request timeout"
22+
23+
[Links]
24+
API = "https://fr.faqs.bookmyname.com/frfaqs/dyndns"
+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package bookmyname
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(EnvUsername, EnvPassword).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+
EnvUsername: "user",
24+
EnvPassword: "secret",
25+
},
26+
},
27+
{
28+
desc: "missing username",
29+
envVars: map[string]string{
30+
EnvUsername: "",
31+
EnvPassword: "secret",
32+
},
33+
expected: "bookmyname: some credentials information are missing: BOOKMYNAME_USERNAME",
34+
},
35+
{
36+
desc: "missing paswword",
37+
envVars: map[string]string{
38+
EnvUsername: "user",
39+
EnvPassword: "",
40+
},
41+
expected: "bookmyname: some credentials information are missing: BOOKMYNAME_PASSWORD",
42+
},
43+
{
44+
desc: "missing credentials",
45+
envVars: map[string]string{},
46+
expected: "bookmyname: some credentials information are missing: BOOKMYNAME_USERNAME,BOOKMYNAME_PASSWORD",
47+
},
48+
}
49+
50+
for _, test := range testCases {
51+
t.Run(test.desc, func(t *testing.T) {
52+
defer envTest.RestoreEnv()
53+
envTest.ClearEnv()
54+
55+
envTest.Apply(test.envVars)
56+
57+
p, err := NewDNSProvider()
58+
59+
if test.expected == "" {
60+
require.NoError(t, err)
61+
require.NotNil(t, p)
62+
require.NotNil(t, p.config)
63+
require.NotNil(t, p.client)
64+
} else {
65+
require.EqualError(t, err, test.expected)
66+
}
67+
})
68+
}
69+
}
70+
71+
func TestNewDNSProviderConfig(t *testing.T) {
72+
testCases := []struct {
73+
desc string
74+
username string
75+
password string
76+
expected string
77+
}{
78+
{
79+
desc: "success",
80+
username: "user",
81+
password: "secret",
82+
},
83+
{
84+
desc: "missing username",
85+
password: "secret",
86+
expected: "bookmyname: credentials missing",
87+
},
88+
{
89+
desc: "missing password",
90+
username: "user",
91+
expected: "bookmyname: credentials missing",
92+
},
93+
{
94+
desc: "missing credentials",
95+
expected: "bookmyname: credentials missing",
96+
},
97+
}
98+
99+
for _, test := range testCases {
100+
t.Run(test.desc, func(t *testing.T) {
101+
config := NewDefaultConfig()
102+
config.Username = test.username
103+
config.Password = test.password
104+
105+
p, err := NewDNSProviderConfig(config)
106+
107+
if test.expected == "" {
108+
require.NoError(t, err)
109+
require.NotNil(t, p)
110+
require.NotNil(t, p.config)
111+
require.NotNil(t, p.client)
112+
} else {
113+
require.EqualError(t, err, test.expected)
114+
}
115+
})
116+
}
117+
}
118+
119+
func TestLivePresent(t *testing.T) {
120+
if !envTest.IsLiveTest() {
121+
t.Skip("skipping live test")
122+
}
123+
124+
envTest.RestoreEnv()
125+
provider, err := NewDNSProvider()
126+
require.NoError(t, err)
127+
128+
err = provider.Present(envTest.GetDomain(), "", "123d==")
129+
require.NoError(t, err)
130+
}
131+
132+
func TestLiveCleanUp(t *testing.T) {
133+
if !envTest.IsLiveTest() {
134+
t.Skip("skipping live test")
135+
}
136+
137+
envTest.RestoreEnv()
138+
provider, err := NewDNSProvider()
139+
require.NoError(t, err)
140+
141+
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
142+
require.NoError(t, err)
143+
}

0 commit comments

Comments
 (0)