1
0
mirror of https://github.com/go-acme/lego.git synced 2024-11-24 08:52:18 +02:00

cloudxns: provider deprecation (#2324)

This commit is contained in:
Ludovic Fernandez 2024-11-06 23:58:56 +01:00 committed by GitHub
parent af7e2edd4e
commit 67230e268a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 19 additions and 787 deletions

View File

@ -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/cloudns/">ClouDNS</a></td>
</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/constellix/">Constellix</a></td>
<td><a href="https://go-acme.github.io/lego/dns/corenetworks/">Core-Networks</a></td>

View File

@ -563,7 +563,7 @@ func displayDNSHelp(w io.Writer, name string) error {
case "cloudxns":
// generated from: providers/dns/cloudxns/cloudxns.toml
ew.writeln(`Configuration for CloudXNS.`)
ew.writeln(`Configuration for CloudXNS (Deprecated).`)
ew.writeln(`Code: 'cloudxns'`)
ew.writeln(`Since: 'v0.5.0'`)
ew.writeln()

View File

@ -1,20 +1,20 @@
---
title: "CloudXNS"
title: "CloudXNS (Deprecated)"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: cloudxns
dnsprovider:
since: "v0.5.0"
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. -->
<!-- providers/dns/cloudxns/cloudxns.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
The CloudXNS DNS provider has shut down.
Configuration for [CloudXNS](https://www.cloudxns.net/).
<!--more-->
@ -23,7 +23,7 @@ Configuration for [CloudXNS](https://www.cloudxns.net/).
- 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
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. -->
<!-- providers/dns/cloudxns/cloudxns.toml -->

View File

@ -2,15 +2,11 @@
package cloudxns
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"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.
@ -38,101 +34,34 @@ type Config struct {
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *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),
},
}
return &Config{}
}
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *internal.Client
}
type DNSProvider struct{}
// 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) {
values, err := env.Get(EnvAPIKey, EnvSecretKey)
if err != nil {
return nil, fmt.Errorf("cloudxns: %w", err)
}
config := NewDefaultConfig()
config.APIKey = values[EnvAPIKey]
config.SecretKey = values[EnvSecretKey]
return NewDNSProviderConfig(config)
return NewDNSProviderConfig(&Config{})
}
// NewDNSProviderConfig return a DNSProvider instance configured for CloudXNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
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
func NewDNSProviderConfig(_ *Config) (*DNSProvider, error) {
return nil, errors.New("cloudxns: provider has shut down")
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth 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)
}
func (d *DNSProvider) Present(_, _, _ string) error {
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth 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)
}
func (d *DNSProvider) CleanUp(_, _, _ string) error {
return 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
return dns01.DefaultPropagationTimeout, dns01.DefaultPollingInterval
}

View File

@ -1,6 +1,8 @@
Name = "CloudXNS"
Description = """"""
URL = "https://www.cloudxns.net/"
Name = "CloudXNS (Deprecated)"
Description = '''
The CloudXNS DNS provider has shut down.
'''
URL = "https://github.com/go-acme/lego/issues/2323"
Code = "cloudxns"
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_TTL = "The TTL of the TXT record used for the DNS challenge"
CLOUDXNS_HTTP_TIMEOUT = "API request timeout"
[Links]
API = "https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip"

View File

@ -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)
}

View File

@ -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[:])
}

View File

@ -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)
})
}
}

View File

@ -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"`
}