mirror of
https://github.com/go-acme/lego.git
synced 2025-07-03 22:20:27 +02:00
cloudxns: provider deprecation (#2324)
This commit is contained in:
committed by
GitHub
parent
af7e2edd4e
commit
67230e268a
@ -77,7 +77,7 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
|||||||
<td><a href="https://go-acme.github.io/lego/dns/cloudflare/">Cloudflare</a></td>
|
<td><a href="https://go-acme.github.io/lego/dns/cloudflare/">Cloudflare</a></td>
|
||||||
<td><a href="https://go-acme.github.io/lego/dns/cloudns/">ClouDNS</a></td>
|
<td><a href="https://go-acme.github.io/lego/dns/cloudns/">ClouDNS</a></td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><a href="https://go-acme.github.io/lego/dns/cloudxns/">CloudXNS</a></td>
|
<td><a href="https://go-acme.github.io/lego/dns/cloudxns/">CloudXNS (Deprecated)</a></td>
|
||||||
<td><a href="https://go-acme.github.io/lego/dns/conoha/">ConoHa</a></td>
|
<td><a href="https://go-acme.github.io/lego/dns/conoha/">ConoHa</a></td>
|
||||||
<td><a href="https://go-acme.github.io/lego/dns/constellix/">Constellix</a></td>
|
<td><a href="https://go-acme.github.io/lego/dns/constellix/">Constellix</a></td>
|
||||||
<td><a href="https://go-acme.github.io/lego/dns/corenetworks/">Core-Networks</a></td>
|
<td><a href="https://go-acme.github.io/lego/dns/corenetworks/">Core-Networks</a></td>
|
||||||
|
@ -563,7 +563,7 @@ func displayDNSHelp(w io.Writer, name string) error {
|
|||||||
|
|
||||||
case "cloudxns":
|
case "cloudxns":
|
||||||
// generated from: providers/dns/cloudxns/cloudxns.toml
|
// generated from: providers/dns/cloudxns/cloudxns.toml
|
||||||
ew.writeln(`Configuration for CloudXNS.`)
|
ew.writeln(`Configuration for CloudXNS (Deprecated).`)
|
||||||
ew.writeln(`Code: 'cloudxns'`)
|
ew.writeln(`Code: 'cloudxns'`)
|
||||||
ew.writeln(`Since: 'v0.5.0'`)
|
ew.writeln(`Since: 'v0.5.0'`)
|
||||||
ew.writeln()
|
ew.writeln()
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
---
|
---
|
||||||
title: "CloudXNS"
|
title: "CloudXNS (Deprecated)"
|
||||||
date: 2019-03-03T16:39:46+01:00
|
date: 2019-03-03T16:39:46+01:00
|
||||||
draft: false
|
draft: false
|
||||||
slug: cloudxns
|
slug: cloudxns
|
||||||
dnsprovider:
|
dnsprovider:
|
||||||
since: "v0.5.0"
|
since: "v0.5.0"
|
||||||
code: "cloudxns"
|
code: "cloudxns"
|
||||||
url: "https://www.cloudxns.net/"
|
url: "https://github.com/go-acme/lego/issues/2323"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
<!-- providers/dns/cloudxns/cloudxns.toml -->
|
<!-- providers/dns/cloudxns/cloudxns.toml -->
|
||||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
|
|
||||||
|
The CloudXNS DNS provider has shut down.
|
||||||
|
|
||||||
Configuration for [CloudXNS](https://www.cloudxns.net/).
|
|
||||||
|
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
@ -23,7 +23,7 @@ Configuration for [CloudXNS](https://www.cloudxns.net/).
|
|||||||
- Since: v0.5.0
|
- Since: v0.5.0
|
||||||
|
|
||||||
|
|
||||||
Here is an example bash command using the CloudXNS provider:
|
Here is an example bash command using the CloudXNS (Deprecated) provider:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CLOUDXNS_API_KEY=xxxx \
|
CLOUDXNS_API_KEY=xxxx \
|
||||||
@ -60,9 +60,6 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## More information
|
|
||||||
|
|
||||||
- [API documentation](https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip)
|
|
||||||
|
|
||||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||||
<!-- providers/dns/cloudxns/cloudxns.toml -->
|
<!-- providers/dns/cloudxns/cloudxns.toml -->
|
||||||
|
@ -2,15 +2,11 @@
|
|||||||
package cloudxns
|
package cloudxns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
"github.com/go-acme/lego/v4/platform/config/env"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudxns/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Environment variables names.
|
// Environment variables names.
|
||||||
@ -38,101 +34,34 @@ type Config struct {
|
|||||||
|
|
||||||
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||||
func NewDefaultConfig() *Config {
|
func NewDefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{}
|
||||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
|
||||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
|
||||||
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
|
|
||||||
HTTPClient: &http.Client{
|
|
||||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSProvider implements the challenge.Provider interface.
|
// DNSProvider implements the challenge.Provider interface.
|
||||||
type DNSProvider struct {
|
type DNSProvider struct{}
|
||||||
config *Config
|
|
||||||
client *internal.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDNSProvider returns a DNSProvider instance configured for CloudXNS.
|
// NewDNSProvider returns a DNSProvider instance configured for CloudXNS.
|
||||||
// Credentials must be passed in the environment variables:
|
|
||||||
// CLOUDXNS_API_KEY and CLOUDXNS_SECRET_KEY.
|
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
values, err := env.Get(EnvAPIKey, EnvSecretKey)
|
return NewDNSProviderConfig(&Config{})
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cloudxns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config := NewDefaultConfig()
|
|
||||||
config.APIKey = values[EnvAPIKey]
|
|
||||||
config.SecretKey = values[EnvSecretKey]
|
|
||||||
|
|
||||||
return NewDNSProviderConfig(config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProviderConfig return a DNSProvider instance configured for CloudXNS.
|
// NewDNSProviderConfig return a DNSProvider instance configured for CloudXNS.
|
||||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
func NewDNSProviderConfig(_ *Config) (*DNSProvider, error) {
|
||||||
if config == nil {
|
return nil, errors.New("cloudxns: provider has shut down")
|
||||||
return nil, errors.New("cloudxns: the configuration of the DNS provider is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := internal.NewClient(config.APIKey, config.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cloudxns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.HTTPClient != nil {
|
|
||||||
client.HTTPClient = config.HTTPClient
|
|
||||||
}
|
|
||||||
|
|
||||||
return &DNSProvider{client: client, config: config}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present creates a TXT record to fulfill the dns-01 challenge.
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
func (d *DNSProvider) Present(_, _, _ string) error {
|
||||||
challengeInfo := dns01.GetChallengeInfo(domain, keyAuth)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
info, err := d.client.GetDomainInformation(ctx, challengeInfo.EffectiveFQDN)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cloudxns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.client.AddTxtRecord(ctx, info, challengeInfo.EffectiveFQDN, challengeInfo.Value, d.config.TTL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cloudxns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters.
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (d *DNSProvider) CleanUp(_, _, _ string) error {
|
||||||
challengeInfo := dns01.GetChallengeInfo(domain, keyAuth)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
info, err := d.client.GetDomainInformation(ctx, challengeInfo.EffectiveFQDN)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cloudxns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
record, err := d.client.FindTxtRecord(ctx, info.ID, challengeInfo.EffectiveFQDN)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cloudxns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.client.RemoveTxtRecord(ctx, record.RecordID, info.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cloudxns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||||
// Adjusting here to cope with spikes in propagation times.
|
// Adjusting here to cope with spikes in propagation times.
|
||||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
return dns01.DefaultPropagationTimeout, dns01.DefaultPollingInterval
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
Name = "CloudXNS"
|
Name = "CloudXNS (Deprecated)"
|
||||||
Description = """"""
|
Description = '''
|
||||||
URL = "https://www.cloudxns.net/"
|
The CloudXNS DNS provider has shut down.
|
||||||
|
'''
|
||||||
|
URL = "https://github.com/go-acme/lego/issues/2323"
|
||||||
Code = "cloudxns"
|
Code = "cloudxns"
|
||||||
Since = "v0.5.0"
|
Since = "v0.5.0"
|
||||||
|
|
||||||
@ -19,6 +21,3 @@ lego --email you@example.com --dns cloudxns --domains my.example.org run
|
|||||||
CLOUDXNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
CLOUDXNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||||
CLOUDXNS_TTL = "The TTL of the TXT record used for the DNS challenge"
|
CLOUDXNS_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||||
CLOUDXNS_HTTP_TIMEOUT = "API request timeout"
|
CLOUDXNS_HTTP_TIMEOUT = "API request timeout"
|
||||||
|
|
||||||
[Links]
|
|
||||||
API = "https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip"
|
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
package cloudxns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/platform/tester"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const envDomain = envNamespace + "DOMAIN"
|
|
||||||
|
|
||||||
var envTest = tester.NewEnvTest(
|
|
||||||
EnvAPIKey,
|
|
||||||
EnvSecretKey).
|
|
||||||
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",
|
|
||||||
EnvSecretKey: "456",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "missing credentials",
|
|
||||||
envVars: map[string]string{
|
|
||||||
EnvAPIKey: "",
|
|
||||||
EnvSecretKey: "",
|
|
||||||
},
|
|
||||||
expected: "cloudxns: some credentials information are missing: CLOUDXNS_API_KEY,CLOUDXNS_SECRET_KEY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "missing API key",
|
|
||||||
envVars: map[string]string{
|
|
||||||
EnvAPIKey: "",
|
|
||||||
EnvSecretKey: "456",
|
|
||||||
},
|
|
||||||
expected: "cloudxns: some credentials information are missing: CLOUDXNS_API_KEY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "missing secret key",
|
|
||||||
envVars: map[string]string{
|
|
||||||
EnvAPIKey: "123",
|
|
||||||
EnvSecretKey: "",
|
|
||||||
},
|
|
||||||
expected: "cloudxns: some credentials information are missing: CLOUDXNS_SECRET_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)
|
|
||||||
require.NotNil(t, p.client)
|
|
||||||
} else {
|
|
||||||
require.EqualError(t, err, test.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewDNSProviderConfig(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
apiKey string
|
|
||||||
secretKey string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "success",
|
|
||||||
apiKey: "123",
|
|
||||||
secretKey: "456",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "missing credentials",
|
|
||||||
expected: "cloudxns: credentials missing: apiKey",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "missing api key",
|
|
||||||
secretKey: "456",
|
|
||||||
expected: "cloudxns: credentials missing: apiKey",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "missing secret key",
|
|
||||||
apiKey: "123",
|
|
||||||
expected: "cloudxns: credentials missing: secretKey",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
config := NewDefaultConfig()
|
|
||||||
config.APIKey = test.apiKey
|
|
||||||
config.SecretKey = test.secretKey
|
|
||||||
|
|
||||||
p, err := NewDNSProviderConfig(config)
|
|
||||||
|
|
||||||
if test.expected == "" {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, p)
|
|
||||||
require.NotNil(t, p.config)
|
|
||||||
require.NotNil(t, p.client)
|
|
||||||
} 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(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)
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
@ -1,221 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultBaseURL = "https://www.cloudxns.net/api2/"
|
|
||||||
|
|
||||||
// Client CloudXNS client.
|
|
||||||
type Client struct {
|
|
||||||
apiKey string
|
|
||||||
secretKey string
|
|
||||||
|
|
||||||
baseURL *url.URL
|
|
||||||
HTTPClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient creates a CloudXNS client.
|
|
||||||
func NewClient(apiKey, secretKey string) (*Client, error) {
|
|
||||||
if apiKey == "" {
|
|
||||||
return nil, errors.New("credentials missing: apiKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
if secretKey == "" {
|
|
||||||
return nil, errors.New("credentials missing: secretKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseURL, _ := url.Parse(defaultBaseURL)
|
|
||||||
|
|
||||||
return &Client{
|
|
||||||
apiKey: apiKey,
|
|
||||||
secretKey: secretKey,
|
|
||||||
baseURL: baseURL,
|
|
||||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDomainInformation Get domain name information for a FQDN.
|
|
||||||
func (c *Client) GetDomainInformation(ctx context.Context, fqdn string) (*Data, error) {
|
|
||||||
endpoint := c.baseURL.JoinPath("domain")
|
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
authZone, err := dns01.FindZoneByFqdn(fqdn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not find zone: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var domains []Data
|
|
||||||
err = c.do(req, &domains)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, data := range domains {
|
|
||||||
if data.Domain == authZone {
|
|
||||||
return &data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("zone %s not found for domain %s", authZone, fqdn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindTxtRecord return the TXT record a zone ID and a FQDN.
|
|
||||||
func (c *Client) FindTxtRecord(ctx context.Context, zoneID, fqdn string) (*TXTRecord, error) {
|
|
||||||
endpoint := c.baseURL.JoinPath("record", zoneID)
|
|
||||||
|
|
||||||
query := endpoint.Query()
|
|
||||||
query.Set("host_id", "0")
|
|
||||||
query.Set("offset", "0")
|
|
||||||
query.Set("row_num", "2000")
|
|
||||||
endpoint.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var records []TXTRecord
|
|
||||||
err = c.do(req, &records)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, record := range records {
|
|
||||||
if record.Host == dns01.UnFqdn(fqdn) && record.Type == "TXT" {
|
|
||||||
return &record, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no existing record found for %q", fqdn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTxtRecord add a TXT record.
|
|
||||||
func (c *Client) AddTxtRecord(ctx context.Context, info *Data, fqdn, value string, ttl int) error {
|
|
||||||
id, err := strconv.Atoi(info.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid zone ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint := c.baseURL.JoinPath("record")
|
|
||||||
|
|
||||||
subDomain, err := dns01.ExtractSubDomain(fqdn, info.Domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
record := TXTRecord{
|
|
||||||
ID: id,
|
|
||||||
Host: subDomain,
|
|
||||||
Value: value,
|
|
||||||
Type: "TXT",
|
|
||||||
LineID: 1,
|
|
||||||
TTL: ttl,
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodPost, endpoint, record)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.do(req, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveTxtRecord remove a TXT record.
|
|
||||||
func (c *Client) RemoveTxtRecord(ctx context.Context, recordID, zoneID string) error {
|
|
||||||
endpoint := c.baseURL.JoinPath("record", recordID, zoneID)
|
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodDelete, endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.do(req, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) do(req *http.Request, result any) error {
|
|
||||||
resp, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return errutils.NewHTTPDoError(req, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
raw, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var response apiResponse
|
|
||||||
err = json.Unmarshal(raw, &response)
|
|
||||||
if err != nil {
|
|
||||||
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.Code != 1 {
|
|
||||||
return fmt.Errorf("[status code %d] invalid code (%v) error: %s", resp.StatusCode, response.Code, response.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(response.Data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(response.Data, result)
|
|
||||||
if err != nil {
|
|
||||||
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newRequest(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestDate := time.Now().Format(time.RFC1123Z)
|
|
||||||
|
|
||||||
req.Header.Set("API-KEY", c.apiKey)
|
|
||||||
req.Header.Set("API-REQUEST-DATE", requestDate)
|
|
||||||
req.Header.Set("API-HMAC", c.hmac(endpoint.String(), requestDate, buf.String()))
|
|
||||||
req.Header.Set("API-FORMAT", "json")
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) hmac(endpoint, date, body string) string {
|
|
||||||
sum := md5.Sum([]byte(c.apiKey + endpoint + body + date + c.secretKey))
|
|
||||||
return hex.EncodeToString(sum[:])
|
|
||||||
}
|
|
@ -1,292 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupTest(t *testing.T, handler http.HandlerFunc) *Client {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
server := httptest.NewServer(handler)
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
|
|
||||||
client, _ := NewClient("myKey", "mySecret")
|
|
||||||
client.baseURL, _ = url.Parse(server.URL + "/")
|
|
||||||
client.HTTPClient = server.Client()
|
|
||||||
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerMock(method string, response *apiResponse, data interface{}) http.HandlerFunc {
|
|
||||||
return func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.Method != method {
|
|
||||||
content, err := json.Marshal(apiResponse{
|
|
||||||
Code: 999, // random code only for the test
|
|
||||||
Message: fmt.Sprintf("invalid method: got %s want %s", req.Method, method),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(rw, string(content), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Data = jsonData
|
|
||||||
|
|
||||||
content, err := json.Marshal(response)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = rw.Write(content)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_GetDomainInformation(t *testing.T) {
|
|
||||||
type result struct {
|
|
||||||
domain *Data
|
|
||||||
error bool
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
fqdn string
|
|
||||||
response *apiResponse
|
|
||||||
data []Data
|
|
||||||
expected result
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "domain found",
|
|
||||||
fqdn: "_acme-challenge.example.org.",
|
|
||||||
response: &apiResponse{
|
|
||||||
Code: 1,
|
|
||||||
},
|
|
||||||
data: []Data{
|
|
||||||
{
|
|
||||||
ID: "1",
|
|
||||||
Domain: "example.com.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "2",
|
|
||||||
Domain: "example.org.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: result{domain: &Data{
|
|
||||||
ID: "2",
|
|
||||||
Domain: "example.org.",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "domains not found",
|
|
||||||
fqdn: "_acme-challenge.huu.com.",
|
|
||||||
response: &apiResponse{
|
|
||||||
Code: 1,
|
|
||||||
},
|
|
||||||
data: []Data{
|
|
||||||
{
|
|
||||||
ID: "5",
|
|
||||||
Domain: "example.com.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "6",
|
|
||||||
Domain: "example.org.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: result{error: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
client := setupTest(t, handlerMock(http.MethodGet, test.response, test.data))
|
|
||||||
|
|
||||||
domain, err := client.GetDomainInformation(context.Background(), test.fqdn)
|
|
||||||
|
|
||||||
if test.expected.error {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, test.expected.domain, domain)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_FindTxtRecord(t *testing.T) {
|
|
||||||
type result struct {
|
|
||||||
txtRecord *TXTRecord
|
|
||||||
error bool
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
fqdn string
|
|
||||||
zoneID string
|
|
||||||
txtRecords []TXTRecord
|
|
||||||
response *apiResponse
|
|
||||||
expected result
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "record found",
|
|
||||||
fqdn: "_acme-challenge.example.org.",
|
|
||||||
zoneID: "test-zone",
|
|
||||||
txtRecords: []TXTRecord{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
RecordID: "Record-A",
|
|
||||||
Host: "_acme-challenge.example.org",
|
|
||||||
Value: "txtTXTtxtTXTtxtTXTtxtTXT",
|
|
||||||
Type: "TXT",
|
|
||||||
LineID: 6,
|
|
||||||
TTL: 30,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
RecordID: "Record-B",
|
|
||||||
Host: "_acme-challenge.example.com",
|
|
||||||
Value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
|
||||||
Type: "TXT",
|
|
||||||
LineID: 6,
|
|
||||||
TTL: 30,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
response: &apiResponse{
|
|
||||||
Code: 1,
|
|
||||||
},
|
|
||||||
expected: result{
|
|
||||||
txtRecord: &TXTRecord{
|
|
||||||
ID: 1,
|
|
||||||
RecordID: "Record-A",
|
|
||||||
Host: "_acme-challenge.example.org",
|
|
||||||
Value: "txtTXTtxtTXTtxtTXTtxtTXT",
|
|
||||||
Type: "TXT",
|
|
||||||
LineID: 6,
|
|
||||||
TTL: 30,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "record not found",
|
|
||||||
fqdn: "_acme-challenge.huu.com.",
|
|
||||||
zoneID: "test-zone",
|
|
||||||
txtRecords: []TXTRecord{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
RecordID: "Record-A",
|
|
||||||
Host: "_acme-challenge.example.org",
|
|
||||||
Value: "txtTXTtxtTXTtxtTXTtxtTXT",
|
|
||||||
Type: "TXT",
|
|
||||||
LineID: 6,
|
|
||||||
TTL: 30,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
RecordID: "Record-B",
|
|
||||||
Host: "_acme-challenge.example.com",
|
|
||||||
Value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
|
||||||
Type: "TXT",
|
|
||||||
LineID: 6,
|
|
||||||
TTL: 30,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
response: &apiResponse{
|
|
||||||
Code: 1,
|
|
||||||
},
|
|
||||||
expected: result{error: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
client := setupTest(t, handlerMock(http.MethodGet, test.response, test.txtRecords))
|
|
||||||
|
|
||||||
txtRecord, err := client.FindTxtRecord(context.Background(), test.zoneID, test.fqdn)
|
|
||||||
|
|
||||||
if test.expected.error {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, test.expected.txtRecord, txtRecord)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_AddTxtRecord(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
domain *Data
|
|
||||||
fqdn string
|
|
||||||
value string
|
|
||||||
ttl int
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "sub-domain",
|
|
||||||
domain: &Data{
|
|
||||||
ID: "1",
|
|
||||||
Domain: "example.com.",
|
|
||||||
},
|
|
||||||
fqdn: "_acme-challenge.foo.example.com.",
|
|
||||||
value: "txtTXTtxtTXTtxtTXTtxtTXT",
|
|
||||||
ttl: 30,
|
|
||||||
expected: `{"domain_id":1,"host":"_acme-challenge.foo","value":"txtTXTtxtTXTtxtTXTtxtTXT","type":"TXT","line_id":"1","ttl":"30"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "main domain",
|
|
||||||
domain: &Data{
|
|
||||||
ID: "2",
|
|
||||||
Domain: "example.com.",
|
|
||||||
},
|
|
||||||
fqdn: "_acme-challenge.example.com.",
|
|
||||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
|
||||||
ttl: 30,
|
|
||||||
expected: `{"domain_id":2,"host":"_acme-challenge","value":"TXTtxtTXTtxtTXTtxtTXTtxt","type":"TXT","line_id":"1","ttl":"30"}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
response := &apiResponse{
|
|
||||||
Code: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
assert.NotNil(t, req.Body)
|
|
||||||
content, err := io.ReadAll(req.Body)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, test.expected, string(bytes.TrimSpace(content)))
|
|
||||||
|
|
||||||
handlerMock(http.MethodPost, response, nil).ServeHTTP(rw, req)
|
|
||||||
})
|
|
||||||
|
|
||||||
err := client.AddTxtRecord(context.Background(), test.domain, test.fqdn, test.value, test.ttl)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
type apiResponse struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data json.RawMessage `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data Domain information.
|
|
||||||
type Data struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
TTL int `json:"ttl,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TXTRecord a TXT record.
|
|
||||||
type TXTRecord struct {
|
|
||||||
ID int `json:"domain_id,omitempty"`
|
|
||||||
RecordID string `json:"record_id,omitempty"`
|
|
||||||
|
|
||||||
Host string `json:"host"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
LineID int `json:"line_id,string"`
|
|
||||||
TTL int `json:"ttl,string"`
|
|
||||||
}
|
|
Reference in New Issue
Block a user