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/)                          | | | [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/)                          | | | [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/)                         | | | [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/)                 | | | [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 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/)                            |                                                                                 | | | [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 --> | <!-- END DNS PROVIDERS LIST --> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -119,6 +119,7 @@ func allDNSCodes() string { | |||||||
| 		"vkcloud", | 		"vkcloud", | ||||||
| 		"vscale", | 		"vscale", | ||||||
| 		"vultr", | 		"vultr", | ||||||
|  | 		"websupport", | ||||||
| 		"wedos", | 		"wedos", | ||||||
| 		"yandex", | 		"yandex", | ||||||
| 		"yandexcloud", | 		"yandexcloud", | ||||||
| @@ -2367,6 +2368,28 @@ func displayDNSHelp(w io.Writer, name string) error { | |||||||
| 		ew.writeln() | 		ew.writeln() | ||||||
| 		ew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`) | 		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": | 	case "wedos": | ||||||
| 		// generated from: providers/dns/wedos/wedos.toml | 		// generated from: providers/dns/wedos/wedos.toml | ||||||
| 		ew.writeln(`Configuration for WEDOS.`) | 		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 |   $ lego dnshelp -c code | ||||||
|  |  | ||||||
| Supported DNS providers: | 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 | 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/vkcloud" | ||||||
| 	"github.com/go-acme/lego/v4/providers/dns/vscale" | 	"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/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/wedos" | ||||||
| 	"github.com/go-acme/lego/v4/providers/dns/yandex" | 	"github.com/go-acme/lego/v4/providers/dns/yandex" | ||||||
| 	"github.com/go-acme/lego/v4/providers/dns/yandexcloud" | 	"github.com/go-acme/lego/v4/providers/dns/yandexcloud" | ||||||
| @@ -328,10 +329,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { | |||||||
| 		return vinyldns.NewDNSProvider() | 		return vinyldns.NewDNSProvider() | ||||||
| 	case "vkcloud": | 	case "vkcloud": | ||||||
| 		return vkcloud.NewDNSProvider() | 		return vkcloud.NewDNSProvider() | ||||||
| 	case "vultr": |  | ||||||
| 		return vultr.NewDNSProvider() |  | ||||||
| 	case "vscale": | 	case "vscale": | ||||||
| 		return vscale.NewDNSProvider() | 		return vscale.NewDNSProvider() | ||||||
|  | 	case "vultr": | ||||||
|  | 		return vultr.NewDNSProvider() | ||||||
|  | 	case "websupport": | ||||||
|  | 		return websupport.NewDNSProvider() | ||||||
| 	case "wedos": | 	case "wedos": | ||||||
| 		return wedos.NewDNSProvider() | 		return wedos.NewDNSProvider() | ||||||
| 	case "yandex": | 	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