diff --git a/README.md b/README.md
index c1c25eac..6f287400 100644
--- a/README.md
+++ b/README.md
@@ -46,29 +46,29 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
| | | | |
|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
-| [Akamai EdgeDNS](https://go-acme.github.io/lego/dns/edgedns/) | [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) | [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) |
-| [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) | [Azure](https://go-acme.github.io/lego/dns/azure/) |
-| [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) | [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) |
-| [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) | [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) |
-| [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [deSEC.io](https://go-acme.github.io/lego/dns/desec/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) |
-| [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod](https://go-acme.github.io/lego/dns/dnspod/) | [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) |
-| [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) | [Dyn](https://go-acme.github.io/lego/dns/dyn/) |
-| [Dynu](https://go-acme.github.io/lego/dns/dynu/) | [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) |
-| [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) |
-| [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) |
-| [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) |
-| [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Ionos](https://go-acme.github.io/lego/dns/ionos/) |
-| [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) |
-| [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) |
-| [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) |
-| [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [Njalla](https://go-acme.github.io/lego/dns/njalla/) |
-| [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) |
-| [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [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/) | [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) |
-| [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/) | [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/) | |
+| [Akamai EdgeDNS](https://go-acme.github.io/lego/dns/edgedns/) | [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [all-inkl](https://go-acme.github.io/lego/dns/allinkl/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) |
+| [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) |
+| [Azure](https://go-acme.github.io/lego/dns/azure/) | [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) |
+| [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) | [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) |
+| [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [deSEC.io](https://go-acme.github.io/lego/dns/desec/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) |
+| [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) | [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod](https://go-acme.github.io/lego/dns/dnspod/) |
+| [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) | [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) |
+| [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Dynu](https://go-acme.github.io/lego/dns/dynu/) | [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) |
+| [External program](https://go-acme.github.io/lego/dns/exec/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) |
+| [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) |
+| [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) |
+| [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) |
+| [Ionos](https://go-acme.github.io/lego/dns/ionos/) | [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) |
+| [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) |
+| [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) |
+| [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) |
+| [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) |
+| [OVH](https://go-acme.github.io/lego/dns/ovh/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) |
+| [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/) | [Simply.com](https://go-acme.github.io/lego/dns/simply/) |
+| [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [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/) |
+| [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/) |
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index 581f0280..d3fceabe 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -16,6 +16,7 @@ func allDNSCodes() string {
"manual",
"acme-dns",
"alidns",
+ "allinkl",
"arvancloud",
"auroradns",
"autodns",
@@ -150,6 +151,26 @@ func displayDNSHelp(name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/alidns`)
+ case "allinkl":
+ // generated from: providers/dns/allinkl/allinkl.toml
+ ew.writeln(`Configuration for all-inkl.`)
+ ew.writeln(`Code: 'allinkl'`)
+ ew.writeln(`Since: 'v4.5.0'`)
+ ew.writeln()
+
+ ew.writeln(`Credentials:`)
+ ew.writeln(` - "ALL_INKL_API_KEY": API login`)
+ ew.writeln(` - "ALL_INKL_PASSWORD": API password`)
+ ew.writeln()
+
+ ew.writeln(`Additional Configuration:`)
+ ew.writeln(` - "ALL_INKL_HTTP_TIMEOUT": API request timeout`)
+ ew.writeln(` - "ALL_INKL_POLLING_INTERVAL": Time between DNS propagation check`)
+ ew.writeln(` - "ALL_INKL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
+
+ ew.writeln()
+ ew.writeln(`More information: https://go-acme.github.io/lego/dns/allinkl`)
+
case "arvancloud":
// generated from: providers/dns/arvancloud/arvancloud.toml
ew.writeln(`Configuration for ArvanCloud.`)
diff --git a/docs/content/dns/zz_gen_allinkl.md b/docs/content/dns/zz_gen_allinkl.md
new file mode 100644
index 00000000..443bcb41
--- /dev/null
+++ b/docs/content/dns/zz_gen_allinkl.md
@@ -0,0 +1,63 @@
+---
+title: "all-inkl"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: allinkl
+---
+
+
+
+
+
+Since: v4.5.0
+
+Configuration for [all-inkl](https://all-inkl.com).
+
+
+
+
+- Code: `allinkl`
+
+Here is an example bash command using the all-inkl provider:
+
+```bash
+ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \
+ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \
+lego --email myemail@example.com --dns allinkl --domains my.example.org run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `ALL_INKL_API_KEY` | API login |
+| `ALL_INKL_PASSWORD` | API password |
+
+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 |
+|--------------------------------|-------------|
+| `ALL_INKL_HTTP_TIMEOUT` | API request timeout |
+| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check |
+| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
+
+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://kasapi.kasserver.com/dokumentation/phpdoc/index.html)
+
+
+
+
diff --git a/go.mod b/go.mod
index f5ee7668..ccc94770 100644
--- a/go.mod
+++ b/go.mod
@@ -31,6 +31,7 @@ require (
github.com/linode/linodego v0.25.3
github.com/liquidweb/liquidweb-go v1.6.3
github.com/miekg/dns v1.1.40
+ github.com/mitchellh/mapstructure v1.4.1
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04
github.com/nrdcg/auroradns v1.0.1
github.com/nrdcg/desec v0.5.0
diff --git a/go.sum b/go.sum
index c442fa2c..97a68ff7 100644
--- a/go.sum
+++ b/go.sum
@@ -324,8 +324,9 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
diff --git a/providers/dns/allinkl/allinkl.go b/providers/dns/allinkl/allinkl.go
new file mode 100644
index 00000000..a8807b32
--- /dev/null
+++ b/providers/dns/allinkl/allinkl.go
@@ -0,0 +1,161 @@
+// Package allinkl implements a DNS provider for solving the DNS-01 challenge using all-inkl.
+package allinkl
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "sync"
+ "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/allinkl/internal"
+)
+
+// Environment variables names.
+const (
+ envNamespace = "ALL_INKL_"
+
+ EnvLogin = envNamespace + "LOGIN"
+ EnvPassword = envNamespace + "PASSWORD"
+
+ EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+ EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
+ EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
+)
+
+// Config is used to configure the creation of the DNSProvider.
+type Config struct {
+ Login 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, dns01.DefaultPropagationTimeout),
+ PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
+ HTTPClient: &http.Client{
+ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
+ },
+ }
+}
+
+// DNSProvider implements the challenge.Provider interface.
+type DNSProvider struct {
+ config *Config
+ client *internal.Client
+
+ recordIDs map[string]string
+ recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for all-inkl.
+// Credentials must be passed in the environment variable: ALL_INKL_API_KEY, ALL_INKL_PASSWORD.
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvLogin, EnvPassword)
+ if err != nil {
+ return nil, fmt.Errorf("allinkl: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.Login = values[EnvLogin]
+ config.Password = values[EnvPassword]
+
+ return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for all-inkl.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("allinkl: the configuration of the DNS provider is nil")
+ }
+
+ if config.Login == "" || config.Password == "" {
+ return nil, errors.New("allinkl: missing credentials")
+ }
+
+ client := internal.NewClient(config.Login, config.Password)
+
+ if config.HTTPClient != nil {
+ client.HTTPClient = config.HTTPClient
+ }
+
+ return &DNSProvider{
+ config: config,
+ client: client,
+ recordIDs: make(map[string]string),
+ }, 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 using the specified parameters.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value := dns01.GetRecord(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(fqdn)
+ if err != nil {
+ return fmt.Errorf("allinkl: could not determine zone for domain %q: %w", domain, err)
+ }
+
+ credential, err := d.client.Authentication(60, true)
+ if err != nil {
+ return fmt.Errorf("allinkl: %w", err)
+ }
+
+ subDomain := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone))
+
+ record := internal.DNSRequest{
+ ZoneHost: authZone,
+ RecordType: "TXT",
+ RecordName: subDomain,
+ RecordData: value,
+ }
+
+ recordID, err := d.client.AddDNSSettings(credential, record)
+ if err != nil {
+ return fmt.Errorf("allinkl: %w", err)
+ }
+
+ d.recordIDsMu.Lock()
+ d.recordIDs[token] = recordID
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _ := dns01.GetRecord(domain, keyAuth)
+
+ credential, err := d.client.Authentication(60, true)
+ if err != nil {
+ return fmt.Errorf("allinkl: %w", err)
+ }
+
+ // gets the record's unique ID from when we created it
+ d.recordIDsMu.Lock()
+ recordID, ok := d.recordIDs[token]
+ d.recordIDsMu.Unlock()
+ if !ok {
+ return fmt.Errorf("allinkl: unknown record ID for '%s' '%s'", fqdn, token)
+ }
+
+ _, err = d.client.DeleteDNSSettings(credential, recordID)
+ if err != nil {
+ return fmt.Errorf("allinkl: %w", err)
+ }
+
+ return nil
+}
diff --git a/providers/dns/allinkl/allinkl.toml b/providers/dns/allinkl/allinkl.toml
new file mode 100644
index 00000000..ea1aa636
--- /dev/null
+++ b/providers/dns/allinkl/allinkl.toml
@@ -0,0 +1,24 @@
+Name = "all-inkl"
+Description = ''''''
+URL = "https://all-inkl.com"
+Code = "allinkl"
+Since = "v4.5.0"
+
+Example = '''
+ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \
+ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \
+lego --email myemail@example.com --dns allinkl --domains my.example.org run
+'''
+
+[Configuration]
+ [Configuration.Credentials]
+ ALL_INKL_API_KEY = "API login"
+ ALL_INKL_PASSWORD = "API password"
+ [Configuration.Additional]
+ ALL_INKL_POLLING_INTERVAL = "Time between DNS propagation check"
+ ALL_INKL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+ ALL_INKL_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+ API = "https://kasapi.kasserver.com/dokumentation/phpdoc/index.html"
+ Guide = "https://kasapi.kasserver.com/dokumentation/"
diff --git a/providers/dns/allinkl/allinkl_test.go b/providers/dns/allinkl/allinkl_test.go
new file mode 100644
index 00000000..af85f8c5
--- /dev/null
+++ b/providers/dns/allinkl/allinkl_test.go
@@ -0,0 +1,142 @@
+package allinkl
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester"
+ "github.com/stretchr/testify/require"
+)
+
+const envDomain = envNamespace + "DOMAIN"
+
+var envTest = tester.NewEnvTest(EnvLogin, EnvPassword).WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+ testCases := []struct {
+ desc string
+ envVars map[string]string
+ expected string
+ }{
+ {
+ desc: "success",
+ envVars: map[string]string{
+ EnvLogin: "user",
+ EnvPassword: "secret",
+ },
+ },
+ {
+ desc: "missing credentials: account name",
+ envVars: map[string]string{
+ EnvLogin: "",
+ EnvPassword: "secret",
+ },
+ expected: "allinkl: some credentials information are missing: ALL_INKL_LOGIN",
+ },
+ {
+ desc: "missing credentials: api key",
+ envVars: map[string]string{
+ EnvLogin: "user",
+ EnvPassword: "",
+ },
+ expected: "allinkl: some credentials information are missing: ALL_INKL_PASSWORD",
+ },
+ {
+ desc: "missing credentials: all",
+ envVars: map[string]string{
+ EnvLogin: "",
+ EnvPassword: "",
+ },
+ expected: "allinkl: some credentials information are missing: ALL_INKL_LOGIN,ALL_INKL_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)
+ require.NotNil(t, p.client)
+ } else {
+ require.EqualError(t, err, test.expected)
+ }
+ })
+ }
+}
+
+func TestNewDNSProviderConfig(t *testing.T) {
+ testCases := []struct {
+ desc string
+ login string
+ password string
+ expected string
+ }{
+ {
+ desc: "success",
+ login: "user",
+ password: "secret",
+ },
+ {
+ desc: "missing account name",
+ password: "secret",
+ expected: "allinkl: missing credentials",
+ },
+ {
+ desc: "missing api key",
+ login: "user",
+ expected: "allinkl: missing credentials",
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ config := NewDefaultConfig()
+ config.Login = test.login
+ config.Password = test.password
+
+ p, err := NewDNSProviderConfig(config)
+
+ if test.expected == "" {
+ require.NoError(t, err)
+ require.NotNil(t, p)
+ require.NotNil(t, p.config)
+ require.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)
+
+ err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/allinkl/internal/client.go b/providers/dns/allinkl/internal/client.go
new file mode 100644
index 00000000..37d9eaef
--- /dev/null
+++ b/providers/dns/allinkl/internal/client.go
@@ -0,0 +1,286 @@
+package internal
+
+import (
+ "bytes"
+ "crypto/sha1"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+const (
+ authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php"
+ apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php"
+)
+
+// Client a KAS server client.
+type Client struct {
+ login string
+ password string
+
+ authEndpoint string
+ apiEndpoint string
+ HTTPClient *http.Client
+ floodTime time.Time
+}
+
+// NewClient creates a new Client.
+func NewClient(login string, password string) *Client {
+ return &Client{
+ login: login,
+ password: password,
+ authEndpoint: authEndpoint,
+ apiEndpoint: apiEndpoint,
+ HTTPClient: &http.Client{Timeout: 10 * time.Second},
+ }
+}
+
+// Authentication Creates a credential token.
+// - sessionLifetime: Validity of the token in seconds.
+// - sessionUpdateLifetime: with `true` the session is extended with every request.
+func (c Client) Authentication(sessionLifetime int, sessionUpdateLifetime bool) (string, error) {
+ hash := sha1.New()
+ hash.Write([]byte(c.password))
+
+ sul := "N"
+ if sessionUpdateLifetime {
+ sul = "Y"
+ }
+
+ ar := AuthRequest{
+ Login: c.login,
+ AuthData: fmt.Sprintf("%x", hash.Sum(nil)),
+ AuthType: "sha1",
+ SessionLifetime: sessionLifetime,
+ SessionUpdateLifetime: sul,
+ }
+
+ body, err := json.Marshal(ar)
+ if err != nil {
+ return "", fmt.Errorf("request marshal: %w", err)
+ }
+
+ payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body)))
+
+ req, err := http.NewRequest(http.MethodPost, c.authEndpoint, bytes.NewReader(payload))
+ if err != nil {
+ return "", fmt.Errorf("request creation: %w", err)
+ }
+
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("request execution: %w", err)
+ }
+
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ data, _ := ioutil.ReadAll(resp.Body)
+ return "", fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data))
+ }
+
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("response read: %w", err)
+ }
+
+ var e KasAuthEnvelope
+ decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))})
+ err = decoder.Decode(&e)
+ if err != nil {
+ return "", fmt.Errorf("response xml decode: %w", err)
+ }
+
+ if e.Body.Fault != nil {
+ return "", e.Body.Fault
+ }
+
+ return e.Body.KasAuthResponse.Return.Text, nil
+}
+
+// GetDNSSettings Reading out the DNS settings of a zone.
+// - zone: host zone.
+// - recordID: the ID of the resource record (optional).
+func (c *Client) GetDNSSettings(credentialToken, zone, recordID string) ([]ReturnInfo, error) {
+ requestParams := map[string]string{"zone_host": zone}
+
+ if recordID != "" {
+ requestParams["record_id"] = recordID
+ }
+
+ item, err := c.do(credentialToken, "get_dns_settings", requestParams)
+ if err != nil {
+ return nil, err
+ }
+
+ raw := getValue(item)
+
+ var g GetDNSSettingsAPIResponse
+ err = mapstructure.Decode(raw, &g)
+ if err != nil {
+ return nil, fmt.Errorf("response struct decode: %w", err)
+ }
+
+ c.updateFloodTime(g.Response.KasFloodDelay)
+
+ return g.Response.ReturnInfo, nil
+}
+
+// AddDNSSettings Creation of a DNS resource record.
+func (c *Client) AddDNSSettings(credentialToken string, record DNSRequest) (string, error) {
+ item, err := c.do(credentialToken, "add_dns_settings", record)
+ if err != nil {
+ return "", err
+ }
+
+ raw := getValue(item)
+
+ var g AddDNSSettingsAPIResponse
+ err = mapstructure.Decode(raw, &g)
+ if err != nil {
+ return "", fmt.Errorf("response struct decode: %w", err)
+ }
+
+ c.updateFloodTime(g.Response.KasFloodDelay)
+
+ return g.Response.ReturnInfo, nil
+}
+
+// DeleteDNSSettings Deleting a DNS Resource Record.
+func (c *Client) DeleteDNSSettings(credentialToken, recordID string) (bool, error) {
+ requestParams := map[string]string{"record_id": recordID}
+
+ item, err := c.do(credentialToken, "delete_dns_settings", requestParams)
+ if err != nil {
+ return false, err
+ }
+
+ raw := getValue(item)
+
+ var g DeleteDNSSettingsAPIResponse
+ err = mapstructure.Decode(raw, &g)
+ if err != nil {
+ return false, fmt.Errorf("response struct decode: %w", err)
+ }
+
+ c.updateFloodTime(g.Response.KasFloodDelay)
+
+ return g.Response.ReturnInfo, nil
+}
+
+func (c Client) do(credentialToken, action string, requestParams interface{}) (*Item, error) {
+ time.Sleep(time.Until(c.floodTime))
+
+ ar := KasRequest{
+ Login: c.login,
+ AuthType: "session",
+ AuthData: credentialToken,
+ Action: action,
+ RequestParams: requestParams,
+ }
+
+ body, err := json.Marshal(ar)
+ if err != nil {
+ return nil, fmt.Errorf("request marshal: %w", err)
+ }
+
+ payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))
+
+ req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(payload))
+ if err != nil {
+ return nil, fmt.Errorf("request creation: %w", err)
+ }
+
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("request execution: %w", err)
+ }
+
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ data, _ := ioutil.ReadAll(resp.Body)
+ return nil, fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data))
+ }
+
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("response read: %w", err)
+ }
+
+ var e KasAPIResponseEnvelope
+ decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))})
+ err = decoder.Decode(&e)
+ if err != nil {
+ return nil, fmt.Errorf("response xml decode: %w", err)
+ }
+
+ if e.Body.Fault != nil {
+ return nil, e.Body.Fault
+ }
+
+ return e.Body.KasAPIResponse.Return, nil
+}
+
+func (c *Client) updateFloodTime(delay float64) {
+ c.floodTime = time.Now().Add(time.Duration(delay * float64(time.Second)))
+}
+
+func getValue(item *Item) interface{} {
+ switch {
+ case item.Raw != "":
+ v, _ := strconv.ParseBool(item.Raw)
+ return v
+
+ case item.Text != "":
+ switch item.Type {
+ case "xsd:string":
+ return item.Text
+ case "xsd:float":
+ v, _ := strconv.ParseFloat(item.Text, 64)
+ return v
+ case "xsd:int":
+ v, _ := strconv.ParseInt(item.Text, 10, 64)
+ return v
+ default:
+ return item.Text
+ }
+
+ case item.Value != nil:
+ return getValue(item.Value)
+
+ case len(item.Items) > 0 && item.Type == "SOAP-ENC:Array":
+ var v []interface{}
+ for _, i := range item.Items {
+ v = append(v, getValue(i))
+ }
+
+ return v
+
+ case len(item.Items) > 0:
+ v := map[string]interface{}{}
+ for _, i := range item.Items {
+ v[getKey(i)] = getValue(i)
+ }
+
+ return v
+
+ default:
+ return ""
+ }
+}
+
+func getKey(item *Item) string {
+ if item.Key == nil {
+ return ""
+ }
+
+ return item.Key.Text
+}
diff --git a/providers/dns/allinkl/internal/client_test.go b/providers/dns/allinkl/internal/client_test.go
new file mode 100644
index 00000000..e2b51d1e
--- /dev/null
+++ b/providers/dns/allinkl/internal/client_test.go
@@ -0,0 +1,194 @@
+package internal
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestClient_Authentication(t *testing.T) {
+ mux := http.NewServeMux()
+ server := httptest.NewServer(mux)
+ t.Cleanup(server.Close)
+
+ mux.HandleFunc("/", testHandler("auth.xml"))
+
+ client := NewClient("user", "secret")
+ client.authEndpoint = server.URL
+
+ credentialToken, err := client.Authentication(60, false)
+ require.NoError(t, err)
+
+ assert.Equal(t, "593959ca04f0de9689b586c6a647d15d", credentialToken)
+}
+
+func TestClient_Authentication_error(t *testing.T) {
+ mux := http.NewServeMux()
+ server := httptest.NewServer(mux)
+ t.Cleanup(server.Close)
+
+ mux.HandleFunc("/", testHandler("auth_fault.xml"))
+
+ client := NewClient("user", "secret")
+ client.authEndpoint = server.URL
+
+ _, err := client.Authentication(60, false)
+ require.Error(t, err)
+}
+
+func TestClient_GetDNSSettings(t *testing.T) {
+ mux := http.NewServeMux()
+ server := httptest.NewServer(mux)
+ t.Cleanup(server.Close)
+
+ mux.HandleFunc("/", testHandler("get_dns_settings.xml"))
+
+ client := NewClient("user", "secret")
+ client.apiEndpoint = server.URL
+
+ token := "sha1secret"
+
+ records, err := client.GetDNSSettings(token, "example.com", "")
+ require.NoError(t, err)
+
+ expected := []ReturnInfo{
+ {
+ ID: "57297429",
+ Zone: "example.org",
+ Name: "",
+ Type: "A",
+ Data: "10.0.0.1",
+ Changeable: "Y",
+ Aux: 0,
+ },
+ {
+ ID: int64(0),
+ Zone: "example.org",
+ Name: "",
+ Type: "NS",
+ Data: "ns5.kasserver.com.",
+ Changeable: "N",
+ Aux: 0,
+ },
+ {
+ ID: int64(0),
+ Zone: "example.org",
+ Name: "",
+ Type: "NS",
+ Data: "ns6.kasserver.com.",
+ Changeable: "N",
+ Aux: 0,
+ },
+ {
+ ID: "57297479",
+ Zone: "example.org",
+ Name: "*",
+ Type: "A",
+ Data: "10.0.0.1",
+ Changeable: "Y",
+ Aux: 0,
+ },
+ {
+ ID: "57297481",
+ Zone: "example.org",
+ Name: "",
+ Type: "MX",
+ Data: "user.kasserver.com.",
+ Changeable: "Y",
+ Aux: 10,
+ },
+ {
+ ID: "57297483",
+ Zone: "example.org",
+ Name: "",
+ Type: "TXT",
+ Data: "v=spf1 mx a ?all",
+ Changeable: "Y",
+ Aux: 0,
+ },
+ {
+ ID: "57297485",
+ Zone: "example.org",
+ Name: "_dmarc",
+ Type: "TXT",
+ Data: "v=DMARC1; p=none;",
+ Changeable: "Y",
+ Aux: 0,
+ },
+ }
+
+ assert.Equal(t, expected, records)
+}
+
+func TestClient_AddDNSSettings(t *testing.T) {
+ mux := http.NewServeMux()
+ server := httptest.NewServer(mux)
+ t.Cleanup(server.Close)
+
+ mux.HandleFunc("/", testHandler("add_dns_settings.xml"))
+
+ client := NewClient("user", "secret")
+ client.apiEndpoint = server.URL
+
+ token := "sha1secret"
+
+ record := DNSRequest{
+ ZoneHost: "42cnc.de.",
+ RecordType: "TXT",
+ RecordName: "lego",
+ RecordData: "abcdefgh",
+ }
+
+ recordID, err := client.AddDNSSettings(token, record)
+ require.NoError(t, err)
+
+ assert.Equal(t, "57347444", recordID)
+}
+
+func TestClient_DeleteDNSSettings(t *testing.T) {
+ mux := http.NewServeMux()
+ server := httptest.NewServer(mux)
+ t.Cleanup(server.Close)
+
+ mux.HandleFunc("/", testHandler("delete_dns_settings.xml"))
+
+ client := NewClient("user", "secret")
+ client.apiEndpoint = server.URL
+
+ token := "sha1secret"
+
+ r, err := client.DeleteDNSSettings(token, "57347450")
+ require.NoError(t, err)
+
+ assert.True(t, r)
+}
+
+func testHandler(filename string) http.HandlerFunc {
+ return func(rw http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
+ return
+ }
+
+ file, err := os.Open(filepath.Join("fixtures", filename))
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ defer func() { _ = file.Close() }()
+
+ _, err = io.Copy(rw, file)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+}
diff --git a/providers/dns/allinkl/internal/fixtures/add_dns_settings.json b/providers/dns/allinkl/internal/fixtures/add_dns_settings.json
new file mode 100644
index 00000000..9d5cd65f
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/add_dns_settings.json
@@ -0,0 +1,18 @@
+{
+ "Request": {
+ "KasRequestParams": {
+ "record_aux": 0,
+ "record_data": "abcdefgh",
+ "record_name": "lego",
+ "record_type": "TXT",
+ "zone_host": "example.org."
+ },
+ "KasRequestTime": 1625014992,
+ "KasRequestType": true
+ },
+ "Response": {
+ "KasFloodDelay": 0.5,
+ "ReturnInfo": "57347444",
+ "ReturnString": "TRUE"
+ }
+}
diff --git a/providers/dns/allinkl/internal/fixtures/add_dns_settings.xml b/providers/dns/allinkl/internal/fixtures/add_dns_settings.xml
new file mode 100644
index 00000000..8fa3a420
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/add_dns_settings.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ -
+ Request
+
+
-
+ KasRequestTime
+ 1625014992
+
+ -
+ KasRequestType
+
+
+ -
+ KasRequestParams
+
+
-
+ zone_host
+ example.org.
+
+ -
+ record_type
+ TXT
+
+ -
+ record_name
+ lego
+
+ -
+ record_data
+ abcdefgh
+
+ -
+ record_aux
+ 0
+
+
+
+
+
+ -
+ Response
+
+
-
+ KasFloodDelay
+ 0.5
+
+ -
+ ReturnString
+ TRUE
+
+ -
+ ReturnInfo
+ 57347444
+
+
+
+
+
+
+
diff --git a/providers/dns/allinkl/internal/fixtures/auth.xml b/providers/dns/allinkl/internal/fixtures/auth.xml
new file mode 100644
index 00000000..349b544e
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/auth.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ 593959ca04f0de9689b586c6a647d15d
+
+
+
diff --git a/providers/dns/allinkl/internal/fixtures/auth_fault.xml b/providers/dns/allinkl/internal/fixtures/auth_fault.xml
new file mode 100644
index 00000000..a945063d
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/auth_fault.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ SOAP-ENV:Client
+ kas_login_syntax_incorrect
+ KasAuth
+
+
+
diff --git a/providers/dns/allinkl/internal/fixtures/delete_dns_settings.json b/providers/dns/allinkl/internal/fixtures/delete_dns_settings.json
new file mode 100644
index 00000000..7ac41f18
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/delete_dns_settings.json
@@ -0,0 +1,14 @@
+{
+ "Request": {
+ "KasRequestParams": {
+ "record_id": "57347444"
+ },
+ "KasRequestTime": 1625016066,
+ "KasRequestType": true
+ },
+ "Response": {
+ "KasFloodDelay": 0.5,
+ "ReturnInfo": true,
+ "ReturnString": "TRUE"
+ }
+}
diff --git a/providers/dns/allinkl/internal/fixtures/delete_dns_settings.xml b/providers/dns/allinkl/internal/fixtures/delete_dns_settings.xml
new file mode 100644
index 00000000..42f52e3f
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/delete_dns_settings.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+ -
+ Request
+
+
-
+ KasRequestTime
+ 1625016066
+
+ -
+ KasRequestType
+
+
+ -
+ KasRequestParams
+
+
-
+ record_id
+ 57347444
+
+
+
+
+
+ -
+ Response
+
+
-
+ KasFloodDelay
+ 0.5
+
+ -
+ ReturnString
+ TRUE
+
+ -
+ ReturnInfo
+
+
+
+
+
+
+
+
diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings.json b/providers/dns/allinkl/internal/fixtures/get_dns_settings.json
new file mode 100644
index 00000000..724eb32f
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings.json
@@ -0,0 +1,78 @@
+{
+ "Request": {
+ "KasRequestParams": {
+ "zone_host": "example.org"
+ },
+ "KasRequestTime": 1625012975,
+ "KasRequestType": true
+ },
+ "Response": {
+ "KasFloodDelay": 0.5,
+ "ReturnInfo": [
+ {
+ "record_aux": 0,
+ "record_changeable": "Y",
+ "record_data": "10.0.0.1",
+ "record_id": "57297429",
+ "record_name": "",
+ "record_type": "A",
+ "record_zone": "example.org"
+ },
+ {
+ "record_aux": 0,
+ "record_changeable": "N",
+ "record_data": "ns5.kasserver.com.",
+ "record_id": 0,
+ "record_name": "",
+ "record_type": "NS",
+ "record_zone": "example.org"
+ },
+ {
+ "record_aux": 0,
+ "record_changeable": "N",
+ "record_data": "ns6.kasserver.com.",
+ "record_id": 0,
+ "record_name": "",
+ "record_type": "NS",
+ "record_zone": "example.org"
+ },
+ {
+ "record_aux": 0,
+ "record_changeable": "Y",
+ "record_data": "10.0.0.1",
+ "record_id": "57297479",
+ "record_name": "*",
+ "record_type": "A",
+ "record_zone": "example.org"
+ },
+ {
+ "record_aux": 10,
+ "record_changeable": "Y",
+ "record_data": "user.kasserver.com.",
+ "record_id": "57297481",
+ "record_name": "",
+ "record_type": "MX",
+ "record_zone": "example.org"
+ },
+ {
+ "record_aux": 0,
+ "record_changeable": "Y",
+ "record_data": "v=spf1 mx a ?all",
+ "record_id": "57297483",
+ "record_name": "",
+ "record_type": "TXT",
+ "record_zone": "example.org"
+ },
+ {
+ "record_aux": 0,
+ "record_changeable": "Y",
+ "record_data": "v=DMARC1; p=none;",
+ "record_id": "57297485",
+ "record_name": "_dmarc",
+ "record_type": "TXT",
+ "record_zone": "example.org"
+ }
+ ],
+ "ReturnString": "TRUE"
+ }
+}
diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings.xml
new file mode 100644
index 00000000..6bdb0095
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings.xml
@@ -0,0 +1,263 @@
+
+
+
+
+
+ -
+ Request
+
+
-
+ KasRequestTime
+ 1624993260
+
+ -
+ KasRequestType
+
+
+ -
+ KasRequestParams
+
+
-
+ zone_host
+ example.org
+
+
+
+
+
+ -
+ Response
+
+
-
+ KasFloodDelay
+ 0.5
+
+ -
+ ReturnString
+ TRUE
+
+ -
+ ReturnInfo
+
+
-
+
-
+ record_zone
+ example.org
+
+ -
+ record_name
+
+
+ -
+ record_type
+ A
+
+ -
+ record_data
+ 10.0.0.1
+
+ -
+ record_aux
+ 0
+
+ -
+ record_id
+ 57297429
+
+ -
+ record_changeable
+ Y
+
+
+ -
+
-
+ record_zone
+ example.org
+
+ -
+ record_name
+
+
+ -
+ record_type
+ NS
+
+ -
+ record_data
+ ns5.kasserver.com.
+
+ -
+ record_aux
+ 0
+
+ -
+ record_id
+ 0
+
+ -
+ record_changeable
+ N
+
+
+ -
+
-
+ record_zone
+ example.org
+
+ -
+ record_name
+
+
+ -
+ record_type
+ NS
+
+ -
+ record_data
+ ns6.kasserver.com.
+
+ -
+ record_aux
+ 0
+
+ -
+ record_id
+ 0
+
+ -
+ record_changeable
+ N
+
+
+ -
+
-
+ record_zone
+ example.org
+
+ -
+ record_name
+ *
+
+ -
+ record_type
+ A
+
+ -
+ record_data
+ 10.0.0.1
+
+ -
+ record_aux
+ 0
+
+ -
+ record_id
+ 57297479
+
+ -
+ record_changeable
+ Y
+
+
+ -
+
-
+ record_zone
+ example.org
+
+ -
+ record_name
+
+
+ -
+ record_type
+ MX
+
+ -
+ record_data
+ user.kasserver.com.
+
+ -
+ record_aux
+ 10
+
+ -
+ record_id
+ 57297481
+
+ -
+ record_changeable
+ Y
+
+
+ -
+
-
+ record_zone
+ example.org
+
+ -
+ record_name
+
+
+ -
+ record_type
+ TXT
+
+ -
+ record_data
+ v=spf1 mx a ?all
+
+ -
+ record_aux
+ 0
+
+ -
+ record_id
+ 57297483
+
+ -
+ record_changeable
+ Y
+
+
+ -
+
-
+ record_zone
+ example.org
+
+ -
+ record_name
+ _dmarc
+
+ -
+ record_type
+ TXT
+
+ -
+ record_data
+ v=DMARC1; p=none;
+
+ -
+ record_aux
+ 0
+
+ -
+ record_id
+ 57297485
+
+ -
+ record_changeable
+ Y
+
+
+
+
+
+
+
+
+
+
diff --git a/providers/dns/allinkl/internal/types.go b/providers/dns/allinkl/internal/types.go
new file mode 100644
index 00000000..ac2ddd39
--- /dev/null
+++ b/providers/dns/allinkl/internal/types.go
@@ -0,0 +1,46 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+)
+
+// Trimmer trim all XML fields.
+type Trimmer struct {
+ decoder *xml.Decoder
+}
+
+func (tr Trimmer) Token() (xml.Token, error) {
+ t, err := tr.decoder.Token()
+ if cd, ok := t.(xml.CharData); ok {
+ t = xml.CharData(bytes.TrimSpace(cd))
+ }
+ return t, err
+}
+
+// Fault a SOAP fault.
+type Fault struct {
+ Code string `xml:"faultcode"`
+ Message string `xml:"faultstring"`
+ Actor string `xml:"faultactor"`
+}
+
+func (f Fault) Error() string {
+ return fmt.Sprintf("%s: %s: %s", f.Actor, f.Code, f.Message)
+}
+
+// KasResponse a KAS SOAP response.
+type KasResponse struct {
+ Return *Item `xml:"return"`
+}
+
+// Item an item of the KAS SOAP response.
+type Item struct {
+ Text string `xml:",chardata" json:"text,omitempty"`
+ Type string `xml:"type,attr" json:"type,omitempty"`
+ Raw string `xml:"nil,attr" json:"raw,omitempty"`
+ Key *Item `xml:"key" json:"key,omitempty"`
+ Value *Item `xml:"value" json:"value,omitempty"`
+ Items []*Item `xml:"item" json:"item,omitempty"`
+}
diff --git a/providers/dns/allinkl/internal/types_api.go b/providers/dns/allinkl/internal/types_api.go
new file mode 100644
index 00000000..49db25a3
--- /dev/null
+++ b/providers/dns/allinkl/internal/types_api.go
@@ -0,0 +1,94 @@
+package internal
+
+import "encoding/xml"
+
+// kasAPIEnvelope a KAS API request envelope.
+const kasAPIEnvelope = `
+
+
+
+ %s
+
+
+`
+
+// KasAPIResponseEnvelope a KAS envelope of the API response.
+type KasAPIResponseEnvelope struct {
+ XMLName xml.Name `xml:"Envelope"`
+ Body KasAPIBody `xml:"Body"`
+}
+
+type KasAPIBody struct {
+ KasAPIResponse *KasResponse `xml:"KasApiResponse"`
+ Fault *Fault `xml:"Fault"`
+}
+
+// ---
+
+type KasRequest struct {
+ // Login the relevant KAS login.
+ Login string `json:"kas_login,omitempty"`
+ // AuthType the authentication type.
+ AuthType string `json:"kas_auth_type,omitempty"`
+ // AuthData the authentication data.
+ AuthData string `json:"kas_auth_data,omitempty"`
+ // Action API function.
+ Action string `json:"kas_action,omitempty"`
+ // RequestParams Parameters to the API function.
+ RequestParams interface{} `json:"KasRequestParams,omitempty"`
+}
+
+type DNSRequest struct {
+ // ZoneHost the zone in question (must be a FQDN).
+ ZoneHost string `json:"zone_host"`
+ // RecordType the TYPE of the resource record (MX, A, AAAA etc.).
+ RecordType string `json:"record_type"`
+ // RecordName the NAME of the resource record.
+ RecordName string `json:"record_name"`
+ // RecordData the DATA of the resource record.
+ RecordData string `json:"record_data"`
+ // RecordAux the AUX of the resource record.
+ RecordAux int `json:"record_aux"`
+}
+
+// ---
+
+type GetDNSSettingsAPIResponse struct {
+ Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"`
+}
+
+type GetDNSSettingsResponse struct {
+ KasFloodDelay float64 `json:"KasFloodDelay" mapstructure:"KasFloodDelay"`
+ ReturnInfo []ReturnInfo `json:"ReturnInfo" mapstructure:"ReturnInfo"`
+ ReturnString string `json:"ReturnString"`
+}
+
+type ReturnInfo struct {
+ ID interface{} `json:"record_id,omitempty" mapstructure:"record_id"`
+ Zone string `json:"record_zone,omitempty" mapstructure:"record_zone"`
+ Name string `json:"record_name,omitempty" mapstructure:"record_name"`
+ Type string `json:"record_type,omitempty" mapstructure:"record_type"`
+ Data string `json:"record_data,omitempty" mapstructure:"record_data"`
+ Changeable string `json:"record_changeable,omitempty" mapstructure:"record_changeable"`
+ Aux int `json:"record_aux,omitempty" mapstructure:"record_aux"`
+}
+
+type AddDNSSettingsAPIResponse struct {
+ Response AddDNSSettingsResponse `json:"Response" mapstructure:"Response"`
+}
+
+type AddDNSSettingsResponse struct {
+ KasFloodDelay float64 `json:"KasFloodDelay" mapstructure:"KasFloodDelay"`
+ ReturnInfo string `json:"ReturnInfo" mapstructure:"ReturnInfo"`
+ ReturnString string `json:"ReturnString" mapstructure:"ReturnString"`
+}
+
+type DeleteDNSSettingsAPIResponse struct {
+ Response DeleteDNSSettingsResponse `json:"Response"`
+}
+
+type DeleteDNSSettingsResponse struct {
+ KasFloodDelay float64 `json:"KasFloodDelay"`
+ ReturnInfo bool `json:"ReturnInfo"`
+ ReturnString string `json:"ReturnString"`
+}
diff --git a/providers/dns/allinkl/internal/types_auth.go b/providers/dns/allinkl/internal/types_auth.go
new file mode 100644
index 00000000..181f76df
--- /dev/null
+++ b/providers/dns/allinkl/internal/types_auth.go
@@ -0,0 +1,34 @@
+package internal
+
+import "encoding/xml"
+
+// kasAuthEnvelope a KAS authentication request envelope.
+const kasAuthEnvelope = `
+
+
+
+ %s
+
+
+`
+
+// KasAuthEnvelope a KAS envelope of the authentication response.
+type KasAuthEnvelope struct {
+ XMLName xml.Name `xml:"Envelope"`
+ Body KasAuthBody `xml:"Body"`
+}
+
+type KasAuthBody struct {
+ KasAuthResponse *KasResponse `xml:"KasAuthResponse"`
+ Fault *Fault `xml:"Fault"`
+}
+
+// ---
+
+type AuthRequest struct {
+ Login string `json:"kas_login,omitempty"`
+ AuthData string `json:"kas_auth_data,omitempty"`
+ AuthType string `json:"kas_auth_type,omitempty"`
+ SessionLifetime int `json:"session_lifetime,omitempty"`
+ SessionUpdateLifetime string `json:"session_update_lifetime,omitempty"`
+}
diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go
index bd36f4be..b9ef5f33 100644
--- a/providers/dns/dns_providers.go
+++ b/providers/dns/dns_providers.go
@@ -7,6 +7,7 @@ import (
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/providers/dns/acmedns"
"github.com/go-acme/lego/v4/providers/dns/alidns"
+ "github.com/go-acme/lego/v4/providers/dns/allinkl"
"github.com/go-acme/lego/v4/providers/dns/arvancloud"
"github.com/go-acme/lego/v4/providers/dns/auroradns"
"github.com/go-acme/lego/v4/providers/dns/autodns"
@@ -104,6 +105,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return acmedns.NewDNSProvider()
case "alidns":
return alidns.NewDNSProvider()
+ case "allinkl":
+ return allinkl.NewDNSProvider()
case "arvancloud":
return arvancloud.NewDNSProvider()
case "azure":