From 95952e4ba81855a72cdce764bd42152442d3b699 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 13 Feb 2024 02:29:43 +0100 Subject: [PATCH 1/2] Add DNS provider for Abion --- providers/dns/abion/abion.go | 212 +++++++++++++++ providers/dns/abion/abion.toml | 22 ++ providers/dns/abion/abion_test.go | 118 ++++++++ providers/dns/abion/internal/client.go | 172 ++++++++++++ providers/dns/abion/internal/client_test.go | 255 ++++++++++++++++++ .../dns/abion/internal/fixtures/error.json | 9 + .../internal/fixtures/update-request.json | 23 ++ .../dns/abion/internal/fixtures/update.json | 45 ++++ .../dns/abion/internal/fixtures/zone.json | 45 ++++ .../dns/abion/internal/fixtures/zones.json | 22 ++ providers/dns/abion/internal/types.go | 73 +++++ 11 files changed, 996 insertions(+) create mode 100644 providers/dns/abion/abion.go create mode 100644 providers/dns/abion/abion.toml create mode 100644 providers/dns/abion/abion_test.go create mode 100644 providers/dns/abion/internal/client.go create mode 100644 providers/dns/abion/internal/client_test.go create mode 100644 providers/dns/abion/internal/fixtures/error.json create mode 100644 providers/dns/abion/internal/fixtures/update-request.json create mode 100644 providers/dns/abion/internal/fixtures/update.json create mode 100644 providers/dns/abion/internal/fixtures/zone.json create mode 100644 providers/dns/abion/internal/fixtures/zones.json create mode 100644 providers/dns/abion/internal/types.go diff --git a/providers/dns/abion/abion.go b/providers/dns/abion/abion.go new file mode 100644 index 0000000000..9e256f0966 --- /dev/null +++ b/providers/dns/abion/abion.go @@ -0,0 +1,212 @@ +// Package abion implements a DNS provider for solving the DNS-01 challenge using Abion. +package abion + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v5/challenge" + "github.com/go-acme/lego/v5/challenge/dns01" + "github.com/go-acme/lego/v5/internal/env" + "github.com/go-acme/lego/v5/providers/dns/abion/internal" + "github.com/go-acme/lego/v5/providers/dns/internal/clientdebug" +) + +// Environment variables names. +const ( + envNamespace = "ABION_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Abion. +// Credentials must be passed in the environment variable: ABION_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("abion: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Abion. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("abion: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("abion: credentials missing") + } + + client := internal.NewClient(config.APIKey) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + client.HTTPClient = clientdebug.Wrap(client.HTTPClient) + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(ctx context.Context, domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(ctx, domain, keyAuth) + + authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("abion: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("abion: %w", err) + } + + zones, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("abion: get zone %w", err) + } + + var data []internal.Record + + if sub, ok := zones.Data.Attributes.Records[subDomain]; ok { + if records, exist := sub["TXT"]; exist { + data = append(data, records...) + } + } + + data = append(data, internal.Record{ + TTL: d.config.TTL, + Data: info.Value, + Comments: "lego", + }) + + patch := internal.ZoneRequest{ + Data: internal.Zone{ + Type: "zone", + ID: dns01.UnFqdn(authZone), + Attributes: internal.Attributes{ + Records: map[string]map[string][]internal.Record{ + subDomain: {"TXT": data}, + }, + }, + }, + } + + _, err = d.client.UpdateZone(ctx, dns01.UnFqdn(authZone), patch) + if err != nil { + return fmt.Errorf("abion: update zone %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(ctx, domain, keyAuth) + + authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("abion: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("abion: %w", err) + } + + zones, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("abion: get zone %w", err) + } + + var data []internal.Record + + if sub, ok := zones.Data.Attributes.Records[subDomain]; ok { + if records, exist := sub["TXT"]; exist { + for _, record := range records { + if record.Data != info.Value { + data = append(data, record) + } + } + } + } + + payload := map[string][]internal.Record{} + if len(data) == 0 { + payload["TXT"] = nil + } else { + payload["TXT"] = data + } + + patch := internal.ZoneRequest{ + Data: internal.Zone{ + Type: "zone", + ID: dns01.UnFqdn(authZone), + Attributes: internal.Attributes{ + Records: map[string]map[string][]internal.Record{ + subDomain: payload, + }, + }, + }, + } + + _, err = d.client.UpdateZone(ctx, dns01.UnFqdn(authZone), patch) + if err != nil { + return fmt.Errorf("abion: update zone %w", err) + } + + return nil +} diff --git a/providers/dns/abion/abion.toml b/providers/dns/abion/abion.toml new file mode 100644 index 0000000000..fcb0d36688 --- /dev/null +++ b/providers/dns/abion/abion.toml @@ -0,0 +1,22 @@ +Name = "Abion" +Description = '''''' +URL = "https://abion.com" +Code = "abion" +Since = "v5.0.0" + +Example = ''' +ABION_API_KEY="xxxxxxxxxxxx" \ +lego --dns abion -d '*.example.com' -d example.com run +''' + +[Configuration] + [Configuration.Credentials] + ABION_API_KEY = "API key" + [Configuration.Additional] + ABION_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" + ABION_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" + ABION_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)" + ABION_HTTP_TIMEOUT = "API request timeout in seconds (Default: 10)" + +[Links] + API = "https://demo.abion.com/pmapi-doc/openapi-ui/index.html" diff --git a/providers/dns/abion/abion_test.go b/providers/dns/abion/abion_test.go new file mode 100644 index 0000000000..093e04feef --- /dev/null +++ b/providers/dns/abion/abion_test.go @@ -0,0 +1,118 @@ +package abion + +import ( + "testing" + + "github.com/go-acme/lego/v5/internal/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "123", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvAPIKey: "", + }, + expected: "abion: some credentials information are missing: ABION_API_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiKey string + ttl int + expected string + }{ + { + desc: "success", + apiKey: "123", + }, + { + desc: "missing credentials", + expected: "abion: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + config.TTL = test.ttl + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(t.Context(), envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(t.Context(), envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/abion/internal/client.go b/providers/dns/abion/internal/client.go new file mode 100644 index 0000000000..a5a1054024 --- /dev/null +++ b/providers/dns/abion/internal/client.go @@ -0,0 +1,172 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v5/internal/errutils" + "github.com/go-acme/lego/v5/internal/useragent" + querystring "github.com/google/go-querystring/query" +) + +// defaultBaseURL represents the API endpoint to call. +const defaultBaseURL = "https://api.abion.com" + +const apiKeyHeader = "X-API-KEY" + +// Client the Abion API client. +type Client struct { + apiKey string + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient Creates a new Client. +func NewClient(apiKey string) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + } +} + +// GetZones Lists all the zones your session can access. +func (c *Client) GetZones(ctx context.Context, page *Pagination) (*APIResponse[[]Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, err + } + + if page != nil { + v, errQ := querystring.Values(page) + if errQ != nil { + return nil, errQ + } + + req.URL.RawQuery = v.Encode() + } + + results := &APIResponse[[]Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not get zones: %w", err) + } + + return results, nil +} + +// GetZone Returns the full information on a single zone. +func (c *Client) GetZone(ctx context.Context, name string) (*APIResponse[*Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones", name) + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, err + } + + results := &APIResponse[*Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not get zone %s: %w", name, err) + } + + return results, nil +} + +// UpdateZone Updates a zone by patching it according to JSON Merge Patch format (RFC 7396). +func (c *Client) UpdateZone(ctx context.Context, name string, patch ZoneRequest) (*APIResponse[*Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones", name) + + req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, patch) + if err != nil { + return nil, err + } + + results := &APIResponse[*Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not update zone %s: %w", name, err) + } + + return results, nil +} + +func (c *Client) do(req *http.Request, result any) error { + useragent.SetHeader(req.Header) + + req.Header.Set(apiKeyHeader, c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + zResp := &APIResponse[any]{} + + err := json.Unmarshal(raw, zResp) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return zResp.Error +} diff --git a/providers/dns/abion/internal/client_test.go b/providers/dns/abion/internal/client_test.go new file mode 100644 index 0000000000..56f89f05ad --- /dev/null +++ b/providers/dns/abion/internal/client_test.go @@ -0,0 +1,255 @@ +package internal + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-acme/lego/v5/internal/tester/servermock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockBuilder() *servermock.Builder[*Client] { + return servermock.NewBuilder( + func(server *httptest.Server) (*Client, error) { + client := NewClient("secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client, nil + }, + servermock.CheckHeader(). + WithJSONHeaders(). + With(apiKeyHeader, "secret"), + ) +} + +func TestUpdateZone(t *testing.T) { + domain := "example.com" + + client := mockBuilder(). + Route("PATCH /v1/zones/"+domain, + servermock.ResponseFromFixture("update.json"), + servermock.CheckRequestJSONBodyFromFixture("update-request.json")). + Build(t) + + patch := ZoneRequest{ + Data: Zone{ + Type: "zone", + ID: domain, + Attributes: Attributes{ + Records: map[string]map[string][]Record{ + "_acme-challenge.test": { + "TXT": []Record{ + {Data: "test"}, + {Data: "test1"}, + {Data: "test2"}, + }, + }, + }, + }, + }, + } + + zone, err := client.UpdateZone(context.Background(), domain, patch) + require.NoError(t, err) + + expected := &APIResponse[*Zone]{ + Meta: &Metadata{ + InvocationID: "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + }, + Data: &Zone{ + Type: "zone", + ID: "dipcon.com", + Attributes: Attributes{ + OrganisationID: "10154", + OrganisationDescription: "My Company AB", + DNSTypeDescription: "Anycast", + Slave: false, + Pending: false, + Deleted: false, + Settings: &Settings{ + MName: "dns01.dipcon.com.", + Refresh: 3600, + Expire: 604800, + TTL: 600, + }, + Records: map[string]map[string][]Record{ + "@": { + "NS": { + { + TTL: 3600, + Data: "193.14.90.194", + Comments: "this is a comment", + }, + }, + }, + }, + Redirects: map[string][]Redirect{ + "": { + { + Path: "/x/y", + Destination: "https://abion.com/?ref=dipcon", + Status: 301, + Slugs: true, + Certificate: true, + }, + }, + }, + }, + }, + } + + assert.Equal(t, expected, zone) +} + +func TestUpdateZone_error(t *testing.T) { + domain := "example.com" + + client := mockBuilder(). + Route("PATCH /v1/zones/"+domain, + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + patch := ZoneRequest{ + Data: Zone{ + Type: "zone", + ID: domain, + Attributes: Attributes{ + Records: map[string]map[string][]Record{ + "_acme-challenge.test": { + "TXT": []Record{ + {Data: "test"}, + {Data: "test1"}, + {Data: "test2"}, + }, + }, + }, + }, + }, + } + + _, err := client.UpdateZone(context.Background(), domain, patch) + require.EqualError(t, err, "could not update zone example.com: api error: status=401, message=Authentication Error") +} + +func TestGetZones(t *testing.T) { + client := mockBuilder(). + Route("GET /v1/zones/", + servermock.ResponseFromFixture("zones.json")). + Build(t) + + zones, err := client.GetZones(context.Background(), nil) + require.NoError(t, err) + + expected := &APIResponse[[]Zone]{ + Meta: &Metadata{ + InvocationID: "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + Pagination: &Pagination{ + Offset: 0, + Limit: 1, + Total: 1, + }, + }, + Data: []Zone{ + { + Type: "zone", + ID: "dipcon.com", + Attributes: Attributes{ + OrganisationID: "10154", + OrganisationDescription: "My Company AB", + DNSTypeDescription: "Anycast", + Slave: true, + Pending: true, + Deleted: true, + }, + }, + }, + } + + assert.Equal(t, expected, zones) +} + +func TestGetZones_error(t *testing.T) { + client := mockBuilder(). + Route("GET /v1/zones/", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + _, err := client.GetZones(context.Background(), nil) + require.EqualError(t, err, "could not get zones: api error: status=401, message=Authentication Error") +} + +func TestGetZone(t *testing.T) { + client := mockBuilder(). + Route("GET /v1/zones/example.com", + servermock.ResponseFromFixture("zone.json")). + Build(t) + + zones, err := client.GetZone(context.Background(), "example.com") + require.NoError(t, err) + + expected := &APIResponse[*Zone]{ + Meta: &Metadata{ + InvocationID: "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + }, + Data: &Zone{ + Type: "zone", + ID: "dipcon.com", + Attributes: Attributes{ + OrganisationID: "10154", + OrganisationDescription: "My Company AB", + DNSTypeDescription: "Anycast", + Slave: false, + Pending: false, + Deleted: false, + Settings: &Settings{ + MName: "dns01.dipcon.com.", + Refresh: 3600, + Expire: 604800, + TTL: 600, + }, + Records: map[string]map[string][]Record{ + "@": { + "NS": { + { + TTL: 3600, + Data: "193.14.90.194", + Comments: "this is a comment", + }, + }, + }, + }, + Redirects: map[string][]Redirect{ + "": { + { + Path: "/x/y", + Destination: "https://abion.com/?ref=dipcon", + Status: 301, + Slugs: true, + Certificate: true, + }, + }, + }, + }, + }, + } + + assert.Equal(t, expected, zones) +} + +func TestGetZone_error(t *testing.T) { + client := mockBuilder(). + Route("GET /v1/zones/example.com", + servermock.ResponseFromFixture("error.json"). + WithStatusCode(http.StatusUnauthorized)). + Build(t) + + _, err := client.GetZone(context.Background(), "example.com") + require.EqualError(t, err, "could not get zone example.com: api error: status=401, message=Authentication Error") +} diff --git a/providers/dns/abion/internal/fixtures/error.json b/providers/dns/abion/internal/fixtures/error.json new file mode 100644 index 0000000000..9877fdb8c3 --- /dev/null +++ b/providers/dns/abion/internal/fixtures/error.json @@ -0,0 +1,9 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147" + }, + "error": { + "status": 401, + "message": "Authentication Error" + } +} diff --git a/providers/dns/abion/internal/fixtures/update-request.json b/providers/dns/abion/internal/fixtures/update-request.json new file mode 100644 index 0000000000..2d40e0a565 --- /dev/null +++ b/providers/dns/abion/internal/fixtures/update-request.json @@ -0,0 +1,23 @@ +{ + "data": { + "type": "zone", + "id": "example.com", + "attributes": { + "records": { + "_acme-challenge.test": { + "TXT": [ + { + "rdata": "test" + }, + { + "rdata": "test1" + }, + { + "rdata": "test2" + } + ] + } + } + } + } +} diff --git a/providers/dns/abion/internal/fixtures/update.json b/providers/dns/abion/internal/fixtures/update.json new file mode 100644 index 0000000000..a26defd639 --- /dev/null +++ b/providers/dns/abion/internal/fixtures/update.json @@ -0,0 +1,45 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147" + }, + "data": { + "type": "zone", + "id": "dipcon.com", + "attributes": { + "organisationId": "10154", + "organisationDescription": "My Company AB", + "dnsTypeDescription": "Anycast", + "slave": false, + "pending": false, + "deleted": false, + "settings": { + "mname": "dns01.dipcon.com.", + "refresh": 3600, + "expire": 604800, + "ttl": 600 + }, + "records": { + "@": { + "NS": [ + { + "ttl": 3600, + "rdata": "193.14.90.194", + "comments": "this is a comment" + } + ] + } + }, + "redirects": { + "": [ + { + "path": "/x/y", + "destination": "https://abion.com/?ref=dipcon", + "status": 301, + "slugs": true, + "certificate": true + } + ] + } + } + } +} diff --git a/providers/dns/abion/internal/fixtures/zone.json b/providers/dns/abion/internal/fixtures/zone.json new file mode 100644 index 0000000000..a26defd639 --- /dev/null +++ b/providers/dns/abion/internal/fixtures/zone.json @@ -0,0 +1,45 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147" + }, + "data": { + "type": "zone", + "id": "dipcon.com", + "attributes": { + "organisationId": "10154", + "organisationDescription": "My Company AB", + "dnsTypeDescription": "Anycast", + "slave": false, + "pending": false, + "deleted": false, + "settings": { + "mname": "dns01.dipcon.com.", + "refresh": 3600, + "expire": 604800, + "ttl": 600 + }, + "records": { + "@": { + "NS": [ + { + "ttl": 3600, + "rdata": "193.14.90.194", + "comments": "this is a comment" + } + ] + } + }, + "redirects": { + "": [ + { + "path": "/x/y", + "destination": "https://abion.com/?ref=dipcon", + "status": 301, + "slugs": true, + "certificate": true + } + ] + } + } + } +} diff --git a/providers/dns/abion/internal/fixtures/zones.json b/providers/dns/abion/internal/fixtures/zones.json new file mode 100644 index 0000000000..3fa444dd9a --- /dev/null +++ b/providers/dns/abion/internal/fixtures/zones.json @@ -0,0 +1,22 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + "offset": 0, + "limit": 1, + "total": 1 + }, + "data": [ + { + "type": "zone", + "id": "dipcon.com", + "attributes": { + "organisationId": "10154", + "organisationDescription": "My Company AB", + "dnsTypeDescription": "Anycast", + "slave": true, + "pending": true, + "deleted": true + } + } + ] +} diff --git a/providers/dns/abion/internal/types.go b/providers/dns/abion/internal/types.go new file mode 100644 index 0000000000..a25f0a0743 --- /dev/null +++ b/providers/dns/abion/internal/types.go @@ -0,0 +1,73 @@ +package internal + +import "fmt" + +type ZoneRequest struct { + Data Zone `json:"data"` +} + +type Pagination struct { + Offset int `json:"offset,omitempty" url:"offset"` + Limit int `json:"limit,omitempty" url:"limit"` + Total int `json:"total,omitempty" url:"total"` +} + +type APIResponse[T any] struct { + Meta *Metadata `json:"meta,omitempty"` + Data T `json:"data,omitempty"` + Error *Error `json:"error,omitempty"` +} + +type Metadata struct { + *Pagination + + InvocationID string `json:"invocationId,omitempty"` +} + +type Zone struct { + Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` + Attributes Attributes `json:"attributes"` +} + +type Attributes struct { + OrganisationID string `json:"organisationId,omitempty"` + OrganisationDescription string `json:"organisationDescription,omitempty"` + DNSTypeDescription string `json:"dnsTypeDescription,omitempty"` + Slave bool `json:"slave,omitempty"` + Pending bool `json:"pending,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Settings *Settings `json:"settings,omitempty"` + Records map[string]map[string][]Record `json:"records,omitempty"` + Redirects map[string][]Redirect `json:"redirects,omitempty"` +} + +type Settings struct { + MName string `json:"mname,omitempty"` + Refresh int `json:"refresh,omitempty"` + Expire int `json:"expire,omitempty"` + TTL int `json:"ttl,omitempty"` +} + +type Record struct { + TTL int `json:"ttl,omitempty"` + Data string `json:"rdata,omitempty"` + Comments string `json:"comments,omitempty"` +} + +type Redirect struct { + Path string `json:"path"` + Destination string `json:"destination"` + Status int `json:"status"` + Slugs bool `json:"slugs"` + Certificate bool `json:"certificate"` +} + +type Error struct { + Status int `json:"status"` + Message string `json:"message"` +} + +func (e *Error) Error() string { + return fmt.Sprintf("api error: status=%d, message=%s", e.Status, e.Message) +} From 8c0a27d0a6f118777866b93400f44f1686f10f6e Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 22 Apr 2026 21:44:46 +0200 Subject: [PATCH 2/2] chore: generate --- README.md | 97 ++++++++++++++------------- cmd/zz_gen_cmd_dnshelp.go | 21 ++++++ docs/content/dns/zz_gen_abion.md | 67 ++++++++++++++++++ docs/data/zz_cli_help.toml | 2 +- providers/dns/zz_gen_dns_providers.go | 3 + 5 files changed, 143 insertions(+), 47 deletions(-) create mode 100644 docs/content/dns/zz_gen_abion.md diff --git a/README.md b/README.md index 091d889386..f5715277d1 100644 --- a/README.md +++ b/README.md @@ -64,238 +64,243 @@ If your DNS provider is not supported, please open an [issue](https://github.com + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + +
1cloud.ru 35.com/三五互联Abion Active24Akamai EdgeDNS
Akamai EdgeDNS Alibaba Cloud DNS AlibabaCloud ESA all-inklAlwaysdata
Alwaysdata Amazon Lightsail Amazon Route 53 Anexia CloudDNSANS SafeDNS
ANS SafeDNS ArtFiles ArvanCloud Aurora DNSAutodns
Autodns Axelname Azion Azure DNSBaidu Cloud
Baidu Cloud Beget.com Binary Lane BindmanBluecat
Bluecat Bluecat v2 BookMyName BunnyCheckdomain
Checkdomain Civo Cloud.ru CloudDNSCloudflare
Cloudflare ClouDNS ConoHa v2 ConoHa v3Constellix
Constellix Core-Networks CPanel/WHM CzechiaDDnss (DynDNS Service)
DDnss (DynDNS Service) Derak Cloud deSEC.io Designate DNSaaS for OpenstackDigital Ocean
Digital Ocean DirectAdmin DNS Made Easy DNS Update (RFC2136)DNSExit
DNSExit dnsHome.de DNSimple Domain Offensive (do.de)Domeneshop
Domeneshop DreamHost Duck DNS DynDynDnsFree.de
DynDnsFree.de Dynu EasyDNS EdgeCenterEfficient IP
Efficient IP Epik EuroDNS ExcedoExoscale
Exoscale External program F5 XC freemyip.comFusionLayer NameSurfer
FusionLayer NameSurfer G-Core Gandi Gandi Live DNS (v5)Gigahost.no
Gigahost.no Glesys Go Daddy Google CloudGravity
Gravity Hetzner Hosting.de Hosting.nlHostinger
Hostinger Hosttech HTTP request http.netHuawei Cloud
Huawei Cloud Hurricane Electric DNS HyperOne IBM Cloud (SoftLayer)IIJ DNS Platform Service
IIJ DNS Platform Service Infoblox Infomaniak Internet.bsINWX
INWX Ionos Ionos Cloud IPv64ISPConfig 3
ISPConfig 3 ISPConfig 3 - Dynamic DNS (DDNS) Module JD Cloud JokerJoohoi's ACME-DNS
Joohoi's ACME-DNS KeyHelp Leaseweb LiaraLima-City
Lima-City Linode (v4) Liquid Web LoopiaLuaDNS
LuaDNS Mail-in-a-Box ManageEngine CloudDNS ManualMetaname
Metaname Metaregistrar mijn.host Mittwaldmyaddr.{tools,dev,io}
myaddr.{tools,dev,io} MyDNS.jp MythicBeasts Name.comNamecheap
Namecheap Namesilo NearlyFreeSpeech.NET NeodigitNetcup
Netcup Netlify Netnod NicmanagerNIFCloud
NIFCloud Njalla Nodion NS1Octenium
Octenium Online.net Open Telekom Cloud Oracle CloudOVH
OVH plesk.com Porkbun PowerDNSRackspace
Rackspace Rain Yun/雨云 RcodeZero reg.ruRegfish
Regfish RimuHosting RU CENTER Sakura CloudScaleway
Scaleway Selectel Selectel v2 SelfHost.(de|eu)Servercow
Servercow Shellrent Simply.com SonicSpaceship
Spaceship Stackpath Syse TechnitiumTencent Cloud DNS
Tencent Cloud DNS Tencent EdgeOne Timeweb Cloud TodayNIC/时代互联TransIP
TransIP UCloud Ultradns United-DomainsVariomedia
Variomedia VegaDNS Vercel Versio.[nl|eu|uk]VinylDNS
VinylDNS Virtualname VK Cloud Volcano Engine/火山引擎Vscale
Vscale Vultr webnames.ca webnames.ruWebsupport
Websupport WEDOS West.cn/西部数码 Yandex 360Yandex Cloud
Yandex Cloud Yandex PDD Zone.ee ZoneEdit
Zonomi
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index af9ee19943..ff69e7beee 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -12,6 +12,7 @@ import ( func allDNSCodes() string { providers := []string{ + "abion", "acmedns", "active24", "alidns", @@ -210,6 +211,26 @@ func displayDNSHelp(w io.Writer, name string) error { ew := &errWriter{w: w} switch name { + case "abion": + // generated from: providers/dns/abion/abion.toml + ew.writeln(`Configuration for Abion.`) + ew.writeln(`Code: 'abion'`) + ew.writeln(`Since: 'v5.0.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ABION_API_KEY": API key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ABION_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) + ew.writeln(` - "ABION_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "ABION_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) + ew.writeln(` - "ABION_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/abion`) + case "acmedns": // generated from: providers/dns/acmedns/acmedns.toml ew.writeln(`Configuration for Joohoi's ACME-DNS.`) diff --git a/docs/content/dns/zz_gen_abion.md b/docs/content/dns/zz_gen_abion.md new file mode 100644 index 0000000000..38f6ad2597 --- /dev/null +++ b/docs/content/dns/zz_gen_abion.md @@ -0,0 +1,67 @@ +--- +title: "Abion" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: abion +dnsprovider: + since: "v5.0.0" + code: "abion" + url: "https://abion.com" +--- + + + + + + +Configuration for [Abion](https://abion.com). + + + + +- Code: `abion` +- Since: v5.0.0 + + +Here is an example bash command using the Abion provider: + +```bash +ABION_API_KEY="xxxxxxxxxxxx" \ +lego --dns abion -d '*.example.com' -d example.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ABION_API_KEY` | API key | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `ABION_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | +| `ABION_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `ABION_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | +| `ABION_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{% ref "dns#configuration-and-credentials" %}}). + + + + +## More information + +- [API documentation](https://demo.abion.com/pmapi-doc/openapi-ui/index.html) + + + + diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 977a954b99..4e0f33dda4 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -467,7 +467,7 @@ To display the documentation for a specific DNS provider, run: $ lego dnshelp -c code Supported DNS providers: - acmedns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnsupdate, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, netnod, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, onecloudru, onlinenet, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ucloud, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnamesca, webnamesru, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi + abion, acmedns, active24, alidns, aliesa, allinkl, alwaysdata, anexia, artfiles, arvancloud, auroradns, autodns, axelname, azion, azuredns, baiducloud, beget, binarylane, bindman, bluecat, bluecatv2, bookmyname, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, com35, conoha, conohav3, constellix, corenetworks, cpanel, czechia, ddnss, derak, desec, designate, digitalocean, directadmin, dnsexit, dnshomede, dnsimple, dnsmadeeasy, dnsupdate, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgecenter, edgedns, edgeone, efficientip, epik, eurodns, excedo, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, gigahostno, glesys, godaddy, gravity, hetzner, hostingde, hostinger, hostingnl, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ionoscloud, ipv64, ispconfig, ispconfigddns, jdcloud, joker, keyhelp, leaseweb, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, namesurfer, nearlyfreespeech, neodigit, netcup, netlify, netnod, nicmanager, nicru, nifcloud, njalla, nodion, ns1, octenium, onecloudru, onlinenet, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, syse, technitium, tencentcloud, timewebcloud, todaynic, transip, ucloud, ultradns, uniteddomains, variomedia, vegadns, vercel, versio, vinyldns, virtualname, vkcloud, volcengine, vscale, vultr, webnamesca, webnamesru, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi More information: https://go-acme.github.io/lego/dns """ diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go index cfd58662e0..4e2b1d2af9 100644 --- a/providers/dns/zz_gen_dns_providers.go +++ b/providers/dns/zz_gen_dns_providers.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/go-acme/lego/v5/challenge" + "github.com/go-acme/lego/v5/providers/dns/abion" "github.com/go-acme/lego/v5/providers/dns/acmedns" "github.com/go-acme/lego/v5/providers/dns/active24" "github.com/go-acme/lego/v5/providers/dns/alidns" @@ -199,6 +200,8 @@ import ( // NewDNSChallengeProviderByName Factory for DNS providers. func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { + case "abion": + return abion.NewDNSProvider() case "acmedns", "acme-dns": return acmedns.NewDNSProvider() case "active24":