mirror of
				https://github.com/go-acme/lego.git
				synced 2025-10-31 16:37:41 +02:00 
			
		
		
		
	Add DNS provider for wedos (#1385)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
		| @@ -66,7 +66,7 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | ||||
| | [reg.ru](https://go-acme.github.io/lego/dns/regru/)                             | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/)                          | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/)                  | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/)                 | | ||||
| | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/)                        | [Selectel](https://go-acme.github.io/lego/dns/selectel/)                        | [Servercow](https://go-acme.github.io/lego/dns/servercow/)                      | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/)                      | | ||||
| | [TransIP](https://go-acme.github.io/lego/dns/transip/)                          | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/)                          | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/)                 | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/)                        | | ||||
| | [Vscale](https://go-acme.github.io/lego/dns/vscale/)                            | [Vultr](https://go-acme.github.io/lego/dns/vultr/)                              | [Yandex](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/)                              | [WEDOS](https://go-acme.github.io/lego/dns/wedos/)                              | [Yandex](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 --> | ||||
|   | ||||
| @@ -95,6 +95,7 @@ func allDNSCodes() string { | ||||
| 		"vinyldns", | ||||
| 		"vscale", | ||||
| 		"vultr", | ||||
| 		"wedos", | ||||
| 		"yandex", | ||||
| 		"zoneee", | ||||
| 		"zonomi", | ||||
| @@ -1830,6 +1831,27 @@ func displayDNSHelp(name string) error { | ||||
| 		ew.writeln() | ||||
| 		ew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`) | ||||
|  | ||||
| 	case "wedos": | ||||
| 		// generated from: providers/dns/wedos/wedos.toml | ||||
| 		ew.writeln(`Configuration for WEDOS.`) | ||||
| 		ew.writeln(`Code:	'wedos'`) | ||||
| 		ew.writeln(`Since:	'v4.4.0'`) | ||||
| 		ew.writeln() | ||||
|  | ||||
| 		ew.writeln(`Credentials:`) | ||||
| 		ew.writeln(`	- "WEDOS_USERNAME":	Username is the same as for the admin account`) | ||||
| 		ew.writeln(`	- "WEDOS_WAPI_PASSWORD":	Password needs to be generated and IP allowed in the admin interface`) | ||||
| 		ew.writeln() | ||||
|  | ||||
| 		ew.writeln(`Additional Configuration:`) | ||||
| 		ew.writeln(`	- "WEDOS_HTTP_TIMEOUT":	API request timeout`) | ||||
| 		ew.writeln(`	- "WEDOS_POLLING_INTERVAL":	Time between DNS propagation check`) | ||||
| 		ew.writeln(`	- "WEDOS_PROPAGATION_TIMEOUT":	Maximum waiting time for DNS propagation`) | ||||
| 		ew.writeln(`	- "WEDOS_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/wedos`) | ||||
|  | ||||
| 	case "yandex": | ||||
| 		// generated from: providers/dns/yandex/yandex.toml | ||||
| 		ew.writeln(`Configuration for Yandex.`) | ||||
|   | ||||
							
								
								
									
										64
									
								
								docs/content/dns/zz_gen_wedos.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								docs/content/dns/zz_gen_wedos.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| --- | ||||
| title: "WEDOS" | ||||
| date: 2019-03-03T16:39:46+01:00 | ||||
| draft: false | ||||
| slug: wedos | ||||
| --- | ||||
|  | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
| <!-- providers/dns/wedos/wedos.toml --> | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
|  | ||||
| Since: v4.4.0 | ||||
|  | ||||
| Configuration for [WEDOS](https://www.wedos.com). | ||||
|  | ||||
|  | ||||
| <!--more--> | ||||
|  | ||||
| - Code: `wedos` | ||||
|  | ||||
| Here is an example bash command using the WEDOS provider: | ||||
|  | ||||
| ```bash | ||||
| WEDOS_USERNAME=xxxxxxxx \ | ||||
| WEDOS_WAPI_PASSWORD=xxxxxxxx \ | ||||
| lego -email myemail@example.com --dns wedos --domains my.example.org -run | ||||
| ``` | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Credentials | ||||
|  | ||||
| | Environment Variable Name | Description | | ||||
| |-----------------------|-------------| | ||||
| | `WEDOS_USERNAME` | Username is the same as for the admin account | | ||||
| | `WEDOS_WAPI_PASSWORD` | Password needs to be generated and IP allowed in the admin interface | | ||||
|  | ||||
| The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. | ||||
| More information [here](/lego/dns/#configuration-and-credentials). | ||||
|  | ||||
|  | ||||
| ## Additional Configuration | ||||
|  | ||||
| | Environment Variable Name | Description | | ||||
| |--------------------------------|-------------| | ||||
| | `WEDOS_HTTP_TIMEOUT` | API request timeout | | ||||
| | `WEDOS_POLLING_INTERVAL` | Time between DNS propagation check | | ||||
| | `WEDOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | ||||
| | `WEDOS_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](/lego/dns/#configuration-and-credentials). | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## More information | ||||
|  | ||||
| - [API documentation](https://kb.wedos.com/en/kategorie/wapi-api-interface/wdns-en/) | ||||
|  | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
| <!-- providers/dns/wedos/wedos.toml --> | ||||
| <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> | ||||
| @@ -86,6 +86,7 @@ import ( | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/vinyldns" | ||||
| 	"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/wedos" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/yandex" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/zoneee" | ||||
| 	"github.com/go-acme/lego/v4/providers/dns/zonomi" | ||||
| @@ -258,6 +259,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { | ||||
| 		return vinyldns.NewDNSProvider() | ||||
| 	case "vscale": | ||||
| 		return vscale.NewDNSProvider() | ||||
| 	case "wedos": | ||||
| 		return wedos.NewDNSProvider() | ||||
| 	case "yandex": | ||||
| 		return yandex.NewDNSProvider() | ||||
| 	case "zoneee": | ||||
|   | ||||
							
								
								
									
										215
									
								
								providers/dns/wedos/internal/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								providers/dns/wedos/internal/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| package internal | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/go-acme/lego/v4/challenge/dns01" | ||||
| ) | ||||
|  | ||||
| const baseURL = "https://api.wedos.com/wapi/json" | ||||
|  | ||||
| const codeOk = 1000 | ||||
|  | ||||
| const ( | ||||
| 	commandPing            = "ping" | ||||
| 	commandDNSDomainCommit = "dns-domain-commit" | ||||
| 	commandDNSRowsList     = "dns-rows-list" | ||||
| 	commandDNSRowDelete    = "dns-row-delete" | ||||
| 	commandDNSRowAdd       = "dns-row-add" | ||||
| 	commandDNSRowUpdate    = "dns-row-update" | ||||
| ) | ||||
|  | ||||
| type ResponsePayload struct { | ||||
| 	Code        int             `json:"code,omitempty"` | ||||
| 	Result      string          `json:"result,omitempty"` | ||||
| 	Timestamp   int             `json:"timestamp,omitempty"` | ||||
| 	SvTRID      string          `json:"svTRID,omitempty"` | ||||
| 	Command     string          `json:"command,omitempty"` | ||||
| 	Data        json.RawMessage `json:"data"` | ||||
| 	DNSRowsList []DNSRow | ||||
| } | ||||
|  | ||||
| type DNSRow struct { | ||||
| 	ID     string      `json:"ID,omitempty"` | ||||
| 	Domain string      `json:"domain,omitempty"` | ||||
| 	Name   string      `json:"name,omitempty"` | ||||
| 	TTL    json.Number `json:"ttl,omitempty" type:"integer"` | ||||
| 	Type   string      `json:"rdtype,omitempty"` | ||||
| 	Data   string      `json:"rdata"` | ||||
| } | ||||
|  | ||||
| type APIRequest struct { | ||||
| 	User    string      `json:"user,omitempty"` | ||||
| 	Auth    string      `json:"auth,omitempty"` | ||||
| 	Command string      `json:"command,omitempty"` | ||||
| 	Data    interface{} `json:"data,omitempty"` | ||||
| } | ||||
|  | ||||
| type Client struct { | ||||
| 	username   string | ||||
| 	password   string | ||||
| 	baseURL    string | ||||
| 	HTTPClient *http.Client | ||||
| } | ||||
|  | ||||
| func NewClient(username string, password string) *Client { | ||||
| 	return &Client{ | ||||
| 		username:   username, | ||||
| 		password:   password, | ||||
| 		baseURL:    baseURL, | ||||
| 		HTTPClient: &http.Client{Timeout: 10 * time.Second}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetRecords lists all the records in the zone. | ||||
| // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-rows-list/ | ||||
| func (c *Client) GetRecords(ctx context.Context, zone string) ([]DNSRow, error) { | ||||
| 	payload := map[string]interface{}{ | ||||
| 		"domain": dns01.UnFqdn(zone), | ||||
| 	} | ||||
|  | ||||
| 	resp, err := c.do(ctx, commandDNSRowsList, payload) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	arrayWrapper := struct { | ||||
| 		Rows []DNSRow `json:"row"` | ||||
| 	}{} | ||||
|  | ||||
| 	err = json.Unmarshal(resp.Data, &arrayWrapper) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return arrayWrapper.Rows, err | ||||
| } | ||||
|  | ||||
| // AddRecord adds a record in the zone, either by updating existing records or creating new ones. | ||||
| // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-add-row/ | ||||
| // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-update/ | ||||
| func (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) error { | ||||
| 	payload := DNSRow{ | ||||
| 		Domain: dns01.UnFqdn(zone), | ||||
| 		TTL:    record.TTL, | ||||
| 		Type:   record.Type, | ||||
| 		Data:   record.Data, | ||||
| 	} | ||||
|  | ||||
| 	cmd := commandDNSRowAdd | ||||
| 	if record.ID == "" { | ||||
| 		payload.Name = record.Name | ||||
| 	} else { | ||||
| 		cmd = commandDNSRowUpdate | ||||
| 		payload.ID = record.ID | ||||
| 	} | ||||
|  | ||||
| 	_, err := c.do(ctx, cmd, payload) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DeleteRecord deletes a record from the zone. | ||||
| // If a record does not have an ID, it will be looked up. | ||||
| // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-delete/ | ||||
| func (c *Client) DeleteRecord(ctx context.Context, zone string, recordID string) error { | ||||
| 	payload := DNSRow{ | ||||
| 		Domain: dns01.UnFqdn(zone), | ||||
| 		ID:     recordID, | ||||
| 	} | ||||
|  | ||||
| 	_, err := c.do(ctx, commandDNSRowDelete, payload) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Commit not really required, all changes will be auto-committed after 5 minutes. | ||||
| // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-domain-commit/ | ||||
| func (c *Client) Commit(ctx context.Context, zone string) error { | ||||
| 	payload := map[string]interface{}{ | ||||
| 		"name": dns01.UnFqdn(zone), | ||||
| 	} | ||||
|  | ||||
| 	_, err := c.do(ctx, commandDNSDomainCommit, payload) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) Ping(ctx context.Context) error { | ||||
| 	_, err := c.do(ctx, commandPing, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) do(ctx context.Context, command string, payload interface{}) (*ResponsePayload, error) { | ||||
| 	requestObject := map[string]interface{}{ | ||||
| 		"request": APIRequest{ | ||||
| 			User:    c.username, | ||||
| 			Auth:    authToken(c.username, c.password), | ||||
| 			Command: command, | ||||
| 			Data:    payload, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	jsonBytes, err := json.Marshal(requestObject) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	form := url.Values{} | ||||
| 	form.Add("request", string(jsonBytes)) | ||||
|  | ||||
| 	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(form.Encode())) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||||
|  | ||||
| 	resp, err := c.HTTPClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode/100 != 2 { | ||||
| 		return nil, fmt.Errorf("API error, status code: %d", resp.StatusCode) | ||||
| 	} | ||||
|  | ||||
| 	responseWrapper := struct { | ||||
| 		Response ResponsePayload `json:"response"` | ||||
| 	}{} | ||||
|  | ||||
| 	err = json.Unmarshal(body, &responseWrapper) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if responseWrapper.Response.Code != codeOk { | ||||
| 		return nil, fmt.Errorf("wedos responded with error code %d = %s", responseWrapper.Response.Code, responseWrapper.Response.Result) | ||||
| 	} | ||||
|  | ||||
| 	return &responseWrapper.Response, err | ||||
| } | ||||
							
								
								
									
										149
									
								
								providers/dns/wedos/internal/client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								providers/dns/wedos/internal/client_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| package internal | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"regexp" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func setupNew(t *testing.T, expectedForm string, filename string) *Client { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	mux := http.NewServeMux() | ||||
| 	server := httptest.NewServer(mux) | ||||
| 	t.Cleanup(server.Close) | ||||
|  | ||||
| 	mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { | ||||
| 		err := req.ParseForm() | ||||
| 		if err != nil { | ||||
| 			http.Error(rw, err.Error(), http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		exp := regexp.MustCompile(`"auth":"\w+",`) | ||||
|  | ||||
| 		form := req.PostForm.Get("request") | ||||
| 		form = exp.ReplaceAllString(form, `"auth":"xxx",`) | ||||
|  | ||||
| 		if form != expectedForm { | ||||
| 			t.Logf("invalid form data: %s", req.PostForm.Get("request")) | ||||
| 			http.Error(rw, fmt.Sprintf("invalid form data: %s", req.PostForm.Get("request")), http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data, err := ioutil.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename)) | ||||
| 		if err != nil { | ||||
| 			http.Error(rw, err.Error(), http.StatusInternalServerError) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		rw.Header().Set("Content-Type", "application/json") | ||||
| 		_, _ = rw.Write(data) | ||||
| 	}) | ||||
|  | ||||
| 	client := NewClient("user", "secret") | ||||
| 	client.baseURL = server.URL | ||||
|  | ||||
| 	return client | ||||
| } | ||||
|  | ||||
| func TestClient_GetRecords(t *testing.T) { | ||||
| 	expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-rows-list","data":{"domain":"example.com"}}}` | ||||
| 	client := setupNew(t, expectedForm, commandDNSRowsList) | ||||
|  | ||||
| 	records, err := client.GetRecords(context.Background(), "example.com.") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	assert.Len(t, records, 4) | ||||
|  | ||||
| 	expected := []DNSRow{ | ||||
| 		{ | ||||
| 			ID:   "911", | ||||
| 			TTL:  "1800", | ||||
| 			Type: "A", | ||||
| 			Data: "1.2.3.4", | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:   "913", | ||||
| 			TTL:  "1800", | ||||
| 			Type: "MX", | ||||
| 			Data: "1 mail1.wedos.net", | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:   "914", | ||||
| 			TTL:  "1800", | ||||
| 			Type: "MX", | ||||
| 			Data: "10 mailbackup.wedos.net", | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:   "912", | ||||
| 			Name: "*", | ||||
| 			TTL:  "1800", | ||||
| 			Type: "A", | ||||
| 			Data: "1.2.3.4", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, expected, records) | ||||
| } | ||||
|  | ||||
| func TestClient_AddRecord(t *testing.T) { | ||||
| 	expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-add","data":{"domain":"example.com","name":"foo","ttl":1800,"rdtype":"TXT","rdata":"foobar"}}}` | ||||
|  | ||||
| 	client := setupNew(t, expectedForm, commandDNSRowAdd) | ||||
|  | ||||
| 	record := DNSRow{ | ||||
| 		ID:     "", | ||||
| 		Domain: "example.com", | ||||
| 		Name:   "foo", | ||||
| 		TTL:    "1800", | ||||
| 		Type:   "TXT", | ||||
| 		Data:   "foobar", | ||||
| 	} | ||||
|  | ||||
| 	err := client.AddRecord(context.Background(), "example.com.", record) | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
|  | ||||
| func TestClient_AddRecord_update(t *testing.T) { | ||||
| 	expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-update","data":{"ID":"1","domain":"example.com","ttl":1800,"rdtype":"TXT","rdata":"foobar"}}}` | ||||
|  | ||||
| 	client := setupNew(t, expectedForm, commandDNSRowUpdate) | ||||
|  | ||||
| 	record := DNSRow{ | ||||
| 		ID:     "1", | ||||
| 		Domain: "example.com", | ||||
| 		Name:   "foo", | ||||
| 		TTL:    "1800", | ||||
| 		Type:   "TXT", | ||||
| 		Data:   "foobar", | ||||
| 	} | ||||
|  | ||||
| 	err := client.AddRecord(context.Background(), "example.com.", record) | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
|  | ||||
| func TestClient_DeleteRecord(t *testing.T) { | ||||
| 	expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-delete","data":{"ID":"1","domain":"example.com","rdata":""}}}` | ||||
|  | ||||
| 	client := setupNew(t, expectedForm, commandDNSRowDelete) | ||||
|  | ||||
| 	err := client.DeleteRecord(context.Background(), "example.com.", "1") | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
|  | ||||
| func TestClient_Commit(t *testing.T) { | ||||
| 	expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-domain-commit","data":{"name":"example.com"}}}` | ||||
|  | ||||
| 	client := setupNew(t, expectedForm, commandDNSDomainCommit) | ||||
|  | ||||
| 	err := client.Commit(context.Background(), "example.com.") | ||||
| 	require.NoError(t, err) | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "response": { | ||||
|     "code": 1000, | ||||
|     "result": "OK", | ||||
|     "timestamp": 1291192534, | ||||
|     "svTRID": "1291192534.6326.32542.1", | ||||
|     "command": "dns-domain-commit" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								providers/dns/wedos/internal/fixtures/dns-row-add.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								providers/dns/wedos/internal/fixtures/dns-row-add.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "response": { | ||||
|     "code": 1000, | ||||
|     "result": "OK", | ||||
|     "timestamp": 1291210501, | ||||
|     "svTRID": "1291210501.7672.19698.1", | ||||
|     "command": "dns-row-add" | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "response": { | ||||
|     "code": 1000, | ||||
|     "result": "OK", | ||||
|     "timestamp": 1291370821, | ||||
|     "svTRID": "1291370821.1702.7371.1", | ||||
|     "command": "dns-row-delete" | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "response": { | ||||
|     "code": 1000, | ||||
|     "result": "OK", | ||||
|     "timestamp": 1291370821, | ||||
|     "svTRID": "1291370821.1702.7371.1", | ||||
|     "command": "dns-row-update" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										49
									
								
								providers/dns/wedos/internal/fixtures/dns-rows-list.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								providers/dns/wedos/internal/fixtures/dns-rows-list.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| { | ||||
|   "response": { | ||||
|     "code": 1000, | ||||
|     "result": "OK", | ||||
|     "timestamp": 1291194425, | ||||
|     "svTRID": "1291194425.9562.9881.1", | ||||
|     "command": "dns-rows-list", | ||||
|     "data": { | ||||
|       "row": [ | ||||
|         { | ||||
|           "ID": "911", | ||||
|           "name": "", | ||||
|           "ttl": "1800", | ||||
|           "rdtype": "A", | ||||
|           "rdata": "1.2.3.4", | ||||
|           "changed_date": "2010-12-01 09:54:41", | ||||
|           "author_comment": "" | ||||
|         }, | ||||
|         { | ||||
|           "ID": "913", | ||||
|           "name": "", | ||||
|           "ttl": "1800", | ||||
|           "rdtype": "MX", | ||||
|           "rdata": "1 mail1.wedos.net", | ||||
|           "changed_date": "2010-12-01 09:54:54", | ||||
|           "author_comment": "" | ||||
|         }, | ||||
|         { | ||||
|           "ID": "914", | ||||
|           "name": "", | ||||
|           "ttl": "1800", | ||||
|           "rdtype": "MX", | ||||
|           "rdata": "10 mailbackup.wedos.net", | ||||
|           "changed_date": "2010-12-01 09:55:07", | ||||
|           "author_comment": "" | ||||
|         }, | ||||
|         { | ||||
|           "ID": "912", | ||||
|           "name": "*", | ||||
|           "ttl": "1800", | ||||
|           "rdtype": "A", | ||||
|           "rdata": "1.2.3.4", | ||||
|           "changed_date": "2010-12-01 09:54:46", | ||||
|           "author_comment": "" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										73
									
								
								providers/dns/wedos/internal/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								providers/dns/wedos/internal/token.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| package internal | ||||
|  | ||||
| import ( | ||||
| 	"crypto/sha1" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func authToken(userName string, wapiPass string) string { | ||||
| 	return sha1string(userName + sha1string(wapiPass) + czechHourString()) | ||||
| } | ||||
|  | ||||
| func sha1string(txt string) string { | ||||
| 	h := sha1.New() | ||||
| 	_, _ = io.WriteString(h, txt) | ||||
| 	return fmt.Sprintf("%x", h.Sum(nil)) | ||||
| } | ||||
|  | ||||
| func czechHourString() string { | ||||
| 	return formatHour(czechHour()) | ||||
| } | ||||
|  | ||||
| func czechHour() int { | ||||
| 	tryZones := []string{"Europe/Prague", "Europe/Paris", "CET"} | ||||
|  | ||||
| 	for _, zoneName := range tryZones { | ||||
| 		loc, err := time.LoadLocation(zoneName) | ||||
| 		if err == nil { | ||||
| 			return time.Now().In(loc).Hour() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// hopefully this will never be used | ||||
| 	// this is fallback for containers without tzdata installed | ||||
| 	return utcToCet(time.Now().UTC()).Hour() | ||||
| } | ||||
|  | ||||
| func utcToCet(utc time.Time) time.Time { | ||||
| 	// https://en.wikipedia.org/wiki/Central_European_Time | ||||
| 	// As of 2011, all member states of the European Union observe summer time (daylight saving time), | ||||
| 	// from the last Sunday in March to the last Sunday in October. | ||||
| 	// States within the CET area switch to Central European Summer Time (CEST -- UTC+02:00) for the summer.[1] | ||||
| 	utcMonth := utc.Month() | ||||
| 	if utcMonth < time.March || utcMonth > time.October { | ||||
| 		return utc.Add(time.Hour) | ||||
| 	} | ||||
| 	if utcMonth > time.March && utcMonth < time.October { | ||||
| 		return utc.Add(time.Hour * 2) | ||||
| 	} | ||||
|  | ||||
| 	dayOff := 0 | ||||
| 	breaking := time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC) | ||||
| 	for { | ||||
| 		if breaking.Weekday() == time.Sunday { | ||||
| 			break | ||||
| 		} | ||||
| 		dayOff-- | ||||
| 		breaking = time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC) | ||||
| 		if dayOff < -7 { | ||||
| 			panic("safety exit to avoid infinite loop") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (utcMonth == time.March && utc.Before(breaking)) || (utcMonth == time.October && utc.After(breaking)) { | ||||
| 		return utc.Add(time.Hour) | ||||
| 	} | ||||
| 	return utc.Add(time.Hour * 2) | ||||
| } | ||||
|  | ||||
| func formatHour(hour int) string { | ||||
| 	return fmt.Sprintf("%02d", hour) | ||||
| } | ||||
							
								
								
									
										186
									
								
								providers/dns/wedos/wedos.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								providers/dns/wedos/wedos.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| package wedos | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"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/wedos/internal" | ||||
| ) | ||||
|  | ||||
| // Environment variables names. | ||||
| const ( | ||||
| 	envNamespace = "WEDOS_" | ||||
|  | ||||
| 	EnvUsername = envNamespace + "USERNAME" | ||||
| 	EnvPassword = envNamespace + "WAPI_PASSWORD" | ||||
|  | ||||
| 	EnvTTL                = envNamespace + "TTL" | ||||
| 	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" | ||||
| 	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL" | ||||
| 	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT" | ||||
| ) | ||||
|  | ||||
| const minTTL = 5 * 60 // 5 minutes | ||||
|  | ||||
| // Config is used to configure the creation of the DNSProvider. | ||||
| type Config struct { | ||||
| 	Username           string | ||||
| 	Password           string | ||||
| 	PropagationTimeout time.Duration | ||||
| 	PollingInterval    time.Duration | ||||
| 	TTL                int | ||||
| 	HTTPClient         *http.Client | ||||
| } | ||||
|  | ||||
| // NewDefaultConfig returns a default configuration for the DNSProvider. | ||||
| func NewDefaultConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), | ||||
| 		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), | ||||
| 		TTL:                env.GetOrDefaultInt(EnvTTL, minTTL), | ||||
| 		HTTPClient: &http.Client{ | ||||
| 			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // DNSProvider implements the challenge.Provider interface. | ||||
| type DNSProvider struct { | ||||
| 	config *Config | ||||
| 	client *internal.Client | ||||
| } | ||||
|  | ||||
| // NewDNSProvider returns a DNSProvider instance. | ||||
| func NewDNSProvider() (*DNSProvider, error) { | ||||
| 	values, err := env.Get(EnvUsername, EnvPassword) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("wedos: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	config := NewDefaultConfig() | ||||
| 	config.Username = values[EnvUsername] | ||||
| 	config.Password = values[EnvPassword] | ||||
|  | ||||
| 	return NewDNSProviderConfig(config) | ||||
| } | ||||
|  | ||||
| // NewDNSProviderConfig return a DNSProvider. | ||||
| func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { | ||||
| 	if config == nil { | ||||
| 		return nil, errors.New("wedos: the configuration of the DNS provider is nil") | ||||
| 	} | ||||
|  | ||||
| 	if config.Username == "" || config.Password == "" { | ||||
| 		return nil, errors.New("wedos: some credentials information are missing") | ||||
| 	} | ||||
|  | ||||
| 	if config.TTL < minTTL { | ||||
| 		return nil, fmt.Errorf("wedos: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) | ||||
| 	} | ||||
|  | ||||
| 	client := internal.NewClient(config.Username, config.Password) | ||||
|  | ||||
| 	if config.HTTPClient != nil { | ||||
| 		client.HTTPClient = config.HTTPClient | ||||
| 	} | ||||
|  | ||||
| 	return &DNSProvider{config: config, client: client}, nil | ||||
| } | ||||
|  | ||||
| // Timeout returns the timeout and interval to use when checking for DNS 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 | ||||
| } | ||||
|  | ||||
| // Present creates a TXT record to fulfill the dns-01 challenge. | ||||
| func (d *DNSProvider) Present(domain, token, keyAuth string) error { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	fqdn, value := dns01.GetRecord(domain, keyAuth) | ||||
|  | ||||
| 	authZone, err := dns01.FindZoneByFqdn(fqdn) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("wedos: could not determine zone for domain %q: %w", domain, err) | ||||
| 	} | ||||
|  | ||||
| 	subDomain := strings.TrimSuffix(fqdn, authZone) | ||||
|  | ||||
| 	record := internal.DNSRow{ | ||||
| 		Name: subDomain, | ||||
| 		TTL:  json.Number(strconv.Itoa(d.config.TTL)), | ||||
| 		Type: "TXT", | ||||
| 		Data: value, | ||||
| 	} | ||||
|  | ||||
| 	records, err := d.client.GetRecords(ctx, authZone) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("wedos: could not get records for domain %q: %w", domain, err) | ||||
| 	} | ||||
|  | ||||
| 	for _, candidate := range records { | ||||
| 		if candidate.Type == "TXT" && candidate.Name == subDomain && candidate.Data == value { | ||||
| 			record.ID = candidate.ID | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err = d.client.AddRecord(ctx, authZone, record) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("wedos: could not add TXT record for domain %q: %w", domain, err) | ||||
| 	} | ||||
|  | ||||
| 	err = d.client.Commit(ctx, authZone) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("wedos: could not commit TXT record for domain %q: %w", domain, err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // CleanUp removes the TXT record matching the specified parameters. | ||||
| func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	fqdn, value := dns01.GetRecord(domain, keyAuth) | ||||
|  | ||||
| 	authZone, err := dns01.FindZoneByFqdn(fqdn) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("wedos: could not determine zone for domain %q: %w", domain, err) | ||||
| 	} | ||||
|  | ||||
| 	subDomain := strings.TrimSuffix(fqdn, authZone) | ||||
|  | ||||
| 	records, err := d.client.GetRecords(ctx, authZone) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("wedos: could not get records for domain %q: %w", domain, err) | ||||
| 	} | ||||
|  | ||||
| 	for _, candidate := range records { | ||||
| 		if candidate.Type != "TXT" || candidate.Name != subDomain || candidate.Data != value { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		err = d.client.DeleteRecord(ctx, authZone, candidate.ID) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("wedos: could not remove TXT record for domain %q: %w", domain, err) | ||||
| 		} | ||||
|  | ||||
| 		err = d.client.Commit(ctx, authZone) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("wedos: could not commit TXT record for domain %q: %w", domain, err) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										24
									
								
								providers/dns/wedos/wedos.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								providers/dns/wedos/wedos.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| Name = "WEDOS" | ||||
| Description = '''''' | ||||
| URL = "https://www.wedos.com" | ||||
| Code = "wedos" | ||||
| Since = "v4.4.0" | ||||
|  | ||||
| Example = ''' | ||||
| WEDOS_USERNAME=xxxxxxxx \ | ||||
| WEDOS_WAPI_PASSWORD=xxxxxxxx \ | ||||
| lego -email myemail@example.com --dns wedos --domains my.example.org -run | ||||
| ''' | ||||
|  | ||||
| [Configuration] | ||||
|   [Configuration.Credentials] | ||||
|     WEDOS_USERNAME = "Username is the same as for the admin account" | ||||
|     WEDOS_WAPI_PASSWORD = "Password needs to be generated and IP allowed in the admin interface" | ||||
|   [Configuration.Additional] | ||||
|     WEDOS_POLLING_INTERVAL = "Time between DNS propagation check" | ||||
|     WEDOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" | ||||
|     WEDOS_HTTP_TIMEOUT = "API request timeout" | ||||
|     WEDOS_TTL = "The TTL of the TXT record used for the DNS challenge" | ||||
|  | ||||
| [Links] | ||||
|   API = "https://kb.wedos.com/en/kategorie/wapi-api-interface/wdns-en/" | ||||
							
								
								
									
										141
									
								
								providers/dns/wedos/wedos_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								providers/dns/wedos/wedos_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| package wedos | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/go-acme/lego/v4/platform/tester" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| const envDomain = envNamespace + "DOMAIN" | ||||
|  | ||||
| var envTest = tester.NewEnvTest(EnvUsername, EnvPassword). | ||||
| 	WithDomain(envDomain) | ||||
|  | ||||
| func TestNewDNSProvider(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		desc     string | ||||
| 		envVars  map[string]string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc: "success", | ||||
| 			envVars: map[string]string{ | ||||
| 				EnvUsername: "admin@example.com", | ||||
| 				EnvPassword: "secret", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "missing credentials: username", | ||||
| 			envVars: map[string]string{ | ||||
| 				EnvUsername: "", | ||||
| 				EnvPassword: "secret", | ||||
| 			}, | ||||
| 			expected: "wedos: some credentials information are missing: WEDOS_USERNAME", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "missing credentials: password", | ||||
| 			envVars: map[string]string{ | ||||
| 				EnvUsername: "admin@example.com", | ||||
| 				EnvPassword: "", | ||||
| 			}, | ||||
| 			expected: "wedos: some credentials information are missing: WEDOS_WAPI_PASSWORD", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "missing credentials: all", | ||||
| 			envVars: map[string]string{ | ||||
| 				EnvUsername: "", | ||||
| 				EnvPassword: "", | ||||
| 			}, | ||||
| 			expected: "wedos: some credentials information are missing: WEDOS_USERNAME,WEDOS_WAPI_PASSWORD", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 			} else { | ||||
| 				require.EqualError(t, err, test.expected) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNewDNSProviderConfig(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		desc     string | ||||
| 		username string | ||||
| 		password string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:     "success", | ||||
| 			username: "admin@example.com", | ||||
| 			password: "secret", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "missing username", | ||||
| 			password: "secret", | ||||
| 			expected: "wedos: some credentials information are missing", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "missing WAPI password", | ||||
| 			username: "admin@example.com", | ||||
| 			expected: "wedos: some credentials information are missing", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testCases { | ||||
| 		t.Run(test.desc, func(t *testing.T) { | ||||
| 			config := NewDefaultConfig() | ||||
| 			config.Username = test.username | ||||
| 			config.Password = test.password | ||||
|  | ||||
| 			p, err := NewDNSProviderConfig(config) | ||||
|  | ||||
| 			if test.expected == "" { | ||||
| 				require.NoError(t, err) | ||||
| 				require.NotNil(t, p) | ||||
| 				require.NotNil(t, p.config) | ||||
| 			} 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