liquidweb: detect zone automatically (#2031)

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
Jack Hayhurst 2023-10-14 11:08:29 -04:00 committed by GitHub
parent 2140e6befe
commit 8afdc9d01c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 471 additions and 133 deletions

View file

@ -1635,9 +1635,8 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln() ew.writeln()
ew.writeln(`Credentials:`) ew.writeln(`Credentials:`)
ew.writeln(` - "LIQUID_WEB_PASSWORD": Storm API Password`) ew.writeln(` - "LIQUID_WEB_PASSWORD": Liquid Web API Password`)
ew.writeln(` - "LIQUID_WEB_USERNAME": Storm API Username`) ew.writeln(` - "LIQUID_WEB_USERNAME": Liquid Web API Username`)
ew.writeln(` - "LIQUID_WEB_ZONE": DNS Zone`)
ew.writeln() ew.writeln()
ew.writeln(`Additional Configuration:`) ew.writeln(`Additional Configuration:`)
@ -1645,7 +1644,8 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln(` - "LIQUID_WEB_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "LIQUID_WEB_POLLING_INTERVAL": Time between DNS propagation check`)
ew.writeln(` - "LIQUID_WEB_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "LIQUID_WEB_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
ew.writeln(` - "LIQUID_WEB_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "LIQUID_WEB_TTL": The TTL of the TXT record used for the DNS challenge`)
ew.writeln(` - "LIQUID_WEB_URL": Storm API endpoint`) ew.writeln(` - "LIQUID_WEB_URL": Liquid Web API endpoint`)
ew.writeln(` - "LIQUID_WEB_ZONE": DNS Zone`)
ew.writeln() ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/liquidweb`) ew.writeln(`More information: https://go-acme.github.io/lego/dns/liquidweb`)

View file

@ -28,7 +28,6 @@ Here is an example bash command using the Liquid Web provider:
```bash ```bash
LIQUID_WEB_USERNAME=someuser \ LIQUID_WEB_USERNAME=someuser \
LIQUID_WEB_PASSWORD=somepass \ LIQUID_WEB_PASSWORD=somepass \
LIQUID_WEB_ZONE=tacoman.com.net \
lego --email you@example.com --dns liquidweb --domains my.example.org run lego --email you@example.com --dns liquidweb --domains my.example.org run
``` ```
@ -39,9 +38,8 @@ lego --email you@example.com --dns liquidweb --domains my.example.org run
| Environment Variable Name | Description | | Environment Variable Name | Description |
|-----------------------|-------------| |-----------------------|-------------|
| `LIQUID_WEB_PASSWORD` | Storm API Password | | `LIQUID_WEB_PASSWORD` | Liquid Web API Password |
| `LIQUID_WEB_USERNAME` | Storm API Username | | `LIQUID_WEB_USERNAME` | Liquid Web API Username |
| `LIQUID_WEB_ZONE` | DNS Zone |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. 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 [here]({{< ref "dns#configuration-and-credentials" >}}).
@ -55,7 +53,8 @@ More information [here]({{< ref "dns#configuration-and-credentials" >}}).
| `LIQUID_WEB_POLLING_INTERVAL` | Time between DNS propagation check | | `LIQUID_WEB_POLLING_INTERVAL` | Time between DNS propagation check |
| `LIQUID_WEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `LIQUID_WEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `LIQUID_WEB_TTL` | The TTL of the TXT record used for the DNS challenge | | `LIQUID_WEB_TTL` | The TTL of the TXT record used for the DNS challenge |
| `LIQUID_WEB_URL` | Storm API endpoint | | `LIQUID_WEB_URL` | Liquid Web API endpoint |
| `LIQUID_WEB_ZONE` | DNS Zone |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. 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 [here]({{< ref "dns#configuration-and-credentials" >}}).
@ -65,7 +64,7 @@ More information [here]({{< ref "dns#configuration-and-credentials" >}}).
## More information ## More information
- [API documentation](https://cart.liquidweb.com/storm/api/docs/v1/) - [API documentation](https://api.liquidweb.com/docs/)
- [Go client](https://github.com/liquidweb/liquidweb-go) - [Go client](https://github.com/liquidweb/liquidweb-go)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. --> <!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

3
go.mod
View file

@ -40,7 +40,7 @@ require (
github.com/infobloxopen/infoblox-go-client v1.1.1 github.com/infobloxopen/infoblox-go-client v1.1.1
github.com/labbsr0x/bindman-dns-webhook v1.0.2 github.com/labbsr0x/bindman-dns-webhook v1.0.2
github.com/linode/linodego v1.17.2 github.com/linode/linodego v1.17.2
github.com/liquidweb/liquidweb-go v1.6.3 github.com/liquidweb/liquidweb-go v1.6.4
github.com/mattn/go-isatty v0.0.19 github.com/mattn/go-isatty v0.0.19
github.com/miekg/dns v1.1.55 github.com/miekg/dns v1.1.55
github.com/mimuret/golang-iij-dpf v0.9.1 github.com/mimuret/golang-iij-dpf v0.9.1
@ -134,7 +134,6 @@ require (
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect github.com/labbsr0x/goh v1.0.1 // indirect
github.com/liquidweb/go-lwApi v0.0.5 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

6
go.sum
View file

@ -386,12 +386,10 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt
github.com/linode/linodego v1.17.2 h1:b32dj4662PGG5P9qVa6nBezccWdqgukndlMIuPGq1CQ= github.com/linode/linodego v1.17.2 h1:b32dj4662PGG5P9qVa6nBezccWdqgukndlMIuPGq1CQ=
github.com/linode/linodego v1.17.2/go.mod h1:C2iyT3Vg2O2sPxkWka4XAQ5WSUtm5LmTZ3Adw43Ra7Q= github.com/linode/linodego v1.17.2/go.mod h1:C2iyT3Vg2O2sPxkWka4XAQ5WSUtm5LmTZ3Adw43Ra7Q=
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/go-lwApi v0.0.5 h1:CT4cdXzJXmo0bon298kS7NeSk+Gt8/UHpWBBol1NGCA=
github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM=
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
github.com/liquidweb/liquidweb-go v1.6.3 h1:NVHvcnX3eb3BltiIoA+gLYn15nOpkYkdizOEYGSKrk4= github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc=
github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHSUiajPQs8T9c/Rc= github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=

View file

@ -4,7 +4,9 @@ package liquidweb
import ( import (
"errors" "errors"
"fmt" "fmt"
"sort"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@ -14,7 +16,7 @@ import (
"github.com/liquidweb/liquidweb-go/network" "github.com/liquidweb/liquidweb-go/network"
) )
const defaultBaseURL = "https://api.stormondemand.com" const defaultBaseURL = "https://api.liquidweb.com"
// Environment variables names. // Environment variables names.
const ( const (
@ -45,15 +47,13 @@ type Config struct {
// NewDefaultConfig returns a default configuration for the DNSProvider. // NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config { func NewDefaultConfig() *Config {
config := &Config{ return &Config{
BaseURL: defaultBaseURL, BaseURL: defaultBaseURL,
TTL: env.GetOrDefaultInt(EnvTTL, 300), TTL: env.GetOrDefaultInt(EnvTTL, 300),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 1*time.Minute), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 1*time.Minute),
} }
return config
} }
// DNSProvider implements the challenge.Provider interface. // DNSProvider implements the challenge.Provider interface.
@ -66,7 +66,7 @@ type DNSProvider struct {
// NewDNSProvider returns a DNSProvider instance configured for Liquid Web. // NewDNSProvider returns a DNSProvider instance configured for Liquid Web.
func NewDNSProvider() (*DNSProvider, error) { func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvUsername, EnvPassword, EnvZone) values, err := env.Get(EnvUsername, EnvPassword)
if err != nil { if err != nil {
return nil, fmt.Errorf("liquidweb: %w", err) return nil, fmt.Errorf("liquidweb: %w", err)
} }
@ -75,7 +75,7 @@ func NewDNSProvider() (*DNSProvider, error) {
config.BaseURL = env.GetOrFile(EnvURL) config.BaseURL = env.GetOrFile(EnvURL)
config.Username = values[EnvUsername] config.Username = values[EnvUsername]
config.Password = values[EnvPassword] config.Password = values[EnvPassword]
config.Zone = values[EnvZone] config.Zone = env.GetOrDefaultString(EnvZone, "")
return NewDNSProviderConfig(config) return NewDNSProviderConfig(config)
} }
@ -90,19 +90,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
config.BaseURL = defaultBaseURL config.BaseURL = defaultBaseURL
} }
if config.Zone == "" {
return nil, errors.New("liquidweb: zone is missing")
}
if config.Username == "" {
return nil, errors.New("liquidweb: username is missing")
}
if config.Password == "" {
return nil, errors.New("liquidweb: password is missing")
}
// Initialize LW client.
client, err := lw.NewAPI(config.Username, config.Password, config.BaseURL, int(config.HTTPTimeout.Seconds())) client, err := lw.NewAPI(config.Username, config.Password, config.BaseURL, int(config.HTTPTimeout.Seconds()))
if err != nil { if err != nil {
return nil, fmt.Errorf("liquidweb: could not create Liquid Web API client: %w", err) return nil, fmt.Errorf("liquidweb: could not create Liquid Web API client: %w", err)
@ -133,6 +120,15 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
TTL: d.config.TTL, TTL: d.config.TTL,
} }
if params.Zone == "" {
bestZone, err := d.findZone(params.Name)
if err != nil {
return fmt.Errorf("liquidweb: %w", err)
}
params.Zone = bestZone
}
dnsEntry, err := d.client.NetworkDNS.Create(params) dnsEntry, err := d.client.NetworkDNS.Create(params)
if err != nil { if err != nil {
return fmt.Errorf("liquidweb: could not create TXT record: %w", err) return fmt.Errorf("liquidweb: could not create TXT record: %w", err)
@ -167,3 +163,31 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil return nil
} }
func (d *DNSProvider) findZone(domain string) (string, error) {
zones, err := d.client.NetworkDNSZone.ListAll()
if err != nil {
return "", fmt.Errorf("failed to retrieve zones for account: %w", err)
}
// filter the zones on the account to only ones that match
var zs []network.DNSZone
for _, item := range zones.Items {
if strings.HasSuffix(domain, item.Name) {
zs = append(zs, item)
}
}
if len(zs) < 1 {
return "", fmt.Errorf("no valid zone in account for certificate '%s'", domain)
}
// powerdns _only_ looks for records on the longest matching subdomain zone aka,
// for test.sub.example.com if sub.example.com exists,
// it will look there it will not look atexample.com even if it also exists
sort.Slice(zs, func(i, j int) bool {
return len(zs[i].Name) > len(zs[j].Name)
})
return zs[0].Name, nil
}

View file

@ -7,22 +7,21 @@ Since = "v3.1.0"
Example = ''' Example = '''
LIQUID_WEB_USERNAME=someuser \ LIQUID_WEB_USERNAME=someuser \
LIQUID_WEB_PASSWORD=somepass \ LIQUID_WEB_PASSWORD=somepass \
LIQUID_WEB_ZONE=tacoman.com.net \
lego --email you@example.com --dns liquidweb --domains my.example.org run lego --email you@example.com --dns liquidweb --domains my.example.org run
''' '''
[Configuration] [Configuration]
[Configuration.Credentials] [Configuration.Credentials]
LIQUID_WEB_USERNAME = "Storm API Username" LIQUID_WEB_USERNAME = "Liquid Web API Username"
LIQUID_WEB_PASSWORD = "Storm API Password" LIQUID_WEB_PASSWORD = "Liquid Web API Password"
LIQUID_WEB_ZONE = "DNS Zone"
[Configuration.Additional] [Configuration.Additional]
LIQUID_WEB_URL = "Storm API endpoint" LIQUID_WEB_ZONE = "DNS Zone"
LIQUID_WEB_URL = "Liquid Web API endpoint"
LIQUID_WEB_TTL = "The TTL of the TXT record used for the DNS challenge" LIQUID_WEB_TTL = "The TTL of the TXT record used for the DNS challenge"
LIQUID_WEB_POLLING_INTERVAL = "Time between DNS propagation check" LIQUID_WEB_POLLING_INTERVAL = "Time between DNS propagation check"
LIQUID_WEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" LIQUID_WEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
LIQUID_WEB_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)" LIQUID_WEB_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)"
[Links] [Links]
API = "https://cart.liquidweb.com/storm/api/docs/v1/" API = "https://api.liquidweb.com/docs/"
GoClient = "https://github.com/liquidweb/liquidweb-go" GoClient = "https://github.com/liquidweb/liquidweb-go"

View file

@ -1,15 +1,11 @@
package liquidweb package liquidweb
import ( import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing" "testing"
"time" "time"
"github.com/go-acme/lego/v4/platform/tester" "github.com/go-acme/lego/v4/platform/tester"
"github.com/stretchr/testify/assert" "github.com/liquidweb/liquidweb-go/network"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -22,23 +18,20 @@ var envTest = tester.NewEnvTest(
EnvZone). EnvZone).
WithDomain(envDomain) WithDomain(envDomain)
func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { func setupTest(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider {
t.Helper() t.Helper()
mux := http.NewServeMux() serverURL := mockAPIServer(t, initRecs)
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
config := NewDefaultConfig() config := NewDefaultConfig()
config.Username = "blars" config.Username = "blars"
config.Password = "tacoman" config.Password = "tacoman"
config.BaseURL = server.URL config.BaseURL = serverURL
config.Zone = "tacoman.com"
provider, err := NewDNSProviderConfig(config) provider, err := NewDNSProviderConfig(config)
require.NoError(t, err) require.NoError(t, err)
return provider, mux return provider
} }
func TestNewDNSProvider(t *testing.T) { func TestNewDNSProvider(t *testing.T) {
@ -48,7 +41,14 @@ func TestNewDNSProvider(t *testing.T) {
expected string expected string
}{ }{
{ {
desc: "success", desc: "minimum-success",
envVars: map[string]string{
EnvUsername: "blars",
EnvPassword: "tacoman",
},
},
{
desc: "set-everything",
envVars: map[string]string{ envVars: map[string]string{
EnvURL: "https://storm.com", EnvURL: "https://storm.com",
EnvUsername: "blars", EnvUsername: "blars",
@ -59,7 +59,7 @@ func TestNewDNSProvider(t *testing.T) {
{ {
desc: "missing credentials", desc: "missing credentials",
envVars: map[string]string{}, envVars: map[string]string{},
expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME,LIQUID_WEB_PASSWORD,LIQUID_WEB_ZONE", expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME,LIQUID_WEB_PASSWORD",
}, },
{ {
desc: "missing username", desc: "missing username",
@ -74,14 +74,8 @@ func TestNewDNSProvider(t *testing.T) {
envVars: map[string]string{ envVars: map[string]string{
EnvUsername: "blars", EnvUsername: "blars",
EnvZone: "blars.com", EnvZone: "blars.com",
}, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD", },
}, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD",
{
desc: "missing zone",
envVars: map[string]string{
EnvUsername: "blars",
EnvPassword: "tacoman",
}, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_ZONE",
}, },
} }
@ -126,28 +120,21 @@ func TestNewDNSProviderConfig(t *testing.T) {
username: "", username: "",
password: "", password: "",
zone: "", zone: "",
expected: "liquidweb: zone is missing", expected: "liquidweb: could not create Liquid Web API client: provided username is empty",
}, },
{ {
desc: "missing username", desc: "missing username",
username: "", username: "",
password: "secret", password: "secret",
zone: "example.com", zone: "example.com",
expected: "liquidweb: username is missing", expected: "liquidweb: could not create Liquid Web API client: provided username is empty",
}, },
{ {
desc: "missing password", desc: "missing password",
username: "acme", username: "acme",
password: "", password: "",
zone: "example.com", zone: "example.com",
expected: "liquidweb: password is missing", expected: "liquidweb: could not create Liquid Web API client: provided password is empty",
},
{
desc: "missing zone",
username: "acme",
password: "secret",
zone: "",
expected: "liquidweb: zone is missing",
}, },
} }
@ -174,75 +161,102 @@ func TestNewDNSProviderConfig(t *testing.T) {
} }
func TestDNSProvider_Present(t *testing.T) { func TestDNSProvider_Present(t *testing.T) {
provider, mux := setupTest(t) provider := setupTest(t)
mux.HandleFunc("/v1/Network/DNS/Record/create", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
username, password, ok := r.BasicAuth()
assert.Equal(t, "blars", username)
assert.Equal(t, "tacoman", password)
assert.True(t, ok)
reqBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
expectedReqBody := `
{
"params": {
"name": "_acme-challenge.tacoman.com",
"rdata": "\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"",
"ttl": 300,
"type": "TXT",
"zone": "tacoman.com"
}
}`
assert.JSONEq(t, expectedReqBody, string(reqBody))
w.WriteHeader(http.StatusOK)
_, err = fmt.Fprintf(w, `{
"type": "TXT",
"name": "_acme-challenge.tacoman.com",
"rdata": "\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"",
"ttl": 300,
"id": 1234567,
"prio": null
}`)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
err := provider.Present("tacoman.com", "", "") err := provider.Present("tacoman.com", "", "")
require.NoError(t, err) require.NoError(t, err)
} }
func TestDNSProvider_CleanUp(t *testing.T) { func TestDNSProvider_CleanUp(t *testing.T) {
provider, mux := setupTest(t) provider := setupTest(t, network.DNSRecord{
Name: "_acme-challenge.tacoman.com",
mux.HandleFunc("/v1/Network/DNS/Record/delete", func(w http.ResponseWriter, r *http.Request) { RData: "123d==",
assert.Equal(t, http.MethodPost, r.Method) Type: "TXT",
TTL: 300,
username, password, ok := r.BasicAuth() ID: 1234567,
assert.Equal(t, "blars", username) ZoneID: 42,
assert.Equal(t, "tacoman", password)
assert.True(t, ok)
_, err := fmt.Fprintf(w, `{"deleted": "123"}`)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}) })
provider.recordIDs["123"] = 1234567 provider.recordIDs["123d=="] = 1234567
err := provider.CleanUp("tacoman.com.", "123", "") err := provider.CleanUp("tacoman.com.", "123d==", "")
require.NoError(t, err, "fail to remove TXT record") require.NoError(t, err)
}
func TestDNSProvider(t *testing.T) {
testCases := []struct {
desc string
initRecs []network.DNSRecord
domain string
token string
keyAuth string
present bool
expPresentErr string
cleanup bool
}{
{
desc: "expected successful",
domain: "tacoman.com",
token: "123",
keyAuth: "456",
present: true,
cleanup: true,
},
{
desc: "other successful",
domain: "banana.com",
token: "123",
keyAuth: "456",
present: true,
cleanup: true,
},
{
desc: "zone not on account",
domain: "huckleberry.com",
token: "123",
keyAuth: "456",
present: true,
expPresentErr: "no valid zone in account for certificate '_acme-challenge.huckleberry.com'",
cleanup: false,
},
{
desc: "ssl for domain",
domain: "sundae.cherry.com",
token: "5847953",
keyAuth: "34872934",
present: true,
cleanup: true,
},
{
desc: "complicated domain",
domain: "always.money.stand.banana.com",
token: "5847953",
keyAuth: "there is always money in the banana stand",
present: true,
cleanup: true,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
provider := setupTest(t, test.initRecs...)
if test.present {
err := provider.Present(test.domain, test.token, test.keyAuth)
if test.expPresentErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, test.expPresentErr)
}
}
if test.cleanup {
err := provider.CleanUp(test.domain, test.token, test.keyAuth)
require.NoError(t, err)
}
})
}
} }
func TestLivePresent(t *testing.T) { func TestLivePresent(t *testing.T) {

View file

@ -0,0 +1,305 @@
package liquidweb
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"testing"
"github.com/liquidweb/liquidweb-go/network"
"github.com/liquidweb/liquidweb-go/types"
)
func mockAPIServer(t *testing.T, initRecs []network.DNSRecord) string {
t.Helper()
recs := make(map[int]network.DNSRecord)
for _, rec := range initRecs {
recs[int(rec.ID)] = rec
}
mux := http.NewServeMux()
mux.Handle("/v1/Network/DNS/Record/delete", mockAPIDelete(recs))
mux.Handle("/v1/Network/DNS/Record/create", mockAPICreate(recs))
mux.Handle("/v1/Network/DNS/Zone/list", mockAPIListZones())
mux.Handle("/bleed/Network/DNS/Record/delete", mockAPIDelete(recs))
mux.Handle("/bleed/Network/DNS/Record/create", mockAPICreate(recs))
mux.Handle("/bleed/Network/DNS/Zone/list", mockAPIListZones())
server := httptest.NewServer(requireBasicAuth(requireJSON(mux)))
t.Cleanup(server.Close)
return server.URL
}
func requireBasicAuth(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if ok && username == "blars" && password == "tacoman" {
next.ServeHTTP(w, r)
return
}
http.Error(w, "invalid auth", http.StatusForbidden)
}
}
func requireJSON(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
buf := &bytes.Buffer{}
_, err := buf.ReadFrom(r.Body)
if err != nil {
http.Error(w, "malformed request - json required", http.StatusBadRequest)
return
}
r.Body = io.NopCloser(buf)
next.ServeHTTP(w, r)
}
}
func mockAPICreate(recs map[int]network.DNSRecord) http.HandlerFunc {
_, mockAPIServerZones := makeMockZones()
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "invalid request", http.StatusInternalServerError)
return
}
req := struct {
Params network.DNSRecord `json:"params"`
}{}
if err = json.Unmarshal(body, &req); err != nil {
http.Error(w, makeEncodingError(body), http.StatusBadRequest)
return
}
req.Params.ID = types.FlexInt(rand.Intn(10000000))
req.Params.ZoneID = types.FlexInt(mockAPIServerZones[req.Params.Name])
if _, exists := recs[int(req.Params.ID)]; exists {
http.Error(w, "dns record already exists", http.StatusTeapot)
return
}
recs[int(req.Params.ID)] = req.Params
resp, err := json.Marshal(req.Params)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
http.Error(w, string(resp), http.StatusOK)
}
}
func mockAPIDelete(recs map[int]network.DNSRecord) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "invalid request", http.StatusInternalServerError)
return
}
req := struct {
Params struct {
Name string `json:"name"`
ID int `json:"id"`
} `json:"params"`
}{}
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, makeEncodingError(body), http.StatusBadRequest)
return
}
if req.Params.ID == 0 {
http.Error(w, `{"error":"","error_class":"LW::Exception::Input::Multiple","errors":[{"error":"","error_class":"LW::Exception::Input::Required","field":"id","full_message":"The required field 'id' was missing a value.","position":null}],"field":["id"],"full_message":"The following input errors occurred:\nThe required field 'id' was missing a value.","type":null}`, http.StatusOK)
return
}
if _, ok := recs[req.Params.ID]; !ok {
http.Error(w, fmt.Sprintf(`{"error":"","error_class":"LW::Exception::RecordNotFound","field":"network_dns_rr","full_message":"Record 'network_dns_rr: %d' not found","input":"%d","public_message":null}`, req.Params.ID, req.Params.ID), http.StatusOK)
return
}
delete(recs, req.Params.ID)
http.Error(w, fmt.Sprintf("{\"deleted\":%d}", req.Params.ID), http.StatusOK)
}
}
func mockAPIListZones() http.HandlerFunc {
mockZones, mockAPIServerZones := makeMockZones()
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "invalid request", http.StatusInternalServerError)
return
}
req := struct {
Params struct {
PageNum int `json:"page_num"`
} `json:"params"`
}{}
if err = json.Unmarshal(body, &req); err != nil {
http.Error(w, makeEncodingError(body), http.StatusBadRequest)
return
}
switch {
case req.Params.PageNum < 1:
req.Params.PageNum = 1
case req.Params.PageNum > len(mockZones):
req.Params.PageNum = len(mockZones)
}
resp := mockZones[req.Params.PageNum]
resp.ItemTotal = types.FlexInt(len(mockAPIServerZones))
resp.PageNum = types.FlexInt(req.Params.PageNum)
resp.PageSize = 5
resp.PageTotal = types.FlexInt(len(mockZones))
var respBody []byte
if respBody, err = json.Marshal(resp); err == nil {
http.Error(w, string(respBody), http.StatusOK)
return
}
http.Error(w, "", http.StatusInternalServerError)
}
}
func makeEncodingError(buf []byte) string {
return fmt.Sprintf(`{"data":"%q","encoding":"JSON","error":"unexpected end of string while parsing JSON string, at character offset 32 (before \"(end of string)\") at /usr/local/lp/libs/perl/LW/Base/Role/Serializer.pm line 16.\n","error_class":"LW::Exception::Deserialize","full_message":"Could not deserialize \"%q\" from JSON: unexpected end of string while parsing JSON string, at character offset 32 (before \"(end of string)\") at /usr/local/lp/libs/perl/LW/Base/Role/Serializer.pm line 16.\n"}⏎`, string(buf), string(buf))
}
func makeMockZones() (map[int]network.DNSZoneList, map[string]int) {
mockZones := map[int]network.DNSZoneList{
1: {
Items: []network.DNSZone{
{
ID: 1,
Name: "blars.com",
Active: 1,
DelegationStatus: "CORRECT",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 2,
Name: "tacoman.com",
Active: 1,
DelegationStatus: "CORRECT",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 3,
Name: "storm.com",
Active: 1,
DelegationStatus: "CORRECT",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 4,
Name: "not-apple.com",
Active: 1,
DelegationStatus: "BAD_NAMESERVERS",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 5,
Name: "example.com",
Active: 1,
DelegationStatus: "BAD_NAMESERVERS",
PrimaryNameserver: "ns.liquidweb.com",
},
},
},
2: {
Items: []network.DNSZone{
{
ID: 6,
Name: "banana.com",
Active: 1,
DelegationStatus: "NXDOMAIN",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 7,
Name: "cherry.com",
Active: 1,
DelegationStatus: "SERVFAIL",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 8,
Name: "dates.com",
Active: 1,
DelegationStatus: "SERVFAIL",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 9,
Name: "eggplant.com",
Active: 1,
DelegationStatus: "SERVFAIL",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 10,
Name: "fig.com",
Active: 1,
DelegationStatus: "UNKNOWN",
PrimaryNameserver: "ns.liquidweb.com",
},
},
},
3: {
Items: []network.DNSZone{
{
ID: 11,
Name: "grapes.com",
Active: 1,
DelegationStatus: "UNKNOWN",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 12,
Name: "money.banana.com",
Active: 1,
DelegationStatus: "UNKNOWN",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 13,
Name: "money.stand.banana.com",
Active: 1,
DelegationStatus: "UNKNOWN",
PrimaryNameserver: "ns.liquidweb.com",
},
{
ID: 14,
Name: "stand.banana.com",
Active: 1,
DelegationStatus: "UNKNOWN",
PrimaryNameserver: "ns.liquidweb.com",
},
},
},
}
mockAPIServerZones := make(map[string]int)
for _, page := range mockZones {
for _, zone := range page.Items {
mockAPIServerZones[zone.Name] = int(zone.ID)
}
}
return mockZones, mockAPIServerZones
}