Add DNS provider for wedos (#1385)

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
lubko 2021-04-13 21:32:15 +02:00 committed by GitHub
parent 7f53f88555
commit 9002e5c4ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 964 additions and 2 deletions

View file

@ -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/) | | [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/) | | [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/) | | [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/) | | [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/) |
| [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | | | | [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 -->

View file

@ -95,6 +95,7 @@ func allDNSCodes() string {
"vinyldns", "vinyldns",
"vscale", "vscale",
"vultr", "vultr",
"wedos",
"yandex", "yandex",
"zoneee", "zoneee",
"zonomi", "zonomi",
@ -1830,6 +1831,27 @@ func displayDNSHelp(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 "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": case "yandex":
// generated from: providers/dns/yandex/yandex.toml // generated from: providers/dns/yandex/yandex.toml
ew.writeln(`Configuration for Yandex.`) ew.writeln(`Configuration for Yandex.`)

View 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. -->

View file

@ -86,6 +86,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/vinyldns" "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/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/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/zoneee" "github.com/go-acme/lego/v4/providers/dns/zoneee"
"github.com/go-acme/lego/v4/providers/dns/zonomi" "github.com/go-acme/lego/v4/providers/dns/zonomi"
@ -258,6 +259,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return vinyldns.NewDNSProvider() return vinyldns.NewDNSProvider()
case "vscale": case "vscale":
return vscale.NewDNSProvider() return vscale.NewDNSProvider()
case "wedos":
return wedos.NewDNSProvider()
case "yandex": case "yandex":
return yandex.NewDNSProvider() return yandex.NewDNSProvider()
case "zoneee": case "zoneee":

View 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
}

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

View file

@ -0,0 +1,9 @@
{
"response": {
"code": 1000,
"result": "OK",
"timestamp": 1291192534,
"svTRID": "1291192534.6326.32542.1",
"command": "dns-domain-commit"
}
}

View file

@ -0,0 +1,9 @@
{
"response": {
"code": 1000,
"result": "OK",
"timestamp": 1291210501,
"svTRID": "1291210501.7672.19698.1",
"command": "dns-row-add"
}
}

View file

@ -0,0 +1,9 @@
{
"response": {
"code": 1000,
"result": "OK",
"timestamp": 1291370821,
"svTRID": "1291370821.1702.7371.1",
"command": "dns-row-delete"
}
}

View file

@ -0,0 +1,9 @@
{
"response": {
"code": 1000,
"result": "OK",
"timestamp": 1291370821,
"svTRID": "1291370821.1702.7371.1",
"command": "dns-row-update"
}
}

View 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": ""
}
]
}
}
}

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

View 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
}

View 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/"

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