forked from TrueCloudLab/lego
godaddy: add tests on the client (#1521)
This commit is contained in:
parent
60ae6e6dc9
commit
15f3a45342
6 changed files with 251 additions and 29 deletions
|
@ -10,13 +10,10 @@ import (
|
||||||
|
|
||||||
"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/platform/config/env"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/godaddy/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const minTTL = 600
|
||||||
// defaultBaseURL represents the API endpoint to call.
|
|
||||||
defaultBaseURL = "https://api.godaddy.com"
|
|
||||||
minTTL = 600
|
|
||||||
)
|
|
||||||
|
|
||||||
// Environment variables names.
|
// Environment variables names.
|
||||||
const (
|
const (
|
||||||
|
@ -56,6 +53,7 @@ func NewDefaultConfig() *Config {
|
||||||
// DNSProvider implements the challenge.Provider interface.
|
// DNSProvider implements the challenge.Provider interface.
|
||||||
type DNSProvider struct {
|
type DNSProvider struct {
|
||||||
config *Config
|
config *Config
|
||||||
|
client *internal.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProvider returns a DNSProvider instance configured for godaddy.
|
// NewDNSProvider returns a DNSProvider instance configured for godaddy.
|
||||||
|
@ -88,7 +86,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
return nil, fmt.Errorf("godaddy: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
|
return nil, fmt.Errorf("godaddy: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{config: config}, nil
|
client := internal.NewClient(config.APIKey, config.APISecret)
|
||||||
|
|
||||||
|
if config.HTTPClient != nil {
|
||||||
|
client.HTTPClient = config.HTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{config: config, client: client}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout returns the timeout and interval to use when checking for DNS
|
// Timeout returns the timeout and interval to use when checking for DNS
|
||||||
|
@ -108,19 +112,19 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
|
||||||
recordName := extractRecordName(fqdn, domainZone)
|
recordName := extractRecordName(fqdn, domainZone)
|
||||||
|
|
||||||
records, err := d.getRecords(domainZone, "TXT", recordName)
|
records, err := d.client.GetRecords(domainZone, "TXT", recordName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("godaddy: failed to get TXT records: %w", err)
|
return fmt.Errorf("godaddy: failed to get TXT records: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newRecords []DNSRecord
|
var newRecords []internal.DNSRecord
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
if record.Data != "" {
|
if record.Data != "" {
|
||||||
newRecords = append(newRecords, record)
|
newRecords = append(newRecords, record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record := DNSRecord{
|
record := internal.DNSRecord{
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
Name: recordName,
|
Name: recordName,
|
||||||
Data: value,
|
Data: value,
|
||||||
|
@ -128,7 +132,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
newRecords = append(newRecords, record)
|
newRecords = append(newRecords, record)
|
||||||
|
|
||||||
err = d.updateTxtRecords(newRecords, domainZone, recordName)
|
err = d.client.UpdateTxtRecords(newRecords, domainZone, recordName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("godaddy: failed to add TXT record: %w", err)
|
return fmt.Errorf("godaddy: failed to add TXT record: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -147,7 +151,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
|
||||||
recordName := extractRecordName(fqdn, domainZone)
|
recordName := extractRecordName(fqdn, domainZone)
|
||||||
|
|
||||||
records, err := d.getRecords(domainZone, "TXT", recordName)
|
records, err := d.client.GetRecords(domainZone, "TXT", recordName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("godaddy: failed to get TXT records: %w", err)
|
return fmt.Errorf("godaddy: failed to get TXT records: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -156,12 +160,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
allTxtRecords, err := d.getRecords(domainZone, "TXT", "")
|
allTxtRecords, err := d.client.GetRecords(domainZone, "TXT", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("godaddy: failed to get all TXT records: %w", err)
|
return fmt.Errorf("godaddy: failed to get all TXT records: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var recordsKeep []DNSRecord
|
var recordsKeep []internal.DNSRecord
|
||||||
for _, record := range allTxtRecords {
|
for _, record := range allTxtRecords {
|
||||||
if record.Data != value && record.Data != "" {
|
if record.Data != value && record.Data != "" {
|
||||||
recordsKeep = append(recordsKeep, record)
|
recordsKeep = append(recordsKeep, record)
|
||||||
|
@ -170,11 +174,11 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
|
||||||
// GoDaddy API don't provide a way to delete a record, an "empty" record must be added.
|
// GoDaddy API don't provide a way to delete a record, an "empty" record must be added.
|
||||||
if len(recordsKeep) == 0 {
|
if len(recordsKeep) == 0 {
|
||||||
emptyRecord := DNSRecord{Name: "empty", Data: ""}
|
emptyRecord := internal.DNSRecord{Name: "empty", Data: ""}
|
||||||
recordsKeep = append(recordsKeep, emptyRecord)
|
recordsKeep = append(recordsKeep, emptyRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.updateTxtRecords(recordsKeep, domainZone, "")
|
err = d.client.UpdateTxtRecords(recordsKeep, domainZone, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("godaddy: failed to remove TXT record: %w", err)
|
return fmt.Errorf("godaddy: failed to remove TXT record: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package godaddy
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -6,19 +6,33 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNSRecord a DNS record.
|
// DefaultBaseURL represents the API endpoint to call.
|
||||||
type DNSRecord struct {
|
const DefaultBaseURL = "https://api.godaddy.com"
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
type Client struct {
|
||||||
Data string `json:"data"`
|
HTTPClient *http.Client
|
||||||
Priority int `json:"priority,omitempty"`
|
baseURL *url.URL
|
||||||
TTL int `json:"ttl,omitempty"`
|
apiKey string
|
||||||
|
apiSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSProvider) getRecords(domainZone, rType, recordName string) ([]DNSRecord, error) {
|
func NewClient(apiKey string, apiSecret string) *Client {
|
||||||
|
baseURL, _ := url.Parse(DefaultBaseURL)
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||||
|
baseURL: baseURL,
|
||||||
|
apiKey: apiKey,
|
||||||
|
apiSecret: apiSecret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Client) GetRecords(domainZone, rType, recordName string) ([]DNSRecord, error) {
|
||||||
resource := path.Clean(fmt.Sprintf("/v1/domains/%s/records/%s/%s", domainZone, rType, recordName))
|
resource := path.Clean(fmt.Sprintf("/v1/domains/%s/records/%s/%s", domainZone, rType, recordName))
|
||||||
|
|
||||||
resp, err := d.makeRequest(http.MethodGet, resource, nil)
|
resp, err := d.makeRequest(http.MethodGet, resource, nil)
|
||||||
|
@ -43,7 +57,7 @@ func (d *DNSProvider) getRecords(domainZone, rType, recordName string) ([]DNSRec
|
||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSProvider) updateTxtRecords(records []DNSRecord, domainZone, recordName string) error {
|
func (d *Client) UpdateTxtRecords(records []DNSRecord, domainZone, recordName string) error {
|
||||||
body, err := json.Marshal(records)
|
body, err := json.Marshal(records)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -67,15 +81,20 @@ func (d *DNSProvider) updateTxtRecords(records []DNSRecord, domainZone, recordNa
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Response, error) {
|
func (d *Client) makeRequest(method, uri string, body io.Reader) (*http.Response, error) {
|
||||||
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", defaultBaseURL, uri), body)
|
endpoint, err := d.baseURL.Parse(path.Join(d.baseURL.Path, uri))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, endpoint.String(), body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.config.APIKey, d.config.APISecret))
|
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.apiKey, d.apiSecret))
|
||||||
|
|
||||||
return d.config.HTTPClient.Do(req)
|
return d.HTTPClient.Do(req)
|
||||||
}
|
}
|
142
providers/dns/godaddy/internal/client_test.go
Normal file
142
providers/dns/godaddy/internal/client_test.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTest(t *testing.T) (*http.ServeMux, *Client) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
client := NewClient("key", "secret")
|
||||||
|
client.HTTPClient = server.Client()
|
||||||
|
client.baseURL, _ = url.Parse(server.URL)
|
||||||
|
|
||||||
|
return mux, client
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetRecords(t *testing.T) {
|
||||||
|
mux, client := setupTest(t)
|
||||||
|
|
||||||
|
mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusOK, "getrecords.json"))
|
||||||
|
|
||||||
|
records, err := client.GetRecords("example.com", "TXT", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []DNSRecord{
|
||||||
|
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetRecords_errors(t *testing.T) {
|
||||||
|
mux, client := setupTest(t)
|
||||||
|
|
||||||
|
mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusUnprocessableEntity, "errors.json"))
|
||||||
|
|
||||||
|
records, err := client.GetRecords("example.com", "TXT", "")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_UpdateTxtRecords(t *testing.T) {
|
||||||
|
mux, client := setupTest(t)
|
||||||
|
|
||||||
|
mux.HandleFunc("/v1/domains/example.com/records/TXT/lego", func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodPut {
|
||||||
|
http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := req.Header.Get("Authorization")
|
||||||
|
if auth != "sso-key key:secret" {
|
||||||
|
http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
records := []DNSRecord{
|
||||||
|
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.UpdateTxtRecords(records, "example.com", "lego")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_UpdateTxtRecords_errors(t *testing.T) {
|
||||||
|
mux, client := setupTest(t)
|
||||||
|
|
||||||
|
mux.HandleFunc("/v1/domains/example.com/records/TXT/lego",
|
||||||
|
testHandler(http.MethodPut, http.StatusUnprocessableEntity, "errors.json"))
|
||||||
|
|
||||||
|
records := []DNSRecord{
|
||||||
|
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600},
|
||||||
|
{Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.UpdateTxtRecords(records, "example.com", "lego")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
|
||||||
|
return func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != method {
|
||||||
|
http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := req.Header.Get("Authorization")
|
||||||
|
if auth != "sso-key key:secret" {
|
||||||
|
http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.WriteHeader(statusCode)
|
||||||
|
|
||||||
|
if statusCode == http.StatusNoContent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filepath.Join("fixtures", filename))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = file.Close() }()
|
||||||
|
|
||||||
|
_, err = io.Copy(rw, file)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
providers/dns/godaddy/internal/fixtures/errors.json
Normal file
4
providers/dns/godaddy/internal/fixtures/errors.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"code": "INVALID_BODY",
|
||||||
|
"message": "Request body doesn't fulfill schema, see details in `fields`"
|
||||||
|
}
|
38
providers/dns/godaddy/internal/fixtures/getrecords.json
Normal file
38
providers/dns/godaddy/internal/fixtures/getrecords.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name":"_acme-challenge",
|
||||||
|
"type":"TXT",
|
||||||
|
"data":" ",
|
||||||
|
"ttl":600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"_acme-challenge.example",
|
||||||
|
"type":"TXT",
|
||||||
|
"data":"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU",
|
||||||
|
"ttl":600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"_acme-challenge.example",
|
||||||
|
"type":"TXT",
|
||||||
|
"data":"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek",
|
||||||
|
"ttl":600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"_acme-challenge.lego",
|
||||||
|
"type":"TXT",
|
||||||
|
"data":" ",
|
||||||
|
"ttl":600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"_acme-challenge.lego",
|
||||||
|
"type":"TXT",
|
||||||
|
"data":"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A",
|
||||||
|
"ttl":600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"_acme-challenge.lego",
|
||||||
|
"type":"TXT",
|
||||||
|
"data":"acme",
|
||||||
|
"ttl":600
|
||||||
|
}
|
||||||
|
]
|
15
providers/dns/godaddy/internal/types.go
Normal file
15
providers/dns/godaddy/internal/types.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
// DNSRecord a DNS record.
|
||||||
|
type DNSRecord struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
TTL int `json:"ttl,omitempty"`
|
||||||
|
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Protocol string `json:"protocol,omitempty"`
|
||||||
|
Service string `json:"service,omitempty"`
|
||||||
|
Weight int `json:"weight,omitempty"`
|
||||||
|
}
|
Loading…
Reference in a new issue