Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions providers/dns/allinkl/allinkl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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
Expand Down
114 changes: 114 additions & 0 deletions providers/dns/allinkl/allinkl_test.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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)
}
15 changes: 11 additions & 4 deletions providers/dns/allinkl/internal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
Expand All @@ -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)
Expand All @@ -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},
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 5 additions & 4 deletions providers/dns/allinkl/internal/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internal

import (
"net/http/httptest"
"net/url"
"testing"

"github.com/go-acme/lego/v4/platform/tester/servermock"
Expand All @@ -11,15 +12,15 @@ 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
}

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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions providers/dns/allinkl/internal/fixtures/auth-request.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
<Body>
<KasAuth xmlns="https://kasserver.com/">
<Params>{"kas_login":"user","kas_auth_data":"secret","kas_auth_type":"plain","session_lifetime":60,"session_update_lifetime":"Y"}</Params>
</KasAuth>
</Body>
</Envelope>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>zone_not_found</faultstring>
<faultactor>KasApi</faultactor>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
22 changes: 13 additions & 9 deletions providers/dns/allinkl/internal/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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},
}
}

Expand Down Expand Up @@ -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)
}
Expand Down
Loading