Skip to content

Commit

Permalink
Merge branch 'directadmin' of https://github.com/jwklijnsma/lego into…
Browse files Browse the repository at this point in the history
… directadmin
  • Loading branch information
jwklijnsma committed Jul 13, 2024
2 parents 69c7cca + 181e84c commit eda27df
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 116 deletions.
18 changes: 0 additions & 18 deletions cmd/zz_gen_cmd_dnshelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -732,24 +732,6 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/digitalocean`)

case "directadmin":
// generated from: providers/dns/directadmin/directadmin.toml
ew.writeln(`Configuration for directadmin.`)
ew.writeln(`Code: 'directadmin'`)
ew.writeln(`Since: ''`)
ew.writeln()

ew.writeln(`Credentials:`)
ew.writeln(` - "DIRECTADMIN_PASSWORD": yourpassword`)
ew.writeln(` - "DIRECTADMIN_USERNAME": yourusername`)
ew.writeln()

ew.writeln(`Additional Configuration:`)
ew.writeln(` - "DIRECTADMIN_API_URL": The URL of the API`)

ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/directadmin`)

case "dnshomede":
// generated from: providers/dns/dnshomede/dnshomede.toml
ew.writeln(`Configuration for dnsHome.de.`)
Expand Down
8 changes: 4 additions & 4 deletions docs/content/dns/zz_gen_directadmin.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Here is an example bash command using the directadmin provider:

```bash
DIRECTADMIN_API_URL="https://url:2222" \
DIRECTADMIN_USERNAME="yourusername" \
DIRECTADMIN_PASSWORD="yourpassword" \
DIRECTADMIN_USERNAME=xxxx \
DIRECTADMIN_PASSWORD=yyy \
lego --email you@example.com --dns directadmin -d "my.example.org" -d "*.example.org" run
```

Expand All @@ -38,8 +38,8 @@ lego --email you@example.com --dns directadmin -d "my.example.org" -d "*.example

| Environment Variable Name | Description |
|-----------------------|-------------|
| `DIRECTADMIN_PASSWORD` | yourpassword |
| `DIRECTADMIN_USERNAME` | yourusername |
| `DIRECTADMIN_PASSWORD` | API password |
| `DIRECTADMIN_USERNAME` | API username |

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" >}}).
Expand Down
139 changes: 77 additions & 62 deletions providers/dns/directadmin/directadmin.go
Original file line number Diff line number Diff line change
@@ -1,99 +1,114 @@
package directadmin

import (
"context"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"

"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/providers/dns/directadmin/internal"
)

// DNSProvider implements the challenge.Provider interface
type DNSProvider struct {
apiURL string
username string
password string
}
// Environment variables names.
const (
envNamespace = "DIRECTADMIN_"

// NewDNSProvider creates a new DNSProvider instance
func NewDNSProvider() (*DNSProvider, error) {
apiURL := getEnv("DIRECTADMIN_API_URL", "https://api.directadmin.com")
username := getEnv("DIRECTADMIN_USERNAME", "default_username")
password := getEnv("DIRECTADMIN_PASSWORD", "default_password")

return &DNSProvider{
apiURL: apiURL,
username: username,
password: password,
}, nil
EnvAPIURL = envNamespace + "API_URL"
EnvUsername = envNamespace + "USERNAME"
EnvPassword = envNamespace + "PASSWORD"

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 {
BaseURL string
Username string
Password string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}

// getEnv reads an environment variable and returns the value or a default value if the environment variable is not set.
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
BaseURL: env.GetOrDefaultString(EnvAPIURL, internal.DefaultBaseURL),
TTL: env.GetOrDefaultInt(EnvTTL, 30),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
}
return defaultValue
}

// Present creates a TXT record to fulfill the DNS-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)

url := fmt.Sprintf("%s/CMD_API_DNS_CONTROL?domain=%s&json=yes", d.apiURL, domain)
data := fmt.Sprintf("action=add&type=TXT&name=_acme-challenge&value=%s", info.Value)
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
client *internal.Client
}

req, err := http.NewRequest("POST", url, strings.NewReader(data))
// NewDNSProvider returns a DNSProvider instance configured for DirectAdmin.
// Credentials must be passed in the environment variables:
// DIRECTADMIN_API_URL, DIRECTADMIN_USERNAME, DIRECTADMIN_PASSWORD.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvUsername, EnvPassword)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
return nil, fmt.Errorf("directadmin: %w", err)
}

req.SetBasicAuth(d.username, d.password)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
config := NewDefaultConfig()
config.Username = values[EnvUsername]
config.Password = values[EnvPassword]

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
return NewDNSProviderConfig(config)
}

// NewDNSProviderConfig return a DNSProvider instance configured for DirectAdmin.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config.BaseURL == "" {
return nil, errors.New("directadmin: missing API URL")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
if config.Username == "" || config.Password == "" {
return nil, errors.New("directadmin: some credentials information are missing")
}

log.Infof("Presented TXT record for domain %s", domain)
return nil
client, err := internal.NewClient(config.BaseURL, config.Username, config.Password)
if err != nil {
return nil, fmt.Errorf("directadmin: %w", err)
}

return &DNSProvider{client: client}, nil
}

// CleanUp removes the TXT record created for the DNS-01 challenge
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)

url := fmt.Sprintf("%s/CMD_API_DNS_CONTROL?domain=%s&json=yes", d.apiURL, domain)
data := fmt.Sprintf("action=delete&type=TXT&name=_acme-challenge&value=%s", info.Value)

req, err := http.NewRequest("POST", url, strings.NewReader(data))
err := d.client.SetRecord(context.Background(), info.EffectiveFQDN, info.Value)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
return fmt.Errorf("directadmin: %w", err)
}

req.SetBasicAuth(d.username, d.password)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
return nil
}

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
err := d.client.DeleteRecord(context.Background(), info.EffectiveFQDN, info.Value)
if err != nil {
return fmt.Errorf("directadmin: %w", err)
}

log.Infof("Cleaned up TXT record for domain %s", domain)
return nil
}
10 changes: 5 additions & 5 deletions providers/dns/directadmin/directadmin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ Code = "directadmin"

Example = '''
DIRECTADMIN_API_URL="https://url:2222" \
DIRECTADMIN_USERNAME="yourusername" \
DIRECTADMIN_PASSWORD="yourpassword" \
DIRECTADMIN_USERNAME=xxxx \
DIRECTADMIN_PASSWORD=yyy \
lego --email you@example.com --dns directadmin -d "my.example.org" -d "*.example.org" run
'''

[Configuration]
[Configuration.Credentials]
DIRECTADMIN_USERNAME = "yourusername"
DIRECTADMIN_PASSWORD = "yourpassword"
DIRECTADMIN_USERNAME = "API username"
DIRECTADMIN_PASSWORD = "API password"

[Configuration.Additional]
DIRECTADMIN_API_URL = "The URL of the API"

[Links]
API = "https://www.directadmin.com/api.php"
API = "https://www.directadmin.com/api.php"
55 changes: 31 additions & 24 deletions providers/dns/directadmin/directadmin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ import (
"github.com/stretchr/testify/require"
)

const (
envNamespace = "DIRECTADMIN_"
envAPIURL = envNamespace + "API_URL"
envUsername = envNamespace + "USERNAME"
envPassword = envNamespace + "PASSWORD"
envDomain = envNamespace + "DOMAIN"
)
const envDomain = envNamespace + "DOMAIN"

var envTest = tester.NewEnvTest(envAPIURL, envUsername, envPassword).WithDomain(envDomain)
var envTest = tester.NewEnvTest(EnvAPIURL, EnvUsername, EnvPassword).WithDomain(envDomain)

func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
Expand All @@ -26,15 +20,28 @@ func TestNewDNSProvider(t *testing.T) {
{
desc: "success",
envVars: map[string]string{
envAPIURL: "https://api.directadmin.com",
envUsername: "username",
envPassword: "password",
EnvUsername: "test",
EnvPassword: "secret",
},
},
{
desc: "missing API URL",
desc: "missing credentials",
envVars: map[string]string{},
expected: "directadmin: some credentials information are missing: DIRECTADMIN_API_URL, DIRECTADMIN_USERNAME, DIRECTADMIN_PASSWORD",
expected: "directadmin: some credentials information are missing: DIRECTADMIN_USERNAME,DIRECTADMIN_PASSWORD",
},
{
desc: "missing username",
envVars: map[string]string{
EnvPassword: "secret",
},
expected: "directadmin: some credentials information are missing: DIRECTADMIN_USERNAME",
},
{
desc: "missing password",
envVars: map[string]string{
EnvUsername: "test",
},
expected: "directadmin: some credentials information are missing: DIRECTADMIN_PASSWORD",
},
}

Expand All @@ -60,38 +67,38 @@ func TestNewDNSProvider(t *testing.T) {
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
apiURL string
baseURL string
username string
password string
expected string
}{
{
desc: "success",
apiURL: "https://api.directadmin.com",
username: "username",
password: "password",
baseURL: "https://api.directadmin.com",
username: "test",
password: "secret",
},
{
desc: "missing API URL",
expected: "directadmin: APIURL is missing",
expected: "directadmin: missing API URL",
},
{
desc: "missing username",
apiURL: "https://api.directadmin.com",
expected: "directadmin: username is missing",
baseURL: "https://api.directadmin.com",
expected: "directadmin: some credentials information are missing",
},
{
desc: "missing password",
apiURL: "https://api.directadmin.com",
username: "username",
expected: "directadmin: password is missing",
baseURL: "https://api.directadmin.com",
username: "test",
expected: "directadmin: some credentials information are missing",
},
}

for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.APIURL = test.apiURL
config.BaseURL = test.baseURL
config.Username = test.username
config.Password = test.password

Expand Down
Loading

0 comments on commit eda27df

Please sign in to comment.