Skip to content

Commit f204946

Browse files
committed
Add DNS provider for Abion
1 parent a628db5 commit f204946

File tree

10 files changed

+979
-0
lines changed

10 files changed

+979
-0
lines changed

providers/dns/abion/abion.go

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Package abion implements a DNS provider for solving the DNS-01 challenge using Abion.
2+
package abion
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/abion/internal"
15+
)
16+
17+
// Environment variables names.
18+
const (
19+
envNamespace = "ABION_"
20+
21+
EnvAPIKey = envNamespace + "API_KEY"
22+
23+
EnvTTL = envNamespace + "TTL"
24+
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
25+
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
26+
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
27+
)
28+
29+
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
30+
31+
// Config is used to configure the creation of the DNSProvider.
32+
type Config struct {
33+
APIKey string
34+
PropagationTimeout time.Duration
35+
PollingInterval time.Duration
36+
TTL int
37+
HTTPClient *http.Client
38+
}
39+
40+
// NewDefaultConfig returns a default configuration for the DNSProvider.
41+
func NewDefaultConfig() *Config {
42+
return &Config{
43+
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
44+
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
45+
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
46+
HTTPClient: &http.Client{
47+
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
48+
},
49+
}
50+
}
51+
52+
// DNSProvider implements the challenge.Provider interface.
53+
type DNSProvider struct {
54+
config *Config
55+
client *internal.Client
56+
}
57+
58+
// NewDNSProvider returns a DNSProvider instance configured for Abion.
59+
// Credentials must be passed in the environment variable: ABION_API_KEY.
60+
func NewDNSProvider() (*DNSProvider, error) {
61+
values, err := env.Get(EnvAPIKey)
62+
if err != nil {
63+
return nil, fmt.Errorf("abion: %w", err)
64+
}
65+
66+
config := NewDefaultConfig()
67+
config.APIKey = values[EnvAPIKey]
68+
69+
return NewDNSProviderConfig(config)
70+
}
71+
72+
// NewDNSProviderConfig return a DNSProvider instance configured for Abion.
73+
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
74+
if config == nil {
75+
return nil, errors.New("abion: the configuration of the DNS provider is nil")
76+
}
77+
78+
if config.APIKey == "" {
79+
return nil, errors.New("abion: credentials missing")
80+
}
81+
82+
client := internal.NewClient(config.APIKey)
83+
84+
if config.HTTPClient != nil {
85+
client.HTTPClient = config.HTTPClient
86+
}
87+
88+
return &DNSProvider{
89+
config: config,
90+
client: client,
91+
}, nil
92+
}
93+
94+
// Timeout returns the timeout and interval to use when checking for DNS propagation.
95+
// Adjusting here to cope with spikes in propagation times.
96+
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
97+
return d.config.PropagationTimeout, d.config.PollingInterval
98+
}
99+
100+
// Present creates a TXT record to fulfill the dns-01 challenge.
101+
func (d *DNSProvider) Present(domain, _, keyAuth string) error {
102+
ctx := context.Background()
103+
info := dns01.GetChallengeInfo(domain, keyAuth)
104+
105+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
106+
if err != nil {
107+
return fmt.Errorf("abion: could not find zone for domain %q: %w", domain, err)
108+
}
109+
110+
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
111+
if err != nil {
112+
return fmt.Errorf("abion: %w", err)
113+
}
114+
115+
zones, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone))
116+
if err != nil {
117+
return fmt.Errorf("abion: get zone %w", err)
118+
}
119+
120+
var data []internal.Record
121+
if sub, ok := zones.Data.Attributes.Records[subDomain]; ok {
122+
if records, exist := sub["TXT"]; exist {
123+
data = append(data, records...)
124+
}
125+
}
126+
127+
data = append(data, internal.Record{
128+
TTL: d.config.TTL,
129+
Data: info.Value,
130+
Comments: "lego",
131+
})
132+
133+
patch := internal.ZoneRequest{
134+
Data: internal.Zone{
135+
Type: "zone",
136+
ID: dns01.UnFqdn(authZone),
137+
Attributes: internal.Attributes{
138+
Records: map[string]map[string][]internal.Record{
139+
subDomain: {"TXT": data},
140+
},
141+
},
142+
},
143+
}
144+
145+
_, err = d.client.UpdateZone(ctx, dns01.UnFqdn(authZone), patch)
146+
if err != nil {
147+
return fmt.Errorf("abion: update zone %w", err)
148+
}
149+
150+
return nil
151+
}
152+
153+
// CleanUp removes the TXT record matching the specified parameters.
154+
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
155+
ctx := context.Background()
156+
info := dns01.GetChallengeInfo(domain, keyAuth)
157+
158+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
159+
if err != nil {
160+
return fmt.Errorf("abion: could not find zone for domain %q: %w", domain, err)
161+
}
162+
163+
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
164+
if err != nil {
165+
return fmt.Errorf("abion: %w", err)
166+
}
167+
168+
zones, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone))
169+
if err != nil {
170+
return fmt.Errorf("abion: get zone %w", err)
171+
}
172+
173+
var data []internal.Record
174+
if sub, ok := zones.Data.Attributes.Records[subDomain]; ok {
175+
if records, exist := sub["TXT"]; exist {
176+
for _, record := range records {
177+
if record.Data != info.Value {
178+
data = append(data, record)
179+
}
180+
}
181+
}
182+
}
183+
184+
payload := map[string][]internal.Record{}
185+
if len(data) == 0 {
186+
payload["TXT"] = nil
187+
} else {
188+
payload["TXT"] = data
189+
}
190+
191+
patch := internal.ZoneRequest{
192+
Data: internal.Zone{
193+
Type: "zone",
194+
ID: dns01.UnFqdn(authZone),
195+
Attributes: internal.Attributes{
196+
Records: map[string]map[string][]internal.Record{
197+
subDomain: payload,
198+
},
199+
},
200+
},
201+
}
202+
203+
_, err = d.client.UpdateZone(ctx, dns01.UnFqdn(authZone), patch)
204+
if err != nil {
205+
return fmt.Errorf("abion: update zone %w", err)
206+
}
207+
208+
return nil
209+
}

providers/dns/abion/abion.toml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Name = "Abion"
2+
Description = ''''''
3+
URL = "https://abion.com"
4+
Code = "abion"
5+
Since = "v4.21.0"
6+
7+
Example = '''
8+
ABION_API_KEY="xxxxxxxxxxxx" \
9+
lego --email [email protected] --dns abion -d '*.example.com' -d example.com run
10+
'''
11+
12+
[Configuration]
13+
[Configuration.Credentials]
14+
ABION_API_KEY = "API key"
15+
[Configuration.Additional]
16+
ABION_POLLING_INTERVAL = "Time between DNS propagation check"
17+
ABION_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
18+
ABION_TTL = "The TTL of the TXT record used for the DNS challenge"
19+
ABION_HTTP_TIMEOUT = "API request timeout"
20+
21+
[Links]
22+
API = "https://demo.abion.com/pmapi-doc/openapi-ui/index.html"

providers/dns/abion/abion_test.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package abion
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: "123",
24+
},
25+
},
26+
{
27+
desc: "missing credentials",
28+
envVars: map[string]string{
29+
EnvAPIKey: "",
30+
},
31+
expected: "abion: some credentials information are missing: ABION_API_KEY",
32+
},
33+
}
34+
35+
for _, test := range testCases {
36+
t.Run(test.desc, func(t *testing.T) {
37+
defer envTest.RestoreEnv()
38+
envTest.ClearEnv()
39+
40+
envTest.Apply(test.envVars)
41+
42+
p, err := NewDNSProvider()
43+
44+
if test.expected == "" {
45+
require.NoError(t, err)
46+
require.NotNil(t, p)
47+
require.NotNil(t, p.config)
48+
} else {
49+
require.EqualError(t, err, test.expected)
50+
}
51+
})
52+
}
53+
}
54+
55+
func TestNewDNSProviderConfig(t *testing.T) {
56+
testCases := []struct {
57+
desc string
58+
apiKey string
59+
ttl int
60+
expected string
61+
}{
62+
{
63+
desc: "success",
64+
apiKey: "123",
65+
},
66+
{
67+
desc: "missing credentials",
68+
expected: "abion: credentials missing",
69+
},
70+
}
71+
72+
for _, test := range testCases {
73+
t.Run(test.desc, func(t *testing.T) {
74+
config := NewDefaultConfig()
75+
config.APIKey = test.apiKey
76+
config.TTL = test.ttl
77+
78+
p, err := NewDNSProviderConfig(config)
79+
80+
if test.expected == "" {
81+
require.NoError(t, err)
82+
require.NotNil(t, p)
83+
require.NotNil(t, p.config)
84+
} else {
85+
require.EqualError(t, err, test.expected)
86+
}
87+
})
88+
}
89+
}
90+
91+
func TestLivePresent(t *testing.T) {
92+
if !envTest.IsLiveTest() {
93+
t.Skip("skipping live test")
94+
}
95+
96+
envTest.RestoreEnv()
97+
provider, err := NewDNSProvider()
98+
require.NoError(t, err)
99+
100+
err = provider.Present(envTest.GetDomain(), "", "123d==")
101+
require.NoError(t, err)
102+
}
103+
104+
func TestLiveCleanUp(t *testing.T) {
105+
if !envTest.IsLiveTest() {
106+
t.Skip("skipping live test")
107+
}
108+
109+
envTest.RestoreEnv()
110+
provider, err := NewDNSProvider()
111+
require.NoError(t, err)
112+
113+
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
114+
require.NoError(t, err)
115+
}

0 commit comments

Comments
 (0)