diff --git a/README.md b/README.md
index 66ed2e4d6..50da4d5d3 100644
--- a/README.md
+++ b/README.md
@@ -70,188 +70,188 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
| Azure DNS |
Baidu Cloud |
+ Binary Lane |
Bindman |
- Bluecat |
+ | Bluecat |
BookMyName |
Brandit (deprecated) |
Bunny |
- Checkdomain |
+ | Checkdomain |
Civo |
Cloud.ru |
CloudDNS |
- Cloudflare |
+ | Cloudflare |
ClouDNS |
CloudXNS (Deprecated) |
ConoHa v2 |
- ConoHa v3 |
+ | ConoHa v3 |
Constellix |
Core-Networks |
CPanel/WHM |
- Derak Cloud |
+ | Derak Cloud |
deSEC.io |
Designate DNSaaS for Openstack |
Digital Ocean |
- DirectAdmin |
+ | DirectAdmin |
DNS Made Easy |
dnsHome.de |
DNSimple |
- DNSPod (deprecated) |
+ | DNSPod (deprecated) |
Domain Offensive (do.de) |
Domeneshop |
DreamHost |
- Duck DNS |
+ | Duck DNS |
Dyn |
DynDnsFree.de |
Dynu |
- EasyDNS |
+ | EasyDNS |
Efficient IP |
Epik |
Exoscale |
- External program |
+ | External program |
F5 XC |
freemyip.com |
G-Core |
- Gandi |
+ | Gandi |
Gandi Live DNS (v5) |
Glesys |
Go Daddy |
- Google Cloud |
+ | Google Cloud |
Google Domains |
Hetzner |
Hosting.de |
- Hosttech |
+ | Hosttech |
HTTP request |
http.net |
Huawei Cloud |
- Hurricane Electric DNS |
+ | Hurricane Electric DNS |
HyperOne |
IBM Cloud (SoftLayer) |
IIJ DNS Platform Service |
- Infoblox |
+ | Infoblox |
Infomaniak |
Internet Initiative Japan |
Internet.bs |
- INWX |
+ | INWX |
Ionos |
IPv64 |
iwantmyname |
- Joker |
+ | Joker |
Joohoi's ACME-DNS |
Liara |
Lima-City |
- Linode (v4) |
+ | Linode (v4) |
Liquid Web |
Loopia |
LuaDNS |
- Mail-in-a-Box |
+ | Mail-in-a-Box |
ManageEngine CloudDNS |
Manual |
Metaname |
- Metaregistrar |
+ | Metaregistrar |
mijn.host |
Mittwald |
myaddr.{tools,dev,io} |
- MyDNS.jp |
+ | MyDNS.jp |
MythicBeasts |
Name.com |
Namecheap |
- Namesilo |
+ | Namesilo |
NearlyFreeSpeech.NET |
Netcup |
Netlify |
- Nicmanager |
+ | Nicmanager |
NIFCloud |
Njalla |
Nodion |
- NS1 |
+ | NS1 |
Open Telekom Cloud |
Oracle Cloud |
OVH |
- plesk.com |
+ | plesk.com |
Porkbun |
PowerDNS |
Rackspace |
- Rain Yun/雨云 |
+ | Rain Yun/雨云 |
RcodeZero |
reg.ru |
Regfish |
- RFC2136 |
+ | RFC2136 |
RimuHosting |
RU CENTER |
Sakura Cloud |
- Scaleway |
+ | Scaleway |
Selectel |
Selectel v2 |
SelfHost.(de|eu) |
- Servercow |
+ | Servercow |
Shellrent |
Simply.com |
Sonic |
- Spaceship |
+ | Spaceship |
Stackpath |
Technitium |
Tencent Cloud DNS |
- Tencent EdgeOne |
+ | Tencent EdgeOne |
Timeweb Cloud |
TransIP |
UKFast SafeDNS |
- Ultradns |
+ | Ultradns |
Variomedia |
VegaDNS |
Vercel |
- Versio.[nl|eu|uk] |
+ | Versio.[nl|eu|uk] |
VinylDNS |
VK Cloud |
Volcano Engine/火山引擎 |
- Vscale |
+ | Vscale |
Vultr |
Webnames |
Websupport |
- WEDOS |
+ | WEDOS |
West.cn/西部数码 |
Yandex 360 |
Yandex Cloud |
- Yandex PDD |
+ | Yandex PDD |
Zone.ee |
ZoneEdit |
Zonomi |
- |
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index 1aacdc7af..d53274139 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -25,6 +25,7 @@ func allDNSCodes() string {
"azure",
"azuredns",
"baiducloud",
+ "binarylane",
"bindman",
"bluecat",
"bookmyname",
@@ -448,6 +449,26 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/baiducloud`)
+ case "binarylane":
+ // generated from: providers/dns/binarylane/binarylane.toml
+ ew.writeln(`Configuration for Binary Lane.`)
+ ew.writeln(`Code: 'binarylane'`)
+ ew.writeln(`Since: 'v4.26.0'`)
+ ew.writeln()
+
+ ew.writeln(`Credentials:`)
+ ew.writeln(` - "BINARYLANE_API_TOKEN": API token`)
+ ew.writeln()
+
+ ew.writeln(`Additional Configuration:`)
+ ew.writeln(` - "BINARYLANE_HTTP_TIMEOUT": API request timeout in seconds (Default: 30)`)
+ ew.writeln(` - "BINARYLANE_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`)
+ ew.writeln(` - "BINARYLANE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`)
+ ew.writeln(` - "BINARYLANE_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)
+
+ ew.writeln()
+ ew.writeln(`More information: https://go-acme.github.io/lego/dns/binarylane`)
+
case "bindman":
// generated from: providers/dns/bindman/bindman.toml
ew.writeln(`Configuration for Bindman.`)
diff --git a/docs/content/dns/zz_gen_binarylane.md b/docs/content/dns/zz_gen_binarylane.md
new file mode 100644
index 000000000..4d65bb0bc
--- /dev/null
+++ b/docs/content/dns/zz_gen_binarylane.md
@@ -0,0 +1,67 @@
+---
+title: "Binary Lane"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: binarylane
+dnsprovider:
+ since: "v4.26.0"
+ code: "binarylane"
+ url: "https://www.binarylane.com.au/"
+---
+
+
+
+
+
+
+Configuration for [Binary Lane](https://www.binarylane.com.au/).
+
+
+
+
+- Code: `binarylane`
+- Since: v4.26.0
+
+
+Here is an example bash command using the Binary Lane provider:
+
+```bash
+BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \
+lego --email you@example.com --dns binarylane -d '*.example.com' -d example.com run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `BINARYLANE_API_TOKEN` | API token |
+
+The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
+More information [here]({{% ref "dns#configuration-and-credentials" %}}).
+
+
+## Additional Configuration
+
+| Environment Variable Name | Description |
+|--------------------------------|-------------|
+| `BINARYLANE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |
+| `BINARYLANE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |
+| `BINARYLANE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |
+| `BINARYLANE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |
+
+The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
+More information [here]({{% ref "dns#configuration-and-credentials" %}}).
+
+
+
+
+## More information
+
+- [API documentation](https://api.binarylane.com.au/reference/#tag/Domains)
+
+
+
+
diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml
index 62e422217..ea5b037fe 100644
--- a/docs/data/zz_cli_help.toml
+++ b/docs/data/zz_cli_help.toml
@@ -152,7 +152,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
- acme-dns, active24, alidns, allinkl, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
+ acme-dns, active24, alidns, allinkl, arvancloud, auroradns, autodns, axelname, azion, azure, azuredns, baiducloud, binarylane, bindman, bluecat, bookmyname, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, conohav3, constellix, corenetworks, cpanel, derak, desec, designate, digitalocean, directadmin, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dyndnsfree, dynu, easydns, edgedns, edgeone, efficientip, epik, exec, exoscale, f5xc, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, huaweicloud, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, limacity, linode, liquidweb, loopia, luadns, mailinabox, manageengine, manual, metaname, metaregistrar, mijnhost, mittwald, myaddr, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rainyun, rcodezero, regfish, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, selfhostde, servercow, shellrent, simply, sonic, spaceship, stackpath, technitium, tencentcloud, timewebcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, volcengine, vscale, vultr, webnames, websupport, wedos, westcn, yandex, yandex360, yandexcloud, zoneedit, zoneee, zonomi
More information: https://go-acme.github.io/lego/dns
"""
diff --git a/providers/dns/binarylane/binarylane.go b/providers/dns/binarylane/binarylane.go
new file mode 100644
index 000000000..d8f459e2f
--- /dev/null
+++ b/providers/dns/binarylane/binarylane.go
@@ -0,0 +1,157 @@
+// Package binarylane implements a DNS provider for solving the DNS-01 challenge using Binary Lane.
+package binarylane
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "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/binarylane/internal"
+)
+
+// Environment variables names.
+const (
+ envNamespace = "BINARYLANE_"
+
+ EnvAPIToken = envNamespace + "API_TOKEN"
+
+ EnvTTL = envNamespace + "TTL"
+ 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 {
+ APIToken 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{
+ TTL: env.GetOrDefaultInt(EnvTTL, 3600),
+ 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]int64
+ recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Binary Lane.
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAPIToken)
+ if err != nil {
+ return nil, fmt.Errorf("binarylane: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.APIToken = values[EnvAPIToken]
+
+ return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for Binary Lane.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("binarylane: the configuration of the DNS provider is nil")
+ }
+
+ client, err := internal.NewClient(config.APIToken)
+ if err != nil {
+ return nil, fmt.Errorf("binarylane: %w", err)
+ }
+
+ if config.HTTPClient != nil {
+ client.HTTPClient = config.HTTPClient
+ }
+
+ return &DNSProvider{
+ config: config,
+ client: client,
+ recordIDs: make(map[string]int64),
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+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("binarylane: could not find zone for domain %q: %w", domain, err)
+ }
+
+ subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+ if err != nil {
+ return fmt.Errorf("binarylane: %w", err)
+ }
+
+ record := internal.Record{
+ Type: "TXT",
+ Name: subDomain,
+ Data: info.Value,
+ TTL: d.config.TTL,
+ }
+
+ response, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record)
+ if err != nil {
+ return fmt.Errorf("binarylane: create record: %w", err)
+ }
+
+ d.recordIDsMu.Lock()
+ d.recordIDs[token] = response.ID
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("binarylane: could not find zone for domain %q: %w", domain, err)
+ }
+
+ // get 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("binarylane: unknown record ID for '%s'", info.EffectiveFQDN)
+ }
+
+ err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)
+ if err != nil {
+ return fmt.Errorf("binarylane: delete record: %w", err)
+ }
+
+ return 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
+}
diff --git a/providers/dns/binarylane/binarylane.toml b/providers/dns/binarylane/binarylane.toml
new file mode 100644
index 000000000..5038fc3e6
--- /dev/null
+++ b/providers/dns/binarylane/binarylane.toml
@@ -0,0 +1,22 @@
+Name = "Binary Lane"
+Description = ''''''
+URL = "https://www.binarylane.com.au/"
+Code = "binarylane"
+Since = "v4.26.0"
+
+Example = '''
+BINARYLANE_API_TOKEN="xxxxxxxxxxxxxxxxxxxxx" \
+lego --email you@example.com --dns binarylane -d '*.example.com' -d example.com run
+'''
+
+[Configuration]
+ [Configuration.Credentials]
+ BINARYLANE_API_TOKEN = "API token"
+ [Configuration.Additional]
+ BINARYLANE_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)"
+ BINARYLANE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)"
+ BINARYLANE_TTL = "The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)"
+ BINARYLANE_HTTP_TIMEOUT = "API request timeout in seconds (Default: 30)"
+
+[Links]
+ API = "https://api.binarylane.com.au/reference/#tag/Domains"
diff --git a/providers/dns/binarylane/binarylane_test.go b/providers/dns/binarylane/binarylane_test.go
new file mode 100644
index 000000000..9519fe0f2
--- /dev/null
+++ b/providers/dns/binarylane/binarylane_test.go
@@ -0,0 +1,115 @@
+package binarylane
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester"
+ "github.com/stretchr/testify/require"
+)
+
+const envDomain = envNamespace + "DOMAIN"
+
+var envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+ testCases := []struct {
+ desc string
+ envVars map[string]string
+ expected string
+ }{
+ {
+ desc: "success",
+ envVars: map[string]string{
+ EnvAPIToken: "secret",
+ },
+ },
+ {
+ desc: "missing API token",
+ envVars: map[string]string{
+ EnvAPIToken: "",
+ },
+ expected: "binarylane: some credentials information are missing: BINARYLANE_API_TOKEN",
+ },
+ }
+
+ 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
+ apiToken string
+ expected string
+ }{
+ {
+ desc: "success",
+ apiToken: "secret",
+ },
+ {
+ desc: "missing API token",
+ expected: "binarylane: credentials missing",
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ config := NewDefaultConfig()
+ config.APIToken = test.apiToken
+
+ 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/binarylane/internal/client.go b/providers/dns/binarylane/internal/client.go
new file mode 100644
index 000000000..45518cb41
--- /dev/null
+++ b/providers/dns/binarylane/internal/client.go
@@ -0,0 +1,146 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "time"
+
+ "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
+)
+
+const defaultBaseURL = "https://api.binarylane.com.au/v2/"
+
+const authorizationHeader = "Authorization"
+
+// Client the Binary Lane API client.
+type Client struct {
+ apiToken string
+
+ baseURL *url.URL
+ HTTPClient *http.Client
+}
+
+// NewClient creates a new Client.
+func NewClient(apiToken string) (*Client, error) {
+ if apiToken == "" {
+ return nil, errors.New("credentials missing")
+ }
+
+ baseURL, _ := url.Parse(defaultBaseURL)
+
+ return &Client{
+ apiToken: apiToken,
+ baseURL: baseURL,
+ HTTPClient: &http.Client{Timeout: 10 * time.Second},
+ }, nil
+}
+
+// CreateRecord Creates a new domain record.
+// https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records/post
+func (c *Client) CreateRecord(ctx context.Context, domain string, record Record) (*Record, error) {
+ endpoint := c.baseURL.JoinPath("domains", domain, "records")
+
+ if record.Name == "" {
+ record.Name = "@"
+ }
+
+ req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
+ if err != nil {
+ return nil, err
+ }
+
+ var result APIResponse
+ err = c.do(req, &result)
+ if err != nil {
+ return nil, err
+ }
+
+ return result.DomainRecord, nil
+}
+
+// DeleteRecord Deletes an existing domain record.
+// https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records~1%7Brecord_id%7D/delete
+func (c *Client) DeleteRecord(ctx context.Context, domainName string, recordID int64) error {
+ endpoint := c.baseURL.JoinPath("domains", domainName, "records", strconv.FormatInt(recordID, 10))
+
+ req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
+ if err != nil {
+ return err
+ }
+
+ return c.do(req, nil)
+}
+
+func (c *Client) do(req *http.Request, result any) error {
+ req.Header.Set(authorizationHeader, "Bearer "+c.apiToken)
+
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil {
+ return errutils.NewHTTPDoError(req, err)
+ }
+
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode/100 != 2 {
+ return parseError(req, resp)
+ }
+
+ if result == nil {
+ return nil
+ }
+
+ 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 nil
+}
+
+func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, 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.String(), 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
+}
+
+func parseError(req *http.Request, resp *http.Response) error {
+ raw, _ := io.ReadAll(resp.Body)
+
+ var errAPI APIError
+ err := json.Unmarshal(raw, &errAPI)
+ if err != nil {
+ return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
+ }
+
+ return &errAPI
+}
diff --git a/providers/dns/binarylane/internal/client_test.go b/providers/dns/binarylane/internal/client_test.go
new file mode 100644
index 000000000..0398d5adf
--- /dev/null
+++ b/providers/dns/binarylane/internal/client_test.go
@@ -0,0 +1,97 @@
+package internal
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
+ "github.com/stretchr/testify/require"
+)
+
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("secret")
+ if err != nil {
+ return nil, err
+ }
+
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer secret"),
+ )
+}
+
+func TestClient_CreateRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /domains/example.com/records",
+ servermock.ResponseFromFixture("create_record.json"),
+ servermock.CheckRequestJSONBodyFromFixture("create_record-request.json")).
+ Build(t)
+
+ record := Record{
+ Type: "TXT",
+ Name: "foo",
+ Data: "txtTXTtxt",
+ TTL: 300,
+ }
+
+ rec, err := client.CreateRecord(t.Context(), "example.com", record)
+ require.NoError(t, err)
+
+ expected := &Record{
+ ID: 123,
+ Type: "TXT",
+ Name: "foo",
+ Data: "txtTXTtxt",
+ TTL: 300,
+ }
+
+ require.Equal(t, expected, rec)
+}
+
+func TestClient_CreateRecord_error(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /domains/example.com/records",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
+
+ record := Record{
+ Type: "TXT",
+ Name: "foo",
+ Data: "txtTXTtxt",
+ TTL: 300,
+ }
+
+ _, err := client.CreateRecord(t.Context(), "example.com", record)
+ require.EqualError(t, err, "400: type: title: detail: instance: property1: a")
+}
+
+func TestClient_DeleteRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("DELETE /domains/example.com/records/123",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
+
+ err := client.DeleteRecord(t.Context(), "example.com", 123)
+ require.NoError(t, err)
+}
+
+func TestClient_DeleteRecord_error(t *testing.T) {
+ client := mockBuilder().
+ Route("DELETE /domains/example.com/records/123",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
+
+ err := client.DeleteRecord(t.Context(), "example.com", 123)
+ require.EqualError(t, err, "400: type: title: detail: instance: property1: a")
+}
diff --git a/providers/dns/binarylane/internal/fixtures/create_record-request.json b/providers/dns/binarylane/internal/fixtures/create_record-request.json
new file mode 100644
index 000000000..98a349650
--- /dev/null
+++ b/providers/dns/binarylane/internal/fixtures/create_record-request.json
@@ -0,0 +1,6 @@
+{
+ "type": "TXT",
+ "name": "foo",
+ "data": "txtTXTtxt",
+ "ttl": 300
+}
diff --git a/providers/dns/binarylane/internal/fixtures/create_record.json b/providers/dns/binarylane/internal/fixtures/create_record.json
new file mode 100644
index 000000000..709bef23e
--- /dev/null
+++ b/providers/dns/binarylane/internal/fixtures/create_record.json
@@ -0,0 +1,9 @@
+{
+ "domain_record": {
+ "id": 123,
+ "type": "TXT",
+ "name": "foo",
+ "data": "txtTXTtxt",
+ "ttl": 300
+ }
+}
diff --git a/providers/dns/binarylane/internal/fixtures/error.json b/providers/dns/binarylane/internal/fixtures/error.json
new file mode 100644
index 000000000..79d115f74
--- /dev/null
+++ b/providers/dns/binarylane/internal/fixtures/error.json
@@ -0,0 +1,14 @@
+{
+ "type": "type",
+ "title": "title",
+ "status": 400,
+ "detail": "detail",
+ "instance": "instance",
+ "errors": {
+ "property1": [
+ "a"
+ ]
+ },
+ "property1": null,
+ "property2": null
+}
diff --git a/providers/dns/binarylane/internal/types.go b/providers/dns/binarylane/internal/types.go
new file mode 100644
index 000000000..e6c27410a
--- /dev/null
+++ b/providers/dns/binarylane/internal/types.go
@@ -0,0 +1,42 @@
+package internal
+
+import (
+ "fmt"
+ "strings"
+)
+
+type APIError struct {
+ Type string `json:"type"`
+ Title string `json:"title"`
+ Status int `json:"status"`
+ Detail string `json:"detail"`
+ Instance string `json:"instance"`
+ Errors map[string][]string `json:"errors"`
+}
+
+func (a *APIError) Error() string {
+ msg := fmt.Sprintf("%d: %s: %s: %s: %s", a.Status, a.Type, a.Title, a.Detail, a.Instance)
+
+ for s, values := range a.Errors {
+ msg += fmt.Sprintf(": %s: %s", s, strings.Join(values, ", "))
+ }
+
+ return msg
+}
+
+type Record struct {
+ ID int64 `json:"id,omitempty"`
+ Type string `json:"type,omitempty"`
+ Name string `json:"name,omitempty"`
+ Data string `json:"data,omitempty"`
+ Priority int `json:"priority,omitempty"`
+ Port int `json:"port,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ Weight int `json:"weight,omitempty"`
+ Flags int `json:"flags,omitempty"`
+ Tag string `json:"tag,omitempty"`
+}
+
+type APIResponse struct {
+ DomainRecord *Record `json:"domain_record"`
+}
diff --git a/providers/dns/zz_gen_dns_providers.go b/providers/dns/zz_gen_dns_providers.go
index f3598b7b5..44ab9d493 100644
--- a/providers/dns/zz_gen_dns_providers.go
+++ b/providers/dns/zz_gen_dns_providers.go
@@ -19,6 +19,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/azure"
"github.com/go-acme/lego/v4/providers/dns/azuredns"
"github.com/go-acme/lego/v4/providers/dns/baiducloud"
+ "github.com/go-acme/lego/v4/providers/dns/binarylane"
"github.com/go-acme/lego/v4/providers/dns/bindman"
"github.com/go-acme/lego/v4/providers/dns/bluecat"
"github.com/go-acme/lego/v4/providers/dns/bookmyname"
@@ -196,6 +197,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return azuredns.NewDNSProvider()
case "baiducloud":
return baiducloud.NewDNSProvider()
+ case "binarylane":
+ return binarylane.NewDNSProvider()
case "bindman":
return bindman.NewDNSProvider()
case "bluecat":