diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go
index a5b27ff59b..3fa69cd37f 100644
--- a/providers/dns/allinkl/allinkl.go
+++ b/providers/dns/allinkl/allinkl.go
@@ -11,6 +11,7 @@ import (
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/providers/dns/allinkl/internal"
"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
@@ -121,20 +122,33 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
- authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
- if err != nil {
- return fmt.Errorf("allinkl: could not find zone for domain %q: %w", domain, err)
- }
-
ctx := context.Background()
credential, err := d.identifier.Authentication(ctx, 60, true)
if err != nil {
- return fmt.Errorf("allinkl: %w", err)
+ return fmt.Errorf("allinkl: authentication: %w", err)
}
ctx = internal.WithContext(ctx, credential)
+ var authZone string
+
+ for z := range dns01.UnFqdnDomainsSeq(info.EffectiveFQDN) {
+ _, errG := d.client.GetDNSSettings(ctx, z, "")
+ if errG != nil {
+ log.Infof("allinkl: get DNS settings zone[%s] %v", z, errG)
+ continue
+ }
+
+ authZone = z
+
+ break
+ }
+
+ if authZone == "" {
+ return fmt.Errorf("allinkl: unable to find auth zone for '%s'", info.EffectiveFQDN)
+ }
+
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("allinkl: %w", err)
@@ -149,7 +163,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
recordID, err := d.client.AddDNSSettings(ctx, record)
if err != nil {
- return fmt.Errorf("allinkl: %w", err)
+ return fmt.Errorf("allinkl: add DNS settings: %w", err)
}
d.recordIDsMu.Lock()
@@ -167,7 +181,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
credential, err := d.identifier.Authentication(ctx, 60, true)
if err != nil {
- return fmt.Errorf("allinkl: %w", err)
+ return fmt.Errorf("allinkl: authentication: %w", err)
}
ctx = internal.WithContext(ctx, credential)
@@ -183,7 +197,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
_, err = d.client.DeleteDNSSettings(ctx, recordID)
if err != nil {
- return fmt.Errorf("allinkl: %w", err)
+ return fmt.Errorf("allinkl: delete DNS settings: %w", err)
}
return nil
diff --git a/providers/dns/allinkl/allinkl_test.go b/providers/dns/allinkl/allinkl_test.go
index b42adce5d3..beb9ff0d2e 100644
--- a/providers/dns/allinkl/allinkl_test.go
+++ b/providers/dns/allinkl/allinkl_test.go
@@ -1,9 +1,18 @@
package allinkl
import (
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
+ "github.com/go-acme/lego/v4/providers/dns/allinkl/internal"
"github.com/stretchr/testify/require"
)
@@ -143,3 +152,108 @@ func TestLiveCleanUp(t *testing.T) {
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
+
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.Login = "user"
+ config.Password = "secret"
+ config.HTTPClient = server.Client()
+
+ p, err := NewDNSProviderConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ p.client.BaseURL, _ = url.Parse(server.URL)
+ p.identifier.BaseURL, _ = url.Parse(server.URL)
+
+ return p, err
+ },
+ ).Route("POST /KasAuth.php",
+ servermock.ResponseFromInternal("auth.xml"),
+ servermock.CheckRequestBodyFromInternal("auth-request.xml").
+ IgnoreWhitespace(),
+ )
+}
+
+func extractKasRequest(reader io.Reader) (*internal.KasRequest, error) {
+ type ReqEnvelope struct {
+ XMLName xml.Name `xml:"Envelope"`
+ Body struct {
+ KasAPI struct {
+ Params string `xml:"Params"`
+ } `xml:"KasApi"`
+ } `xml:"Body"`
+ }
+
+ raw, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, err
+ }
+
+ reqEnvelope := ReqEnvelope{}
+
+ err = xml.Unmarshal(raw, &reqEnvelope)
+ if err != nil {
+ return nil, err
+ }
+
+ var kReq internal.KasRequest
+
+ err = json.Unmarshal([]byte(reqEnvelope.Body.KasAPI.Params), &kReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return &kReq, nil
+}
+
+func TestDNSProvider_Present(t *testing.T) {
+ provider := mockBuilder().
+ Route("POST /KasApi.php",
+ http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ kReq, err := extractKasRequest(req.Body)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ switch kReq.Action {
+ case "get_dns_settings":
+ params := kReq.RequestParams.(map[string]any)
+
+ if params["zone_host"] == "_acme-challenge.example.com" {
+ servermock.ResponseFromInternal("get_dns_settings_not_found.xml").ServeHTTP(rw, req)
+ } else {
+ servermock.ResponseFromInternal("get_dns_settings.xml").ServeHTTP(rw, req)
+ }
+
+ case "add_dns_settings":
+ servermock.ResponseFromInternal("add_dns_settings.xml").ServeHTTP(rw, req)
+
+ default:
+ http.Error(rw, fmt.Sprintf("unknown action: %v", kReq.Action), http.StatusBadRequest)
+ }
+ }),
+ ).
+ Build(t)
+
+ err := provider.Present("example.com", "abc", "123d==")
+ require.NoError(t, err)
+}
+
+func TestDNSProvider_CleanUp(t *testing.T) {
+ provider := mockBuilder().
+ Route("POST /KasApi.php",
+ servermock.ResponseFromInternal("delete_dns_settings.xml"),
+ servermock.CheckRequestBodyFromInternal("delete_dns_settings-request.xml").
+ IgnoreWhitespace()).
+ Build(t)
+
+ provider.recordIDs["abc"] = "57347450"
+
+ err := provider.CleanUp("example.com", "abc", "123d==")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/allinkl/internal/client.go b/providers/dns/allinkl/internal/client.go
index d747e9b366..69a7b6f197 100644
--- a/providers/dns/allinkl/internal/client.go
+++ b/providers/dns/allinkl/internal/client.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "net/url"
"strconv"
"strings"
"sync"
@@ -15,7 +16,9 @@ import (
"github.com/go-viper/mapstructure/v2"
)
-const apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php"
+const defaultBaseURL = "https://kasapi.kasserver.com/soap/"
+
+const apiPath = "KasApi.php"
type Authentication interface {
Authentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error)
@@ -28,15 +31,17 @@ type Client struct {
floodTime time.Time
muFloodTime sync.Mutex
- baseURL string
+ BaseURL *url.URL
HTTPClient *http.Client
}
// NewClient creates a new Client.
func NewClient(login string) *Client {
+ baseURL, _ := url.Parse(defaultBaseURL)
+
return &Client{
login: login,
- baseURL: apiEndpoint,
+ BaseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}
}
@@ -124,7 +129,9 @@ func (c *Client) newRequest(ctx context.Context, action string, requestParams an
payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(payload))
+ endpoint := c.BaseURL.JoinPath(apiPath)
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}
diff --git a/providers/dns/allinkl/internal/client_test.go b/providers/dns/allinkl/internal/client_test.go
index 4b111e31c2..2d2776153b 100644
--- a/providers/dns/allinkl/internal/client_test.go
+++ b/providers/dns/allinkl/internal/client_test.go
@@ -2,6 +2,7 @@ package internal
import (
"net/http/httptest"
+ "net/url"
"testing"
"github.com/go-acme/lego/v4/platform/tester/servermock"
@@ -11,7 +12,7 @@ import (
func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("user")
- client.baseURL = server.URL
+ client.BaseURL, _ = url.Parse(server.URL)
client.HTTPClient = server.Client()
return client, nil
@@ -19,7 +20,7 @@ func setupClient(server *httptest.Server) (*Client, error) {
func TestClient_GetDNSSettings(t *testing.T) {
client := servermock.NewBuilder[*Client](setupClient).
- Route("POST /", servermock.ResponseFromFixture("get_dns_settings.xml"),
+ Route("POST /KasApi.php", servermock.ResponseFromFixture("get_dns_settings.xml"),
servermock.CheckRequestBodyFromFixture("get_dns_settings-request.xml").
IgnoreWhitespace()).
Build(t)
@@ -98,7 +99,7 @@ func TestClient_GetDNSSettings(t *testing.T) {
func TestClient_AddDNSSettings(t *testing.T) {
client := servermock.NewBuilder[*Client](setupClient).
- Route("POST /", servermock.ResponseFromFixture("add_dns_settings.xml"),
+ Route("POST /KasApi.php", servermock.ResponseFromFixture("add_dns_settings.xml"),
servermock.CheckRequestBodyFromFixture("add_dns_settings-request.xml").
IgnoreWhitespace()).
Build(t)
@@ -118,7 +119,7 @@ func TestClient_AddDNSSettings(t *testing.T) {
func TestClient_DeleteDNSSettings(t *testing.T) {
client := servermock.NewBuilder[*Client](setupClient).
- Route("POST /", servermock.ResponseFromFixture("delete_dns_settings.xml"),
+ Route("POST /KasApi.php", servermock.ResponseFromFixture("delete_dns_settings.xml"),
servermock.CheckRequestBodyFromFixture("delete_dns_settings-request.xml").
IgnoreWhitespace()).
Build(t)
diff --git a/providers/dns/allinkl/internal/fixtures/auth-request.xml b/providers/dns/allinkl/internal/fixtures/auth-request.xml
new file mode 100644
index 0000000000..1cba86f100
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/auth-request.xml
@@ -0,0 +1,7 @@
+
+
+
+ {"kas_login":"user","kas_auth_data":"secret","kas_auth_type":"plain","session_lifetime":60,"session_update_lifetime":"Y"}
+
+
+
diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings_not_found.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings_not_found.xml
new file mode 100644
index 0000000000..ba3f06f876
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings_not_found.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ SOAP-ENV:Server
+ zone_not_found
+ KasApi
+
+
+
diff --git a/providers/dns/allinkl/internal/identity.go b/providers/dns/allinkl/internal/identity.go
index ba8d4d90e4..e95e78899d 100644
--- a/providers/dns/allinkl/internal/identity.go
+++ b/providers/dns/allinkl/internal/identity.go
@@ -6,14 +6,14 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "net/url"
"strings"
"time"
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)
-// authEndpoint represents the Identity API endpoint to call.
-const authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php"
+const authPath = "KasAuth.php"
type token string
@@ -24,17 +24,19 @@ type Identifier struct {
login string
password string
- authEndpoint string
- HTTPClient *http.Client
+ BaseURL *url.URL
+ HTTPClient *http.Client
}
// NewIdentifier creates a new Identifier.
func NewIdentifier(login, password string) *Identifier {
+ baseURL, _ := url.Parse(defaultBaseURL)
+
return &Identifier{
- login: login,
- password: password,
- authEndpoint: authEndpoint,
- HTTPClient: &http.Client{Timeout: 10 * time.Second},
+ login: login,
+ password: password,
+ BaseURL: baseURL,
+ HTTPClient: &http.Client{Timeout: 10 * time.Second},
}
}
@@ -62,7 +64,9 @@ func (c *Identifier) Authentication(ctx context.Context, sessionLifetime int, se
payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body)))
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.authEndpoint, bytes.NewReader(payload))
+ endpoint := c.BaseURL.JoinPath(authPath)
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))
if err != nil {
return "", fmt.Errorf("unable to create request: %w", err)
}
diff --git a/providers/dns/allinkl/internal/identity_test.go b/providers/dns/allinkl/internal/identity_test.go
index 7b93b76889..41d092b139 100644
--- a/providers/dns/allinkl/internal/identity_test.go
+++ b/providers/dns/allinkl/internal/identity_test.go
@@ -3,6 +3,7 @@ package internal
import (
"context"
"net/http/httptest"
+ "net/url"
"testing"
"github.com/go-acme/lego/v4/platform/tester/servermock"
@@ -12,7 +13,7 @@ import (
func setupIdentifierClient(server *httptest.Server) (*Identifier, error) {
client := NewIdentifier("user", "secret")
- client.authEndpoint = server.URL
+ client.BaseURL, _ = url.Parse(server.URL)
client.HTTPClient = server.Client()
return client, nil
@@ -26,10 +27,13 @@ func mockContext(t *testing.T) context.Context {
func TestIdentifier_Authentication(t *testing.T) {
client := servermock.NewBuilder[*Identifier](setupIdentifierClient).
- Route("POST /", servermock.ResponseFromFixture("auth.xml")).
+ Route("POST /KasAuth.php",
+ servermock.ResponseFromFixture("auth.xml"),
+ servermock.CheckRequestBodyFromFixture("auth-request.xml").
+ IgnoreWhitespace()).
Build(t)
- credentialToken, err := client.Authentication(t.Context(), 60, false)
+ credentialToken, err := client.Authentication(t.Context(), 60, true)
require.NoError(t, err)
assert.Equal(t, "593959ca04f0de9689b586c6a647d15d", credentialToken)
@@ -37,7 +41,7 @@ func TestIdentifier_Authentication(t *testing.T) {
func TestIdentifier_Authentication_error(t *testing.T) {
client := servermock.NewBuilder[*Identifier](setupIdentifierClient).
- Route("POST /", servermock.ResponseFromFixture("auth_fault.xml")).
+ Route("POST /KasAuth.php", servermock.ResponseFromFixture("auth_fault.xml")).
Build(t)
_, err := client.Authentication(t.Context(), 60, false)