mirror of
				https://github.com/go-acme/lego.git
				synced 2025-10-31 08:27:38 +02:00 
			
		
		
		
	feat: Add DNS provider for Websupport (#1824)
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							401bf5b4bd
						
					
				
				
					commit
					ad612e639e
				
			| @@ -75,8 +75,8 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | ||||
| | [Sonic](https://go-acme.github.io/lego/dns/sonic/)                              | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/)                      | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/)           | [TransIP](https://go-acme.github.io/lego/dns/transip/)                          | | ||||
| | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/)                   | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/)                        | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/)                    | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/)                          | | ||||
| | [Vercel](https://go-acme.github.io/lego/dns/vercel/)                            | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/)                 | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/)                        | [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/)                         | | ||||
| | [Vscale](https://go-acme.github.io/lego/dns/vscale/)                            | [Vultr](https://go-acme.github.io/lego/dns/vultr/)                              | [WEDOS](https://go-acme.github.io/lego/dns/wedos/)                              | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/)                 | | ||||
| | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/)                        | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/)                           | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/)                            |                                                                                 | | ||||
| | [Vscale](https://go-acme.github.io/lego/dns/vscale/)                            | [Vultr](https://go-acme.github.io/lego/dns/vultr/)                              | [Websupport](https://go-acme.github.io/lego/dns/websupport/)                    | [WEDOS](https://go-acme.github.io/lego/dns/wedos/)                              | | ||||
| | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/)                 | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/)                        | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/)                           | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/)                            | | ||||
|  | ||||
| <!-- END DNS PROVIDERS LIST --> | ||||
|  | ||||
|   | ||||
| @@ -119,6 +119,7 @@ func allDNSCodes() string { | ||||
| 		"vkcloud", | ||||
| 		"vscale", | ||||
| 		"vultr", | ||||
| 		"websupport", | ||||
| 		"wedos", | ||||
| 		"yandex", | ||||
| 		"yandexcloud", | ||||
| @@ -2367,6 +2368,28 @@ func displayDNSHelp(w io.Writer, name string) error { | ||||
| 		ew.writeln() | ||||
| 		ew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`) | ||||
|  | ||||
| 	case "websupport": | ||||
| 		// generated from: providers/dns/websupport/websupport.toml | ||||
| 		ew.writeln(`Configuration for Websupport.`) | ||||
| 		ew.writeln(`Code:	'websupport'`) | ||||
| 		ew.writeln(`Since:	'v4.10.0'`) | ||||
| 		ew.writeln() | ||||
|  | ||||
| 		ew.writeln(`Credentials:`) | ||||
| 		ew.writeln(`	- "WEBSUPPORT_API_KEY":	API key`) | ||||
| 		ew.writeln(`	- "WEBSUPPORT_SECRET":	API secret`) | ||||
| 		ew.writeln() | ||||
|  | ||||
| 		ew.writeln(`Additional Configuration:`) | ||||
| 		ew.writeln(`	- "WEBSUPPORT_HTTP_TIMEOUT":	API request timeout`) | ||||
| 		ew.writeln(`	- "WEBSUPPORT_POLLING_INTERVAL":	Time between DNS propagation check`) | ||||
| 		ew.writeln(`	- "WEBSUPPORT_PROPAGATION_TIMEOUT":	Maximum waiting time for DNS propagation`) | ||||
| 		ew.writeln(`	- "WEBSUPPORT_SEQUENCE_INTERVAL":	Time between sequential requests`) | ||||
| 		ew.writeln(`	- "WEBSUPPORT_TTL":	The TTL of the TXT record used for the DNS challenge`) | ||||
|  | ||||
| 		ew.writeln() | ||||
| 		ew.writeln(`More information: https://go-acme.github.io/lego/dns/websupport`) | ||||
|  | ||||
| 	case "wedos": | ||||
| 		// generated from: providers/dns/wedos/wedos.toml | ||||
| 		ew.writeln(`Configuration for WEDOS.`) | ||||
|   | ||||
							
								
								
									
										70
									
								
								docs/content/dns/zz_gen_websupport.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								docs/content/dns/zz_gen_websupport.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| --- | ||||
| title: "Websupport" | ||||
| date: 2019-03-03T16:39:46+01:00 | ||||
| draft: false | ||||
| slug: websupport | ||||
| dnsprovider: | ||||
|   since:    "v4.10.0" | ||||
|   code:     "websupport" | ||||
|   url:      "https://websupport.sk" | ||||
| --- | ||||
|  | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
| <!-- providers/dns/websupport/websupport.toml --> | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
|  | ||||
|  | ||||
| Configuration for [Websupport](https://websupport.sk). | ||||
|  | ||||
|  | ||||
| <!--more--> | ||||
|  | ||||
| - Code: `websupport` | ||||
| - Since: v4.10.0 | ||||
|  | ||||
|  | ||||
| Here is an example bash command using the Websupport provider: | ||||
|  | ||||
| ```bash | ||||
| WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ | ||||
| WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ | ||||
| lego --email myemail@example.com --dns websupport --domains my.example.org run | ||||
| ``` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Credentials | ||||
|  | ||||
| | Environment Variable Name | Description | | ||||
| |-----------------------|-------------| | ||||
| | `WEBSUPPORT_API_KEY` | API key | | ||||
| | `WEBSUPPORT_SECRET` | API secret | | ||||
|  | ||||
| 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 | | ||||
| |--------------------------------|-------------| | ||||
| | `WEBSUPPORT_HTTP_TIMEOUT` | API request timeout | | ||||
| | `WEBSUPPORT_POLLING_INTERVAL` | Time between DNS propagation check | | ||||
| | `WEBSUPPORT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | ||||
| | `WEBSUPPORT_SEQUENCE_INTERVAL` | Time between sequential requests | | ||||
| | `WEBSUPPORT_TTL` | The TTL of the TXT record used for the DNS challenge | | ||||
|  | ||||
| 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://rest.websupport.sk/docs/v1.zone) | ||||
|  | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
| <!-- providers/dns/websupport/websupport.toml --> | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
| @@ -125,7 +125,7 @@ To display the documentation for a specific DNS provider, run: | ||||
|   $ lego dnshelp -c code | ||||
|  | ||||
| Supported DNS providers: | ||||
|   acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, bindman, bluecat, checkdomain, civo, clouddns, cloudflare, cloudns, cloudxns, conoha, constellix, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, hetzner, hostingde, hosttech, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, ns1, oraclecloud, otc, ovh, pdns, porkbun, rackspace, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, wedos, yandex, yandexcloud, zoneee, zonomi | ||||
|   acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, bindman, bluecat, checkdomain, civo, clouddns, cloudflare, cloudns, cloudxns, conoha, constellix, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, hetzner, hostingde, hosttech, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, ns1, oraclecloud, otc, ovh, pdns, porkbun, rackspace, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, websupport, wedos, yandex, yandexcloud, zoneee, zonomi | ||||
|  | ||||
| More information: https://go-acme.github.io/lego/dns | ||||
| """ | ||||
|   | ||||
| @@ -110,6 +110,7 @@ import ( | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/vkcloud" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/vscale" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/vultr" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/websupport" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/wedos" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/yandex" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/yandexcloud" | ||||
| @@ -328,10 +329,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { | ||||
| 		return vinyldns.NewDNSProvider() | ||||
| 	case "vkcloud": | ||||
| 		return vkcloud.NewDNSProvider() | ||||
| 	case "vultr": | ||||
| 		return vultr.NewDNSProvider() | ||||
| 	case "vscale": | ||||
| 		return vscale.NewDNSProvider() | ||||
| 	case "vultr": | ||||
| 		return vultr.NewDNSProvider() | ||||
| 	case "websupport": | ||||
| 		return websupport.NewDNSProvider() | ||||
| 	case "wedos": | ||||
| 		return wedos.NewDNSProvider() | ||||
| 	case "yandex": | ||||
|   | ||||
							
								
								
									
										258
									
								
								providers/dns/websupport/internal/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								providers/dns/websupport/internal/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,258 @@ | ||||
| package internal | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/hmac" | ||||
| 	"crypto/sha1" | ||||
| 	"encoding/hex" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| const defaultBaseURL = "https://rest.websupport.sk" | ||||
|  | ||||
| // StatusSuccess expected status text when success. | ||||
| const StatusSuccess = "success" | ||||
|  | ||||
| // Client a Websupport DNS API client. | ||||
| type Client struct { | ||||
| 	apiKey     string | ||||
| 	secretKey  string | ||||
| 	BaseURL    string | ||||
| 	HTTPClient *http.Client | ||||
| } | ||||
|  | ||||
| // NewClient creates a new Client. | ||||
| func NewClient(apiKey, secretKey string) (*Client, error) { | ||||
| 	if apiKey == "" || secretKey == "" { | ||||
| 		return nil, errors.New("credentials missing") | ||||
| 	} | ||||
|  | ||||
| 	return &Client{ | ||||
| 		apiKey:     apiKey, | ||||
| 		secretKey:  secretKey, | ||||
| 		BaseURL:    defaultBaseURL, | ||||
| 		HTTPClient: &http.Client{Timeout: 10 * time.Second}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // GetUser gets a user detail. | ||||
| // https://rest.websupport.sk/docs/v1.user#user | ||||
| func (c *Client) GetUser(userID string) (*User, error) { | ||||
| 	baseURL, err := url.Parse(c.BaseURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("base url parsing: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", userID)) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse endpoint: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("request payload: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	result := &User{} | ||||
|  | ||||
| 	err = c.do(req, result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // ListRecords lists all records. | ||||
| // https://rest.websupport.sk/docs/v1.zone#records | ||||
| func (c *Client) ListRecords(domainName string) (*ListResponse, error) { | ||||
| 	baseURL, err := url.Parse(c.BaseURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("base url parsing: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record")) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse endpoint: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("request payload: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	result := &ListResponse{} | ||||
|  | ||||
| 	err = c.do(req, result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // GetRecords gets a DNS record. | ||||
| func (c *Client) GetRecords(domainName string, recordID int) (*Record, error) { | ||||
| 	baseURL, err := url.Parse(c.BaseURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("base url parsing: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record", strconv.Itoa(recordID))) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse endpoint: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	result := &Record{} | ||||
|  | ||||
| 	err = c.do(req, result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // AddRecord adds a DNS record. | ||||
| // https://rest.websupport.sk/docs/v1.zone#post-record | ||||
| func (c *Client) AddRecord(domainName string, record Record) (*Response, error) { | ||||
| 	baseURL, err := url.Parse(c.BaseURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("base url parsing: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record")) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse endpoint: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	payload, err := json.Marshal(record) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("request payload: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader(payload)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	result := &Response{} | ||||
|  | ||||
| 	err = c.do(req, result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // DeleteRecord deletes a DNS record. | ||||
| // https://rest.websupport.sk/docs/v1.zone#delete-record | ||||
| func (c *Client) DeleteRecord(domainName string, recordID int) (*Response, error) { | ||||
| 	baseURL, err := url.Parse(c.BaseURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("base url parsing: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record", strconv.Itoa(recordID))) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse endpoint: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest(http.MethodDelete, endpoint.String(), http.NoBody) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("request payload: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	result := &Response{} | ||||
|  | ||||
| 	err = c.do(req, result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func (c *Client) do(req *http.Request, result any) error { | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 	req.Header.Set("Accept", "application/json") | ||||
| 	req.Header.Set("Accept-Language", "en_us") | ||||
|  | ||||
| 	location, err := time.LoadLocation("GMT") | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("time location: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	err = c.sign(req, time.Now().In(location)) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("signature: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := c.HTTPClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	defer func() { _ = resp.Body.Close() }() | ||||
|  | ||||
| 	if resp.StatusCode > http.StatusBadRequest { | ||||
| 		all, _ := io.ReadAll(resp.Body) | ||||
|  | ||||
| 		var e APIError | ||||
| 		err = json.Unmarshal(all, &e) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("%d: %s", resp.StatusCode, string(all)) | ||||
| 		} | ||||
|  | ||||
| 		return &e | ||||
| 	} | ||||
|  | ||||
| 	all, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("read response body: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	err = json.Unmarshal(all, result) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("unmarshal response body: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) sign(req *http.Request, now time.Time) error { | ||||
| 	if req.URL.Path == "" { | ||||
| 		req.URL.Path += "/" | ||||
| 	} | ||||
|  | ||||
| 	canonicalRequest := fmt.Sprintf("%s %s %d", req.Method, req.URL.Path, now.Unix()) | ||||
|  | ||||
| 	mac := hmac.New(sha1.New, []byte(c.secretKey)) | ||||
| 	_, err := mac.Write([]byte(canonicalRequest)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	hashed := mac.Sum(nil) | ||||
| 	signature := hex.EncodeToString(hashed) | ||||
|  | ||||
| 	req.SetBasicAuth(c.apiKey, signature) | ||||
|  | ||||
| 	req.Header.Set("Date", now.Format(time.RFC3339)) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										232
									
								
								providers/dns/websupport/internal/client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								providers/dns/websupport/internal/client_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | ||||
| package internal | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	mux := http.NewServeMux() | ||||
| 	server := httptest.NewServer(mux) | ||||
| 	t.Cleanup(server.Close) | ||||
|  | ||||
| 	mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { | ||||
| 		if req.Method != method { | ||||
| 			http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		open, err := os.Open(file) | ||||
| 		if err != nil { | ||||
| 			http.Error(rw, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		defer func() { _ = open.Close() }() | ||||
|  | ||||
| 		rw.WriteHeader(status) | ||||
| 		_, err = io.Copy(rw, open) | ||||
| 		if err != nil { | ||||
| 			http.Error(rw, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	client, err := NewClient("apiKey", "secretKey") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	client.HTTPClient = server.Client() | ||||
| 	client.BaseURL = server.URL | ||||
|  | ||||
| 	return client | ||||
| } | ||||
|  | ||||
| func TestClient_GetUser(t *testing.T) { | ||||
| 	client := setupTest(t, http.MethodGet, "/v1/user/self", http.StatusOK, "./fixtures/get-user.json") | ||||
|  | ||||
| 	user, err := client.GetUser("self") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	expected := &User{ | ||||
| 		ID:                      987654321, | ||||
| 		Login:                   "lego@example.com", | ||||
| 		Active:                  true, | ||||
| 		CreateTime:              1675237889, | ||||
| 		Group:                   "users", | ||||
| 		Email:                   "lego@example.com", | ||||
| 		Phone:                   "+123456789", | ||||
| 		ContactPerson:           "", | ||||
| 		AwaitingTosConfirmation: "1", | ||||
| 		UserLanguage:            "sk-SK", | ||||
| 		Credit:                  0, | ||||
| 		VerifyURL:               "https://rest.websupport.sk/v1/user/verify/key/xxx", | ||||
| 		Billing: []Billing{{ | ||||
| 			ID:        1099970, | ||||
| 			Profile:   "default", | ||||
| 			IsDefault: true, | ||||
| 			Name:      "asdsdfs", | ||||
| 			City:      "Žilina", | ||||
| 			Street:    "asddfsdfsdf", | ||||
| 			Zip:       "01234", | ||||
| 			Country:   "sk", | ||||
| 		}}, | ||||
| 		Market: Market{Name: "Slovakia", Identifier: "sk", Currency: "EUR"}, | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, expected, user) | ||||
| } | ||||
|  | ||||
| func TestClient_ListRecords(t *testing.T) { | ||||
| 	client := setupTest(t, http.MethodGet, "/v1/user/self/zone/example.com/record", http.StatusOK, "./fixtures/list-records.json") | ||||
|  | ||||
| 	resp, err := client.ListRecords("example.com") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	expected := &ListResponse{ | ||||
| 		Items: []Record{ | ||||
| 			{ | ||||
| 				ID:      1, | ||||
| 				Type:    "A", | ||||
| 				Name:    "@", | ||||
| 				Content: "37.9.169.99", | ||||
| 				TTL:     600, | ||||
| 			}, { | ||||
| 				ID:      2, | ||||
| 				Type:    "NS", | ||||
| 				Name:    "@", | ||||
| 				Content: "ns1.scaledo.com", | ||||
| 				TTL:     600, | ||||
| 			}, | ||||
| 		}, | ||||
| 		Pager: Pager{Page: 1, PageSize: 0, Items: 2}, | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, expected, resp) | ||||
| } | ||||
|  | ||||
| func TestClient_AddRecord(t *testing.T) { | ||||
| 	client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusCreated, "./fixtures/add-record.json") | ||||
|  | ||||
| 	record := Record{ | ||||
| 		Type:    "TXT", | ||||
| 		Name:    "_acme-challenge", | ||||
| 		Content: "txttxttxt", | ||||
| 		TTL:     600, | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.AddRecord("example.com", record) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	expected := &Response{ | ||||
| 		Status: "success", | ||||
| 		Item: &Record{ | ||||
| 			ID:      4, | ||||
| 			Type:    "A", | ||||
| 			Name:    "@", | ||||
| 			Content: "1.2.3.4", | ||||
| 			TTL:     600, | ||||
| 			Zone: &Zone{ | ||||
| 				ID:         1, | ||||
| 				Name:       "example.com", | ||||
| 				UpdateTime: 1381169608, | ||||
| 			}, | ||||
| 		}, | ||||
| 		Errors: json.RawMessage("[]"), | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, expected, resp) | ||||
| } | ||||
|  | ||||
| func TestClient_AddRecord_error_400(t *testing.T) { | ||||
| 	client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusBadRequest, "./fixtures/add-record-error-400.json") | ||||
|  | ||||
| 	record := Record{ | ||||
| 		Type:    "TXT", | ||||
| 		Name:    "_acme-challenge", | ||||
| 		Content: "txttxttxt", | ||||
| 		TTL:     600, | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.AddRecord("example.com", record) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	assert.Equal(t, "error", resp.Status) | ||||
|  | ||||
| 	expectedRecord := &Record{ | ||||
| 		ID:      0, | ||||
| 		Type:    "A", | ||||
| 		Name:    "something bad !@#$%^&*(", | ||||
| 		Content: "123.456.789.123", | ||||
| 		TTL:     600, | ||||
| 		Zone: &Zone{ | ||||
| 			ID:         1, | ||||
| 			Name:       "scaledo.com", | ||||
| 			UpdateTime: 1381169608, | ||||
| 		}, | ||||
| 	} | ||||
| 	assert.Equal(t, expectedRecord, resp.Item) | ||||
|  | ||||
| 	expected := &Errors{Name: []string{"Invalid input."}, Content: []string{"Wrong IP address format"}} | ||||
| 	assert.Equal(t, expected, ParseError(resp)) | ||||
| } | ||||
|  | ||||
| func TestClient_AddRecord_error_404(t *testing.T) { | ||||
| 	client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusNotFound, "./fixtures/add-record-error-404.json") | ||||
|  | ||||
| 	record := Record{ | ||||
| 		Type:    "TXT", | ||||
| 		Name:    "_acme-challenge", | ||||
| 		Content: "txttxttxt", | ||||
| 		TTL:     600, | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.AddRecord("example.com", record) | ||||
| 	require.Error(t, err) | ||||
|  | ||||
| 	assert.Nil(t, resp) | ||||
| } | ||||
|  | ||||
| func TestClient_DeleteRecord(t *testing.T) { | ||||
| 	client := setupTest(t, http.MethodDelete, "/v1/user/self/zone/example.com/record/123", http.StatusOK, "./fixtures/delete-record.json") | ||||
|  | ||||
| 	resp, err := client.DeleteRecord("example.com", 123) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	expected := &Response{ | ||||
| 		Status: "success", | ||||
| 		Item: &Record{ | ||||
| 			ID:      1, | ||||
| 			Type:    "A", | ||||
| 			Name:    "@", | ||||
| 			Content: "1.2.3.4", | ||||
| 			TTL:     600, | ||||
| 			Zone: &Zone{ | ||||
| 				ID:         1, | ||||
| 				Name:       "scaledo.com", | ||||
| 				UpdateTime: 1381316081, | ||||
| 			}, | ||||
| 		}, | ||||
| 		Errors: json.RawMessage("[]"), | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, expected, resp) | ||||
| } | ||||
|  | ||||
| func TestClient_DeleteRecord_error(t *testing.T) { | ||||
| 	client := setupTest(t, http.MethodDelete, "/v1/user/self/zone/example.com/record/123", http.StatusNotFound, "./fixtures/delete-record-error-404.json") | ||||
|  | ||||
| 	resp, err := client.DeleteRecord("example.com", 123) | ||||
| 	require.Error(t, err) | ||||
|  | ||||
| 	assert.Nil(t, resp) | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| { | ||||
|   "status": "error", | ||||
|   "item": { | ||||
|     "id": null, | ||||
|     "type": "A", | ||||
|     "name": "something bad !@#$%^&*(", | ||||
|     "content": "123.456.789.123", | ||||
|     "ttl": 600, | ||||
|     "prio": null, | ||||
|     "weight": null, | ||||
|     "port": null, | ||||
|     "zone": { | ||||
|       "id": 1, | ||||
|       "name": "scaledo.com", | ||||
|       "updateTime": 1381169608 | ||||
|     } | ||||
|   }, | ||||
|   "errors": { | ||||
|     "content": [ | ||||
|       "Wrong IP address format" | ||||
|     ], | ||||
|     "name": [ | ||||
|       "Invalid input." | ||||
|     ] | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "code": 404, | ||||
|   "message": "Zone not found" | ||||
| } | ||||
							
								
								
									
										19
									
								
								providers/dns/websupport/internal/fixtures/add-record.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								providers/dns/websupport/internal/fixtures/add-record.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| { | ||||
|   "status": "success", | ||||
|   "item": { | ||||
|     "id": 4, | ||||
|     "type": "A", | ||||
|     "name": "@", | ||||
|     "content": "1.2.3.4", | ||||
|     "ttl": 600, | ||||
|     "prio": null, | ||||
|     "weight": null, | ||||
|     "port": null, | ||||
|     "zone": { | ||||
|       "id": 1, | ||||
|       "name": "example.com", | ||||
|       "updateTime": 1381169608 | ||||
|     } | ||||
|   }, | ||||
|   "errors": [] | ||||
| } | ||||
| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "code": 404, | ||||
|   "message": "Record not found" | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| { | ||||
|   "status": "success", | ||||
|   "item": { | ||||
|     "id": 1, | ||||
|     "type": "A", | ||||
|     "name": "@", | ||||
|     "content": "1.2.3.4", | ||||
|     "ttl": 600, | ||||
|     "prio": null, | ||||
|     "weight": null, | ||||
|     "port": null, | ||||
|     "zone": { | ||||
|       "id": 1, | ||||
|       "name": "scaledo.com", | ||||
|       "updateTime": 1381316081 | ||||
|     } | ||||
|   }, | ||||
|   "errors": [] | ||||
| } | ||||
							
								
								
									
										12
									
								
								providers/dns/websupport/internal/fixtures/get-record.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								providers/dns/websupport/internal/fixtures/get-record.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| { | ||||
|   "id": 69966832, | ||||
|   "type": "TXT", | ||||
|   "name": "_acme-challenge", | ||||
|   "content": "txttxttxt", | ||||
|   "ttl": 600, | ||||
|   "zone": { | ||||
|     "id": 0, | ||||
|     "name": "example.com", | ||||
|     "updateTime": 1675240207 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										36
									
								
								providers/dns/websupport/internal/fixtures/get-user.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								providers/dns/websupport/internal/fixtures/get-user.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| { | ||||
|   "id": 987654321, | ||||
|   "login": "lego@example.com", | ||||
|   "parentId": null, | ||||
|   "active": true, | ||||
|   "createTime": 1675237889, | ||||
|   "group": "users", | ||||
|   "email": "lego@example.com", | ||||
|   "phone": "+123456789", | ||||
|   "contactPerson": "", | ||||
|   "awaitingTosConfirmation": "1", | ||||
|   "userLanguage": "sk-SK", | ||||
|   "credit": 0, | ||||
|   "verifyUrl": "https:\/\/rest.websupport.sk\/v1\/user\/verify\/key\/xxx", | ||||
|   "billing": [ | ||||
|     { | ||||
|       "id": 1099970, | ||||
|       "profile": "default", | ||||
|       "isDefault": true, | ||||
|       "name": "asdsdfs", | ||||
|       "city": "\u017dilina", | ||||
|       "street": "asddfsdfsdf", | ||||
|       "companyRegId": null, | ||||
|       "taxId": null, | ||||
|       "vatId": null, | ||||
|       "zip": "01234", | ||||
|       "country": "sk", | ||||
|       "isic": "" | ||||
|     } | ||||
|   ], | ||||
|   "market": { | ||||
|     "name": "Slovakia", | ||||
|     "identifier": "sk", | ||||
|     "currency": "EUR" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										29
									
								
								providers/dns/websupport/internal/fixtures/list-records.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								providers/dns/websupport/internal/fixtures/list-records.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| { | ||||
|   "items": [ | ||||
|     { | ||||
|       "id": 1, | ||||
|       "type": "A", | ||||
|       "name": "@", | ||||
|       "content": "37.9.169.99", | ||||
|       "ttl": 600, | ||||
|       "prio": null, | ||||
|       "weight": null, | ||||
|       "port": null | ||||
|     }, | ||||
|     { | ||||
|       "id": 2, | ||||
|       "type": "NS", | ||||
|       "name": "@", | ||||
|       "content": "ns1.scaledo.com", | ||||
|       "ttl": 600, | ||||
|       "prio": null, | ||||
|       "weight": null, | ||||
|       "port": null | ||||
|     } | ||||
|   ], | ||||
|   "pager": { | ||||
|     "page": 1, | ||||
|     "pagesize": 0, | ||||
|     "items": 2 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										121
									
								
								providers/dns/websupport/internal/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								providers/dns/websupport/internal/types.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| package internal | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| type APIError struct { | ||||
| 	Code    int    `json:"code"` | ||||
| 	Message string `json:"message"` | ||||
| } | ||||
|  | ||||
| func (a *APIError) Error() string { | ||||
| 	return fmt.Sprintf("%d: %s", a.Code, a.Message) | ||||
| } | ||||
|  | ||||
| type Record struct { | ||||
| 	ID      int    `json:"id,omitempty"` | ||||
| 	Type    string `json:"type,omitempty"` | ||||
| 	Name    string `json:"name,omitempty"` // subdomain name or @ if you don't want subdomain | ||||
| 	Content string `json:"content,omitempty"` | ||||
| 	TTL     int    `json:"ttl,omitempty"` // default 600 | ||||
| 	Zone    *Zone  `json:"zone"` | ||||
| } | ||||
|  | ||||
| type Zone struct { | ||||
| 	ID         int    `json:"id"` | ||||
| 	Name       string `json:"name"` | ||||
| 	UpdateTime int    `json:"updateTime"` | ||||
| } | ||||
|  | ||||
| type Response struct { | ||||
| 	Status string          `json:"status"` | ||||
| 	Item   *Record         `json:"item"` | ||||
| 	Errors json.RawMessage `json:"errors"` | ||||
| } | ||||
|  | ||||
| type ListResponse struct { | ||||
| 	Items []Record `json:"items"` | ||||
| 	Pager Pager    `json:"pager"` | ||||
| } | ||||
|  | ||||
| type Pager struct { | ||||
| 	Page     int `json:"page"` | ||||
| 	PageSize int `json:"pagesize"` | ||||
| 	Items    int `json:"items"` | ||||
| } | ||||
|  | ||||
| type Errors struct { | ||||
| 	Name    []string `json:"name"` | ||||
| 	Content []string `json:"content"` | ||||
| } | ||||
|  | ||||
| func (e *Errors) Error() string { | ||||
| 	var msg string | ||||
| 	for i, s := range e.Name { | ||||
| 		msg += s | ||||
| 		if i != len(e.Name)-1 { | ||||
| 			msg += ": " | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i, s := range e.Content { | ||||
| 		msg += s | ||||
| 		if i != len(e.Content)-1 { | ||||
| 			msg += ": " | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return msg | ||||
| } | ||||
|  | ||||
| // ParseError extract error from Response. | ||||
| func ParseError(resp *Response) error { | ||||
| 	var apiError Errors | ||||
| 	err := json.Unmarshal(resp.Errors, &apiError) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return &apiError | ||||
| } | ||||
|  | ||||
| type User struct { | ||||
| 	ID                      int       `json:"id"` | ||||
| 	Login                   string    `json:"login"` | ||||
| 	ParentID                int       `json:"parentId"` | ||||
| 	Active                  bool      `json:"active"` | ||||
| 	CreateTime              int       `json:"createTime"` | ||||
| 	Group                   string    `json:"group"` | ||||
| 	Email                   string    `json:"email"` | ||||
| 	Phone                   string    `json:"phone"` | ||||
| 	ContactPerson           string    `json:"contactPerson"` | ||||
| 	AwaitingTosConfirmation string    `json:"awaitingTosConfirmation"` | ||||
| 	UserLanguage            string    `json:"userLanguage"` | ||||
| 	Credit                  int       `json:"credit"` | ||||
| 	VerifyURL               string    `json:"verifyUrl"` | ||||
| 	Billing                 []Billing `json:"billing"` | ||||
| 	Market                  Market    `json:"market"` | ||||
| } | ||||
|  | ||||
| type Billing struct { | ||||
| 	ID           int    `json:"id"` | ||||
| 	Profile      string `json:"profile"` | ||||
| 	IsDefault    bool   `json:"isDefault"` | ||||
| 	Name         string `json:"name"` | ||||
| 	City         string `json:"city"` | ||||
| 	Street       string `json:"street"` | ||||
| 	CompanyRegID int    `json:"companyRegId"` | ||||
| 	TaxID        int    `json:"taxId"` | ||||
| 	VatID        int    `json:"vatId"` | ||||
| 	Zip          string `json:"zip"` | ||||
| 	Country      string `json:"country"` | ||||
| 	ISIC         string `json:"isic"` | ||||
| } | ||||
|  | ||||
| type Market struct { | ||||
| 	Name       string `json:"name"` | ||||
| 	Identifier string `json:"identifier"` | ||||
| 	Currency   string `json:"currency"` | ||||
| } | ||||
							
								
								
									
										194
									
								
								providers/dns/websupport/websupport.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								providers/dns/websupport/websupport.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,194 @@ | ||||
| // Package websupport implements a DNS provider for solving the DNS-01 challenge using Websupport. | ||||
| package websupport | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"sync" | ||||
| 	"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/websupport/internal" | ||||
| ) | ||||
|  | ||||
| const defaultTTL = 600 | ||||
|  | ||||
| // Environment variables names. | ||||
| const ( | ||||
| 	envNamespace = "WEBSUPPORT_" | ||||
|  | ||||
| 	EnvAPIKey = envNamespace + "API_KEY" | ||||
| 	EnvSecret = envNamespace + "SECRET" | ||||
|  | ||||
| 	EnvTTL                = envNamespace + "TTL" | ||||
| 	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" | ||||
| 	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL" | ||||
| 	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT" | ||||
| 	EnvSequenceInterval   = envNamespace + "SEQUENCE_INTERVAL" | ||||
| ) | ||||
|  | ||||
| // Config is used to configure the creation of the DNSProvider. | ||||
| type Config struct { | ||||
| 	APIKey string | ||||
| 	Secret string | ||||
|  | ||||
| 	PropagationTimeout time.Duration | ||||
| 	PollingInterval    time.Duration | ||||
| 	SequenceInterval   time.Duration | ||||
| 	TTL                int | ||||
| 	HTTPClient         *http.Client | ||||
| } | ||||
|  | ||||
| // NewDefaultConfig returns a default configuration for the DNSProvider. | ||||
| func NewDefaultConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		TTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL), | ||||
| 		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), | ||||
| 		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), | ||||
| 		SequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), | ||||
| 		HTTPClient: &http.Client{ | ||||
| 			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // DNSProvider implements the challenge.Provider interface. | ||||
| type DNSProvider struct { | ||||
| 	config *Config | ||||
| 	client *internal.Client | ||||
|  | ||||
| 	recordIDs   map[string]int | ||||
| 	recordIDsMu sync.Mutex | ||||
| } | ||||
|  | ||||
| // NewDNSProvider returns a DNSProvider instance configured for Websupport. | ||||
| // Credentials must be passed in the environment variables: WEBSUPPORT_API_KEY, WEBSUPPORT_SECRET. | ||||
| func NewDNSProvider() (*DNSProvider, error) { | ||||
| 	values, err := env.Get(EnvAPIKey, EnvSecret) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("websupport: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	config := NewDefaultConfig() | ||||
| 	config.APIKey = values[EnvAPIKey] | ||||
| 	config.Secret = values[EnvSecret] | ||||
|  | ||||
| 	return NewDNSProviderConfig(config) | ||||
| } | ||||
|  | ||||
| // NewDNSProviderConfig return a DNSProvider instance configured for Websupport. | ||||
| func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { | ||||
| 	if config == nil { | ||||
| 		return nil, errors.New("websupport: the configuration of the DNS provider is nil") | ||||
| 	} | ||||
|  | ||||
| 	client, err := internal.NewClient(config.APIKey, config.Secret) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("websupport: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if config.HTTPClient != nil { | ||||
| 		client.HTTPClient = config.HTTPClient | ||||
| 	} | ||||
|  | ||||
| 	return &DNSProvider{ | ||||
| 		config:    config, | ||||
| 		client:    client, | ||||
| 		recordIDs: make(map[string]int), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Present creates a TXT record using the specified parameters. | ||||
| func (d *DNSProvider) Present(domain, token, keyAuth string) error { | ||||
| 	fqdn, value := dns01.GetRecord(domain, keyAuth) | ||||
|  | ||||
| 	authZone, err := dns01.FindZoneByFqdn(fqdn) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("websupport: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	subDomain, err := dns01.ExtractSubDomain(fqdn, authZone) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("websupport: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	record := internal.Record{ | ||||
| 		Type:    "TXT", | ||||
| 		Name:    subDomain, | ||||
| 		Content: value, | ||||
| 		TTL:     d.config.TTL, | ||||
| 	} | ||||
|  | ||||
| 	resp, err := d.client.AddRecord(dns01.UnFqdn(authZone), record) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("websupport: add record: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if resp.Status == internal.StatusSuccess { | ||||
| 		d.recordIDsMu.Lock() | ||||
| 		d.recordIDs[token] = resp.Item.ID | ||||
| 		d.recordIDsMu.Unlock() | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	err = internal.ParseError(resp) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("websupport: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // CleanUp removes the TXT record matching the specified parameters. | ||||
| func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { | ||||
| 	fqdn, _ := dns01.GetRecord(domain, keyAuth) | ||||
|  | ||||
| 	authZone, err := dns01.FindZoneByFqdn(fqdn) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("websupport: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// gets the record's unique ID | ||||
| 	d.recordIDsMu.Lock() | ||||
| 	recordID, ok := d.recordIDs[token] | ||||
| 	d.recordIDsMu.Unlock() | ||||
| 	if !ok { | ||||
| 		return fmt.Errorf("websupport: unknown record ID for '%s' '%s'", fqdn, token) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := d.client.DeleteRecord(dns01.UnFqdn(authZone), recordID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("websupport: delete record: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// deletes record ID from map | ||||
| 	d.recordIDsMu.Lock() | ||||
| 	delete(d.recordIDs, token) | ||||
| 	d.recordIDsMu.Unlock() | ||||
|  | ||||
| 	if resp.Status == internal.StatusSuccess { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	err = internal.ParseError(resp) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("websupport: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| // Sequential All DNS challenges for this provider will be resolved sequentially. | ||||
| // Returns the interval between each iteration. | ||||
| func (d *DNSProvider) Sequential() time.Duration { | ||||
| 	return d.config.SequenceInterval | ||||
| } | ||||
							
								
								
									
										25
									
								
								providers/dns/websupport/websupport.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								providers/dns/websupport/websupport.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| Name = "Websupport" | ||||
| Description = '''''' | ||||
| URL = "https://websupport.sk" | ||||
| Code = "websupport" | ||||
| Since = "v4.10.0" | ||||
|  | ||||
| Example = ''' | ||||
| WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \ | ||||
| WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \ | ||||
| lego --email myemail@example.com --dns websupport --domains my.example.org run | ||||
| ''' | ||||
|  | ||||
| [Configuration] | ||||
|   [Configuration.Credentials] | ||||
|     WEBSUPPORT_API_KEY = "API key" | ||||
|     WEBSUPPORT_SECRET = "API secret" | ||||
|   [Configuration.Additional] | ||||
|     WEBSUPPORT_POLLING_INTERVAL = "Time between DNS propagation check" | ||||
|     WEBSUPPORT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" | ||||
|     WEBSUPPORT_SEQUENCE_INTERVAL = "Time between sequential requests" | ||||
|     WEBSUPPORT_TTL = "The TTL of the TXT record used for the DNS challenge" | ||||
|     WEBSUPPORT_HTTP_TIMEOUT = "API request timeout" | ||||
|  | ||||
| [Links] | ||||
|   API = "https://rest.websupport.sk/docs/v1.zone" | ||||
							
								
								
									
										141
									
								
								providers/dns/websupport/websupport_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								providers/dns/websupport/websupport_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| package websupport | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/go-acme/lego/v4/platform/tester" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| const envDomain = envNamespace + "DOMAIN" | ||||
|  | ||||
| var envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret).WithDomain(envDomain) | ||||
|  | ||||
| func TestNewDNSProvider(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		desc     string | ||||
| 		envVars  map[string]string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc: "success", | ||||
| 			envVars: map[string]string{ | ||||
| 				EnvAPIKey: "key", | ||||
| 				EnvSecret: "secret", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "missing API key", | ||||
| 			envVars: map[string]string{ | ||||
| 				EnvSecret: "secret", | ||||
| 			}, | ||||
| 			expected: "websupport: some credentials information are missing: WEBSUPPORT_API_KEY", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "missing secret", | ||||
| 			envVars: map[string]string{ | ||||
| 				EnvAPIKey: "key", | ||||
| 			}, | ||||
| 			expected: "websupport: some credentials information are missing: WEBSUPPORT_SECRET", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "missing credentials", | ||||
| 			envVars:  map[string]string{}, | ||||
| 			expected: "websupport: some credentials information are missing: WEBSUPPORT_API_KEY,WEBSUPPORT_SECRET", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	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 | ||||
| 		secret   string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:   "success", | ||||
| 			apiKey: "key", | ||||
| 			secret: "secret", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "missing API key", | ||||
| 			secret:   "secret", | ||||
| 			expected: "websupport: credentials missing", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "missing secret", | ||||
| 			apiKey:   "key", | ||||
| 			expected: "websupport: credentials missing", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "missing credentials", | ||||
| 			expected: "websupport: credentials missing", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testCases { | ||||
| 		t.Run(test.desc, func(t *testing.T) { | ||||
| 			config := NewDefaultConfig() | ||||
| 			config.APIKey = test.apiKey | ||||
| 			config.Secret = test.secret | ||||
|  | ||||
| 			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) | ||||
|  | ||||
| 	err = provider.CleanUp(envTest.GetDomain(), "", "123d==") | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user