Skip to content

Commit ffe3a9c

Browse files
committed
Add DNS provider for Abion
1 parent f8db554 commit ffe3a9c

File tree

10 files changed

+976
-0
lines changed

10 files changed

+976
-0
lines changed

providers/dns/abion/abion.go

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

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.20.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)