Selaa lähdekoodia

Fixed initlization of update

Toby Chui 8 kuukautta sitten
vanhempi
commit
8c6e679571

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 186 - 204
mod/geodb/geoipv4.csv


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 357 - 128
mod/geodb/geoipv6.csv


+ 40 - 8
mod/update/update.go

@@ -9,11 +9,11 @@ package update
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
 	"strconv"
 	"strings"
 
-	v308 "imuslab.com/zoraxy/mod/update/v308"
 	"imuslab.com/zoraxy/mod/utils"
 )
 
@@ -22,6 +22,13 @@ import (
 // This function support cross versions updates (e.g. 307 -> 310)
 func RunConfigUpdate(fromVersion int, toVersion int) {
 	versionFile := "./conf/version"
+	isFirstTimeInit, _ := isFirstTimeInitialize("./conf/proxy/")
+	if isFirstTimeInit {
+		//Create version file and exit
+		os.MkdirAll("./conf/", 0775)
+		os.WriteFile(versionFile, []byte(strconv.Itoa(toVersion)), 0775)
+		return
+	}
 	if fromVersion == 0 {
 		//Run auto previous version detection
 		fromVersion = 307
@@ -65,12 +72,37 @@ func GetVersionIntFromVersionNumber(version string) int {
 	return versionInt
 }
 
-func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
-	if fromVersion == 307 && toVersion == 308 {
-		//Updating from v3.0.7 to v3.0.8
-		err := v308.UpdateFrom307To308()
-		if err != nil {
-			panic(err)
-		}
+// Check if the folder "./conf/proxy/" exists and contains files
+func isFirstTimeInitialize(path string) (bool, error) {
+	// Check if the folder exists
+	info, err := os.Stat(path)
+	if os.IsNotExist(err) {
+		// The folder does not exist
+		return true, nil
+	}
+	if err != nil {
+		// Some other error occurred
+		return false, err
+	}
+
+	// Check if it is a directory
+	if !info.IsDir() {
+		// The path is not a directory
+		return false, fmt.Errorf("%s is not a directory", path)
 	}
+
+	// Read the directory contents
+	files, err := ioutil.ReadDir(path)
+	if err != nil {
+		return false, err
+	}
+
+	// Check if the directory is empty
+	if len(files) == 0 {
+		// The folder exists but is empty
+		return true, nil
+	}
+
+	// The folder exists and contains files
+	return false, nil
 }

+ 16 - 0
mod/update/updatelogic.go

@@ -0,0 +1,16 @@
+package update
+
+import v308 "imuslab.com/zoraxy/mod/update/v308"
+
+// Updater Core logic
+func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
+	if fromVersion == 307 && toVersion == 308 {
+		//Updating from v3.0.7 to v3.0.8
+		err := v308.UpdateFrom307To308()
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	//ADD MORE VERSIONS HERE
+}

+ 143 - 0
tools/provider_config_updater/lego/providers/dns/nearlyfreespeech/internal/client.go

@@ -0,0 +1,143 @@
+package internal
+
+import (
+	"context"
+	"crypto/sha1"
+	"encoding/json"
+	"fmt"
+	"io"
+	"math/rand"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-acme/lego/v4/challenge/dns01"
+	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
+	querystring "github.com/google/go-querystring/query"
+)
+
+const apiURL = "https://api.nearlyfreespeech.net"
+
+const authenticationHeader = "X-NFSN-Authentication"
+
+const saltBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+
+type Client struct {
+	login  string
+	apiKey string
+
+	signer *Signer
+
+	baseURL    *url.URL
+	HTTPClient *http.Client
+}
+
+func NewClient(login string, apiKey string) *Client {
+	baseURL, _ := url.Parse(apiURL)
+
+	return &Client{
+		login:      login,
+		apiKey:     apiKey,
+		signer:     NewSigner(),
+		baseURL:    baseURL,
+		HTTPClient: &http.Client{Timeout: 10 * time.Second},
+	}
+}
+
+func (c Client) AddRecord(ctx context.Context, domain string, record Record) error {
+	endpoint := c.baseURL.JoinPath("dns", dns01.UnFqdn(domain), "addRR")
+
+	params, err := querystring.Values(record)
+	if err != nil {
+		return err
+	}
+
+	return c.doRequest(ctx, endpoint, params)
+}
+
+func (c Client) RemoveRecord(ctx context.Context, domain string, record Record) error {
+	endpoint := c.baseURL.JoinPath("dns", dns01.UnFqdn(domain), "removeRR")
+
+	params, err := querystring.Values(record)
+	if err != nil {
+		return err
+	}
+
+	return c.doRequest(ctx, endpoint, params)
+}
+
+func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Values) error {
+	payload := params.Encode()
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(payload))
+	if err != nil {
+		return fmt.Errorf("unable to create request: %w", err)
+	}
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set(authenticationHeader, c.signer.Sign(endpoint.Path, payload, c.login, c.apiKey))
+
+	resp, err := c.HTTPClient.Do(req)
+	if err != nil {
+		return errutils.NewHTTPDoError(req, err)
+	}
+
+	defer func() { _ = resp.Body.Close() }()
+
+	if resp.StatusCode != http.StatusOK {
+		return parseError(req, resp)
+	}
+
+	return nil
+}
+
+func parseError(req *http.Request, resp *http.Response) error {
+	raw, _ := io.ReadAll(resp.Body)
+
+	errAPI := &APIError{}
+	err := json.Unmarshal(raw, errAPI)
+	if err != nil {
+		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
+	}
+
+	return errAPI
+}
+
+type Signer struct {
+	saltShaker func() []byte
+	clock      func() time.Time
+}
+
+func NewSigner() *Signer {
+	return &Signer{saltShaker: getRandomSalt, clock: time.Now}
+}
+
+func (c Signer) Sign(uri string, body, login, apiKey string) string {
+	// Header is "login;timestamp;salt;hash".
+	// hash is SHA1("login;timestamp;salt;api-key;request-uri;body-hash")
+	// and body-hash is SHA1(body).
+
+	bodyHash := sha1.Sum([]byte(body))
+	timestamp := strconv.FormatInt(c.clock().Unix(), 10)
+
+	// Workaround for https://golang.org/issue/58605
+	uri = "/" + strings.TrimLeft(uri, "/")
+
+	salt := c.saltShaker()
+
+	hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", login, timestamp, salt, apiKey, uri, bodyHash)
+
+	return fmt.Sprintf("%s;%s;%s;%02x", login, timestamp, salt, sha1.Sum([]byte(hashInput)))
+}
+
+func getRandomSalt() []byte {
+	// This is the only part of this that needs to be serialized.
+	salt := make([]byte, 16)
+	for i := range 16 {
+		salt[i] = saltBytes[rand.Intn(len(saltBytes))]
+	}
+
+	return salt
+}

+ 152 - 0
tools/provider_config_updater/lego/providers/dns/njalla/internal/client.go

@@ -0,0 +1,152 @@
+package internal
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
+)
+
+const apiEndpoint = "https://njal.la/api/1/"
+
+const authorizationHeader = "Authorization"
+
+// Client is a Njalla API client.
+type Client struct {
+	token string
+
+	apiEndpoint string
+	HTTPClient  *http.Client
+}
+
+// NewClient creates a new Client.
+func NewClient(token string) *Client {
+	return &Client{
+		token:       token,
+		apiEndpoint: apiEndpoint,
+		HTTPClient:  &http.Client{Timeout: 5 * time.Second},
+	}
+}
+
+// AddRecord adds a record.
+func (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) {
+	data := APIRequest{
+		Method: "add-record",
+		Params: record,
+	}
+
+	req, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)
+	if err != nil {
+		return nil, err
+	}
+
+	var result APIResponse[*Record]
+	err = c.do(req, &result)
+	if err != nil {
+		return nil, err
+	}
+
+	return result.Result, nil
+}
+
+// RemoveRecord removes a record.
+func (c *Client) RemoveRecord(ctx context.Context, id string, domain string) error {
+	data := APIRequest{
+		Method: "remove-record",
+		Params: Record{
+			ID:     id,
+			Domain: domain,
+		},
+	}
+
+	req, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)
+	if err != nil {
+		return err
+	}
+
+	err = c.do(req, &APIResponse[json.RawMessage]{})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ListRecords list the records for one domain.
+func (c *Client) ListRecords(ctx context.Context, domain string) ([]Record, error) {
+	data := APIRequest{
+		Method: "list-records",
+		Params: Record{
+			Domain: domain,
+		},
+	}
+
+	req, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)
+	if err != nil {
+		return nil, err
+	}
+
+	var result APIResponse[Records]
+	err = c.do(req, &result)
+	if err != nil {
+		return nil, err
+	}
+
+	return result.Result.Records, nil
+}
+
+func (c *Client) do(req *http.Request, result Response) error {
+	req.Header.Set(authorizationHeader, "Njalla "+c.token)
+
+	resp, err := c.HTTPClient.Do(req)
+	if err != nil {
+		return errutils.NewHTTPDoError(req, err)
+	}
+
+	defer func() { _ = resp.Body.Close() }()
+
+	if resp.StatusCode != http.StatusOK {
+		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
+	}
+
+	raw, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return errutils.NewReadResponseError(req, resp.StatusCode, err)
+	}
+
+	err = json.Unmarshal(raw, result)
+	if err != nil {
+		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
+	}
+
+	return result.GetError()
+}
+
+func newJSONRequest(ctx context.Context, method string, endpoint string, payload any) (*http.Request, error) {
+	buf := new(bytes.Buffer)
+
+	if payload != nil {
+		err := json.NewEncoder(buf).Encode(payload)
+		if err != nil {
+			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
+		}
+	}
+
+	req, err := http.NewRequestWithContext(ctx, method, endpoint, buf)
+	if err != nil {
+		return nil, fmt.Errorf("unable to create request: %w", err)
+	}
+
+	req.Header.Set("Accept", "application/json")
+
+	if payload != nil {
+		req.Header.Set("Content-Type", "application/json")
+	}
+
+	return req, nil
+}

+ 74 - 0
tools/provider_config_updater/lego/providers/dns/rackspace/internal/identity.go

@@ -0,0 +1,74 @@
+package internal
+
+import (
+	"context"
+	"encoding/json"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
+)
+
+// DefaultIdentityURL represents the Identity API endpoint to call.
+const DefaultIdentityURL = "https://identity.api.rackspacecloud.com/v2.0/tokens"
+
+type Identifier struct {
+	baseURL    string
+	httpClient *http.Client
+}
+
+// NewIdentifier creates a new Identifier.
+func NewIdentifier(httpClient *http.Client, baseURL string) *Identifier {
+	if httpClient == nil {
+		httpClient = &http.Client{Timeout: 5 * time.Second}
+	}
+
+	if baseURL == "" {
+		baseURL = DefaultIdentityURL
+	}
+
+	return &Identifier{baseURL: baseURL, httpClient: httpClient}
+}
+
+// Login sends an authentication request.
+// https://docs.rackspace.com/docs/cloud-dns/v1/getting-started/authenticate
+func (a *Identifier) Login(ctx context.Context, apiUser, apiKey string) (*Identity, error) {
+	authData := AuthData{
+		Auth: Auth{
+			APIKeyCredentials: APIKeyCredentials{
+				Username: apiUser,
+				APIKey:   apiKey,
+			},
+		},
+	}
+
+	req, err := newJSONRequest(ctx, http.MethodPost, a.baseURL, authData)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := a.httpClient.Do(req)
+	if err != nil {
+		return nil, errutils.NewHTTPDoError(req, err)
+	}
+
+	defer func() { _ = resp.Body.Close() }()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
+	}
+
+	raw, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
+	}
+
+	var identity Identity
+	err = json.Unmarshal(raw, &identity)
+	if err != nil {
+		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
+	}
+
+	return &identity, nil
+}

+ 129 - 0
tools/provider_config_updater/lego/providers/dns/selectel/selectel_test.go

@@ -0,0 +1,129 @@
+package selectel
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+var envTest = tester.NewEnvTest(EnvAPIToken, EnvTTL)
+
+func TestNewDNSProvider(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		envVars  map[string]string
+		expected string
+	}{
+		{
+			desc: "success",
+			envVars: map[string]string{
+				EnvAPIToken: "123",
+			},
+		},
+		{
+			desc: "missing api key",
+			envVars: map[string]string{
+				EnvAPIToken: "",
+			},
+			expected: fmt.Sprintf("selectel: some credentials information are missing: %s", EnvAPIToken),
+		},
+	}
+
+	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)
+				assert.NotNil(t, p.config)
+				assert.NotNil(t, p.client)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+}
+
+func TestNewDNSProviderConfig(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		token    string
+		ttl      int
+		expected string
+	}{
+		{
+			desc:  "success",
+			token: "123",
+			ttl:   60,
+		},
+		{
+			desc:     "missing api key",
+			token:    "",
+			ttl:      60,
+			expected: "selectel: credentials missing",
+		},
+		{
+			desc:     "bad TTL value",
+			token:    "123",
+			ttl:      59,
+			expected: fmt.Sprintf("selectel: invalid TTL, TTL (59) must be greater than %d", minTTL),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			config := NewDefaultConfig()
+			config.TTL = test.ttl
+			config.Token = test.token
+
+			p, err := NewDNSProviderConfig(config)
+
+			if test.expected == "" {
+				require.NoError(t, err)
+				require.NotNil(t, p)
+				assert.NotNil(t, p.config)
+				assert.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)
+
+	time.Sleep(2 * time.Second)
+
+	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
+	require.NoError(t, err)
+}

+ 285 - 0
tools/provider_config_updater/lego/providers/dns/transip/transip_test.go

@@ -0,0 +1,285 @@
+package transip
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/transip/gotransip/v6/domain"
+)
+
+const envDomain = envNamespace + "DOMAIN"
+
+var envTest = tester.NewEnvTest(
+	EnvAccountName,
+	EnvPrivateKeyPath).
+	WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		envVars  map[string]string
+		expected string
+	}{
+		{
+			desc: "success",
+			envVars: map[string]string{
+				EnvAccountName:    "johndoe",
+				EnvPrivateKeyPath: "./fixtures/private.key",
+			},
+		},
+		{
+			desc: "missing all credentials",
+			envVars: map[string]string{
+				EnvAccountName:    "",
+				EnvPrivateKeyPath: "",
+			},
+			expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME,TRANSIP_PRIVATE_KEY_PATH",
+		},
+		{
+			desc: "missing account name",
+			envVars: map[string]string{
+				EnvAccountName:    "",
+				EnvPrivateKeyPath: "./fixtures/private.key",
+			},
+			expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME",
+		},
+		{
+			desc: "missing private key path",
+			envVars: map[string]string{
+				EnvAccountName:    "johndoe",
+				EnvPrivateKeyPath: "",
+			},
+			expected: "transip: some credentials information are missing: TRANSIP_PRIVATE_KEY_PATH",
+		},
+	}
+
+	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.repository)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+
+	// The error message for a file not existing is different on Windows and Linux.
+	// Therefore, we test if the error type is the same.
+	t.Run("could not open private key path", func(t *testing.T) {
+		defer envTest.RestoreEnv()
+		envTest.ClearEnv()
+
+		envTest.Apply(map[string]string{
+			EnvAccountName:    "johndoe",
+			EnvPrivateKeyPath: "./fixtures/non/existent/private.key",
+		})
+
+		_, err := NewDNSProvider()
+		require.ErrorIs(t, err, os.ErrNotExist)
+	})
+}
+
+func TestNewDNSProviderConfig(t *testing.T) {
+	testCases := []struct {
+		desc           string
+		accountName    string
+		privateKeyPath string
+		expected       string
+	}{
+		{
+			desc:           "success",
+			accountName:    "johndoe",
+			privateKeyPath: "./fixtures/private.key",
+		},
+		{
+			desc:     "missing all credentials",
+			expected: "transip: AccountName is required",
+		},
+		{
+			desc:           "missing account name",
+			privateKeyPath: "./fixtures/private.key",
+			expected:       "transip: AccountName is required",
+		},
+		{
+			desc:        "missing private key path",
+			accountName: "johndoe",
+			expected:    "transip: PrivateKeyReader, token or PrivateKeyReader is required",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			config := NewDefaultConfig()
+			config.AccountName = test.accountName
+			config.PrivateKeyPath = test.privateKeyPath
+
+			p, err := NewDNSProviderConfig(config)
+
+			if test.expected == "" {
+				require.NoError(t, err)
+				require.NotNil(t, p)
+				require.NotNil(t, p.config)
+				require.NotNil(t, p.repository)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+
+	// The error message for a file not existing is different on Windows and Linux.
+	// Therefore, we test if the error type is the same.
+	t.Run("could not open private key path", func(t *testing.T) {
+		config := NewDefaultConfig()
+		config.AccountName = "johndoe"
+		config.PrivateKeyPath = "./fixtures/non/existent/private.key"
+
+		_, err := NewDNSProviderConfig(config)
+		require.ErrorIs(t, err, os.ErrNotExist)
+	})
+}
+
+func TestDNSProvider_concurrentGetDNSEntries(t *testing.T) {
+	client := &fakeClient{
+		getInfoLatency:       50 * time.Millisecond,
+		setDNSEntriesLatency: 500 * time.Millisecond,
+		domainName:           "lego.wtf",
+	}
+
+	repo := domain.Repository{Client: client}
+
+	p := &DNSProvider{
+		config:     NewDefaultConfig(),
+		repository: repo,
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	solve := func(domain1, suffix string, timeoutPresent, timeoutSolve, timeoutCleanup time.Duration) error {
+		time.Sleep(timeoutPresent)
+
+		err := p.Present(domain1, "", "")
+		if err != nil {
+			return err
+		}
+
+		time.Sleep(timeoutSolve)
+
+		var found bool
+		for _, entry := range client.dnsEntries {
+			if strings.HasSuffix(entry.Name, suffix) {
+				found = true
+			}
+		}
+		if !found {
+			return fmt.Errorf("record %s not found: %v", suffix, client.dnsEntries)
+		}
+
+		time.Sleep(timeoutCleanup)
+
+		return p.CleanUp(domain1, "", "")
+	}
+
+	go func() {
+		defer wg.Done()
+		err := solve("bar.lego.wtf", ".bar", 500*time.Millisecond, 100*time.Millisecond, 100*time.Millisecond)
+		require.NoError(t, err)
+	}()
+
+	go func() {
+		defer wg.Done()
+		err := solve("foo.lego.wtf", ".foo", 500*time.Millisecond, 200*time.Millisecond, 100*time.Millisecond)
+		require.NoError(t, err)
+	}()
+
+	wg.Wait()
+
+	assert.Empty(t, client.dnsEntries)
+}
+
+func TestDNSProvider_concurrentAddDNSEntry(t *testing.T) {
+	client := &fakeClient{
+		domainName: "lego.wtf",
+	}
+	repo := domain.Repository{Client: client}
+
+	p := &DNSProvider{
+		config:     NewDefaultConfig(),
+		repository: repo,
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	solve := func(domain1 string, timeoutPresent, timeoutCleanup time.Duration) error {
+		time.Sleep(timeoutPresent)
+		err := p.Present(domain1, "", "")
+		if err != nil {
+			return err
+		}
+
+		time.Sleep(timeoutCleanup)
+		return p.CleanUp(domain1, "", "")
+	}
+
+	go func() {
+		defer wg.Done()
+		err := solve("bar.lego.wtf", 550*time.Millisecond, 500*time.Millisecond)
+		require.NoError(t, err)
+	}()
+
+	go func() {
+		defer wg.Done()
+		err := solve("foo.lego.wtf", 500*time.Millisecond, 100*time.Millisecond)
+		require.NoError(t, err)
+	}()
+
+	wg.Wait()
+
+	assert.Empty(t, client.dnsEntries)
+}
+
+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)
+
+	time.Sleep(1 * time.Second)
+
+	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
+	require.NoError(t, err)
+}

+ 114 - 0
tools/provider_config_updater/lego/providers/dns/vinyldns/mock_test.go

@@ -0,0 +1,114 @@
+package vinyldns
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"sync"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func setupTest(t *testing.T) (*http.ServeMux, *DNSProvider) {
+	t.Helper()
+
+	mux := http.NewServeMux()
+	server := httptest.NewServer(mux)
+	t.Cleanup(server.Close)
+
+	config := NewDefaultConfig()
+	config.AccessKey = "foo"
+	config.SecretKey = "bar"
+	config.Host = server.URL
+
+	p, err := NewDNSProviderConfig(config)
+	require.NoError(t, err)
+
+	return mux, p
+}
+
+type mockRouter struct {
+	debug bool
+
+	mu     sync.Mutex
+	routes map[string]map[string]http.HandlerFunc
+}
+
+func newMockRouter() *mockRouter {
+	routes := map[string]map[string]http.HandlerFunc{
+		http.MethodGet:    {},
+		http.MethodPost:   {},
+		http.MethodPut:    {},
+		http.MethodDelete: {},
+	}
+
+	return &mockRouter{
+		routes: routes,
+	}
+}
+
+func (h *mockRouter) Debug() *mockRouter {
+	h.debug = true
+
+	return h
+}
+
+func (h *mockRouter) Get(path string, statusCode int, filename string) *mockRouter {
+	h.add(http.MethodGet, path, statusCode, filename)
+	return h
+}
+
+func (h *mockRouter) Post(path string, statusCode int, filename string) *mockRouter {
+	h.add(http.MethodPost, path, statusCode, filename)
+	return h
+}
+
+func (h *mockRouter) Put(path string, statusCode int, filename string) *mockRouter {
+	h.add(http.MethodPut, path, statusCode, filename)
+	return h
+}
+
+func (h *mockRouter) Delete(path string, statusCode int, filename string) *mockRouter {
+	h.add(http.MethodDelete, path, statusCode, filename)
+	return h
+}
+
+func (h *mockRouter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	if h.debug {
+		fmt.Println(req)
+	}
+
+	rt := h.routes[req.Method]
+	if rt == nil {
+		http.NotFound(rw, req)
+		return
+	}
+
+	hdl := rt[req.URL.Path]
+	if hdl == nil {
+		http.NotFound(rw, req)
+		return
+	}
+
+	hdl(rw, req)
+}
+
+func (h *mockRouter) add(method, path string, statusCode int, filename string) {
+	h.routes[method][path] = func(rw http.ResponseWriter, req *http.Request) {
+		rw.WriteHeader(statusCode)
+
+		data, err := os.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)
+	}
+}

+ 121 - 0
tools/provider_config_updater/lego/providers/dns/websupport/internal/types.go

@@ -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 {
+	Item   *Record         `json:"item"`
+	Status string          `json:"status"`
+	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 errAPI Errors
+	err := json.Unmarshal(resp.Errors, &errAPI)
+	if err != nil {
+		return err
+	}
+
+	return &errAPI
+}
+
+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"`
+}

+ 16 - 0
tools/provider_config_updater/lego/providers/dns/zoneee/internal/types.go

@@ -0,0 +1,16 @@
+package internal
+
+type TXTRecord struct {
+	// Identifier (identificator)
+	ID string `json:"id,omitempty"`
+	// Hostname
+	Name string `json:"name"`
+	// TXT content value
+	Destination string `json:"destination"`
+	// Can this record be deleted
+	Delete bool `json:"delete,omitempty"`
+	// Can this record be modified
+	Modify bool `json:"modify,omitempty"`
+	// API url to get this entity
+	ResourceURL string `json:"resource_url,omitempty"`
+}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä