mirror of
https://github.com/go-acme/lego.git
synced 2026-01-25 05:06:16 +00:00
acme-dns: HTTP storage (#2393)
This commit is contained in:
committed by
GitHub
parent
b83c1d5f64
commit
c2b88e19da
@@ -165,9 +165,6 @@ issues:
|
||||
text: 'Error return value of `fmt.Fprintln` is not checked'
|
||||
linters:
|
||||
- errcheck
|
||||
- path: providers/dns/dns_providers.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: certcrypto/crypto.go
|
||||
text: '(tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable'
|
||||
linters:
|
||||
@@ -220,7 +217,7 @@ issues:
|
||||
text: 'testCases is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: providers/dns/acmedns/acmedns_test.go
|
||||
- path: providers/dns/acmedns/mock_test.go
|
||||
text: 'egTestAccount is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
@@ -228,10 +225,6 @@ issues:
|
||||
text: 'memcachedHosts is a global variable'
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: cmd/zz_gen_cmd_dnshelp.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- funlen
|
||||
- path: providers/dns/checkdomain/internal/types.go
|
||||
text: '`payed` is a misspelling of `paid`'
|
||||
linters:
|
||||
@@ -259,10 +252,6 @@ issues:
|
||||
text: 'cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high'
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: providers/dns/servercow/internal/types.go
|
||||
text: 'the methods of "Value" use pointer receiver and non-pointer receiver.'
|
||||
linters:
|
||||
- recvcheck
|
||||
|
||||
# Those elements have been replaced by non-exposed structures.
|
||||
- path: providers/dns/linode/linode_test.go
|
||||
|
||||
@@ -176,6 +176,7 @@ func displayDNSHelp(w io.Writer, name string) error {
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "ACME_DNS_API_BASE": The ACME-DNS API address`)
|
||||
ew.writeln(` - "ACME_DNS_STORAGE_BASE_URL": The ACME-DNS JSON account data server.`)
|
||||
ew.writeln(` - "ACME_DNS_STORAGE_PATH": The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates.`)
|
||||
ew.writeln()
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ Here is an example bash command using the Joohoi's ACME-DNS provider:
|
||||
ACME_DNS_API_BASE=http://10.0.0.8:4443 \
|
||||
ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \
|
||||
lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run
|
||||
|
||||
# or
|
||||
|
||||
ACME_DNS_API_BASE=http://10.0.0.8:4443 \
|
||||
ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \
|
||||
lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run
|
||||
```
|
||||
|
||||
|
||||
@@ -39,6 +45,7 @@ lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `ACME_DNS_API_BASE` | The ACME-DNS API address |
|
||||
| `ACME_DNS_STORAGE_BASE_URL` | The ACME-DNS JSON account data server. |
|
||||
| `ACME_DNS_STORAGE_PATH` | The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates. |
|
||||
|
||||
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
|
||||
@@ -52,7 +59,7 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}).
|
||||
## More information
|
||||
|
||||
- [API documentation](https://github.com/joohoi/acme-dns#api)
|
||||
- [Go client](https://github.com/cpu/goacmedns)
|
||||
- [Go client](https://github.com/nrdcg/goacmedns)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/acmedns/acmedns.toml -->
|
||||
|
||||
2
go.mod
2
go.mod
@@ -27,7 +27,6 @@ require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/civo/civogo v0.3.11
|
||||
github.com/cloudflare/cloudflare-go v0.112.0
|
||||
github.com/cpu/goacmedns v0.1.1
|
||||
github.com/dnsimple/dnsimple-go v1.7.0
|
||||
github.com/exoscale/egoscale/v3 v3.1.7
|
||||
github.com/go-jose/go-jose/v4 v4.0.4
|
||||
@@ -52,6 +51,7 @@ require (
|
||||
github.com/nrdcg/desec v0.10.0
|
||||
github.com/nrdcg/dnspod-go v0.4.0
|
||||
github.com/nrdcg/freemyip v0.3.0
|
||||
github.com/nrdcg/goacmedns v0.2.0
|
||||
github.com/nrdcg/goinwx v0.10.0
|
||||
github.com/nrdcg/mailinabox v0.2.0
|
||||
github.com/nrdcg/namesilo v0.2.1
|
||||
|
||||
4
go.sum
4
go.sum
@@ -209,8 +209,6 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpu/goacmedns v0.1.1 h1:DM3H2NiN2oam7QljgGY5ygy4yDXhK5Z4JUnqaugs2C4=
|
||||
github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
@@ -638,6 +636,8 @@ github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U
|
||||
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
|
||||
github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc=
|
||||
github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM=
|
||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||
github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM=
|
||||
github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4=
|
||||
github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cpu/goacmedns"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"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/acmedns/internal"
|
||||
"github.com/nrdcg/goacmedns"
|
||||
"github.com/nrdcg/goacmedns/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,9 +22,14 @@ const (
|
||||
// EnvAPIBase is the environment variable name for the ACME-DNS API address.
|
||||
// (e.g. https://acmedns.your-domain.com).
|
||||
EnvAPIBase = envNamespace + "API_BASE"
|
||||
|
||||
// EnvStoragePath is the environment variable name for the ACME-DNS JSON account data file.
|
||||
// A per-domain account will be registered/persisted to this file and used for TXT updates.
|
||||
EnvStoragePath = envNamespace + "STORAGE_PATH"
|
||||
|
||||
// EnvStorageBaseURL is the environment variable name for the ACME-DNS JSON account data.
|
||||
// The URL to the storage server.
|
||||
EnvStorageBaseURL = envNamespace + "STORAGE_BASE_URL"
|
||||
)
|
||||
|
||||
var _ challenge.Provider = (*DNSProvider)(nil)
|
||||
@@ -31,10 +39,10 @@ var _ challenge.Provider = (*DNSProvider)(nil)
|
||||
type acmeDNSClient interface {
|
||||
// UpdateTXTRecord updates the provided account's TXT record
|
||||
// to the given value or returns an error.
|
||||
UpdateTXTRecord(account goacmedns.Account, value string) error
|
||||
UpdateTXTRecord(ctx context.Context, account goacmedns.Account, value string) error
|
||||
// RegisterAccount registers and returns a new account
|
||||
// with the given allowFrom restriction or returns an error.
|
||||
RegisterAccount(allowFrom []string) (goacmedns.Account, error)
|
||||
RegisterAccount(ctx context.Context, allowFrom []string) (goacmedns.Account, error)
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
@@ -46,17 +54,41 @@ type DNSProvider struct {
|
||||
// NewDNSProvider creates an ACME-DNS provider using file based account storage.
|
||||
// Its configuration is loaded from the environment by reading EnvAPIBase and EnvStoragePath.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvAPIBase, EnvStoragePath)
|
||||
values, err := env.Get(EnvAPIBase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme-dns: %w", err)
|
||||
}
|
||||
|
||||
client := goacmedns.NewClient(values[EnvAPIBase])
|
||||
storage := goacmedns.NewFileStorage(values[EnvStoragePath], 0o600)
|
||||
return NewDNSProviderClient(client, storage)
|
||||
storagePath := env.GetOrFile(EnvStoragePath)
|
||||
storageBaseURL := env.GetOrFile(EnvStorageBaseURL)
|
||||
|
||||
if storagePath == "" && storageBaseURL == "" {
|
||||
return nil, fmt.Errorf("acme-dns: %s or %s environment variables not set", EnvStoragePath, EnvStorageBaseURL)
|
||||
}
|
||||
|
||||
if storagePath != "" && storageBaseURL != "" {
|
||||
return nil, fmt.Errorf("acme-dns: %s or %s environment variables cannot be used at the same time", EnvStoragePath, EnvStorageBaseURL)
|
||||
}
|
||||
|
||||
var st goacmedns.Storage
|
||||
if storagePath != "" {
|
||||
st = storage.NewFile(values[EnvStoragePath], 0o600)
|
||||
} else {
|
||||
st, err = internal.NewHTTPStorage(storageBaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme-dns: new HTTP storage: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
client, err := goacmedns.NewClient(values[EnvAPIBase])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme-dns: %w", err)
|
||||
}
|
||||
|
||||
return NewDNSProviderClient(client, st)
|
||||
}
|
||||
|
||||
// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and goacmedns.Storage.
|
||||
// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and [goacmedns.Storage].
|
||||
func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNSProvider, error) {
|
||||
if client == nil {
|
||||
return nil, errors.New("ACME-DNS Client must be not nil")
|
||||
@@ -105,16 +137,18 @@ func (e ErrCNAMERequired) Error() string {
|
||||
// one will be created and registered with the ACME DNS server and an ErrCNAMERequired error is returned.
|
||||
// This will halt issuance and indicate to the user that a one-time manual setup is required for the domain.
|
||||
func (d *DNSProvider) Present(domain, _, keyAuth string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Compute the challenge response FQDN and TXT value for the domain based on the keyAuth.
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
|
||||
// Check if credentials were previously saved for this domain.
|
||||
account, err := d.storage.Fetch(domain)
|
||||
account, err := d.storage.Fetch(ctx, domain)
|
||||
if err != nil {
|
||||
if errors.Is(err, goacmedns.ErrDomainNotFound) {
|
||||
if errors.Is(err, storage.ErrDomainNotFound) {
|
||||
// The account did not exist.
|
||||
// Create a new one and return an error indicating the required one-time manual CNAME setup.
|
||||
return d.register(domain, info.FQDN)
|
||||
return d.register(ctx, domain, info.FQDN)
|
||||
}
|
||||
|
||||
// Errors other than goacmedns.ErrDomainNotFound are unexpected.
|
||||
@@ -122,7 +156,7 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error {
|
||||
}
|
||||
|
||||
// Update the acme-dns TXT record.
|
||||
return d.client.UpdateTXTRecord(account, info.Value)
|
||||
return d.client.UpdateTXTRecord(ctx, account, info.Value)
|
||||
}
|
||||
|
||||
// CleanUp removes the record matching the specified parameters. It is not
|
||||
@@ -137,19 +171,19 @@ func (d *DNSProvider) CleanUp(_, _, _ string) error {
|
||||
// If account creation works as expected a ErrCNAMERequired error is returned describing
|
||||
// the one-time manual CNAME setup required to complete setup of the ACME-DNS hook for the domain.
|
||||
// If any other error occurs it is returned as-is.
|
||||
func (d *DNSProvider) register(domain, fqdn string) error {
|
||||
func (d *DNSProvider) register(ctx context.Context, domain, fqdn string) error {
|
||||
// TODO(@cpu): Read CIDR whitelists from the environment
|
||||
newAcct, err := d.client.RegisterAccount(nil)
|
||||
newAcct, err := d.client.RegisterAccount(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store the new account in the storage and call save to persist the data.
|
||||
err = d.storage.Put(domain, newAcct)
|
||||
err = d.storage.Put(ctx, domain, newAcct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.storage.Save()
|
||||
err = d.storage.Save(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,13 +9,20 @@ Example = '''
|
||||
ACME_DNS_API_BASE=http://10.0.0.8:4443 \
|
||||
ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \
|
||||
lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run
|
||||
|
||||
# or
|
||||
|
||||
ACME_DNS_API_BASE=http://10.0.0.8:4443 \
|
||||
ACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \
|
||||
lego --email you@example.com --dns "acme-dns" -d '*.example.com' -d example.com run
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
ACME_DNS_API_BASE = "The ACME-DNS API address"
|
||||
ACME_DNS_STORAGE_PATH = "The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates."
|
||||
ACME_DNS_STORAGE_BASE_URL = "The ACME-DNS JSON account data server."
|
||||
|
||||
[Links]
|
||||
API = "https://github.com/joohoi/acme-dns#api"
|
||||
GoClient = "https://github.com/cpu/goacmedns"
|
||||
GoClient = "https://github.com/nrdcg/goacmedns"
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cpu/goacmedns"
|
||||
"github.com/nrdcg/goacmedns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
// errorClientErr is used by the Client mocks that return an error.
|
||||
errorClientErr = errors.New("errorClient always errors")
|
||||
// errorStorageErr is used by the Storage mocks that return an error.
|
||||
errorStorageErr = errors.New("errorStorage always errors")
|
||||
)
|
||||
|
||||
const (
|
||||
// Fixed test data for unit tests.
|
||||
egDomain = "example.com"
|
||||
@@ -23,133 +16,6 @@ const (
|
||||
egKeyAuth = "⚷"
|
||||
)
|
||||
|
||||
var egTestAccount = goacmedns.Account{
|
||||
FullDomain: "acme-dns." + egDomain,
|
||||
SubDomain: "random-looking-junk." + egDomain,
|
||||
Username: "spooky.mulder",
|
||||
Password: "trustno1",
|
||||
}
|
||||
|
||||
// mockClient is a mock implementing the acmeDNSClient interface that always
|
||||
// returns a fixed goacmedns.Account from calls to Register.
|
||||
type mockClient struct {
|
||||
mockAccount goacmedns.Account
|
||||
}
|
||||
|
||||
// UpdateTXTRecord does nothing.
|
||||
func (c mockClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterAccount returns c.mockAccount and no errors.
|
||||
func (c mockClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
|
||||
return c.mockAccount, nil
|
||||
}
|
||||
|
||||
// mockUpdateClient is a mock implementing the acmeDNSClient interface that
|
||||
// tracks the calls to UpdateTXTRecord in the records map.
|
||||
type mockUpdateClient struct {
|
||||
mockClient
|
||||
records map[goacmedns.Account]string
|
||||
}
|
||||
|
||||
// UpdateTXTRecord saves a record value to c.records for the given acct.
|
||||
func (c mockUpdateClient) UpdateTXTRecord(acct goacmedns.Account, value string) error {
|
||||
c.records[acct] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// errorUpdateClient is a mock implementing the acmeDNSClient interface that always
|
||||
// returns errors from errorUpdateClient.
|
||||
type errorUpdateClient struct {
|
||||
mockClient
|
||||
}
|
||||
|
||||
// UpdateTXTRecord always returns an error.
|
||||
func (c errorUpdateClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
|
||||
return errorClientErr
|
||||
}
|
||||
|
||||
// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
|
||||
// returns errors from RegisterAccount.
|
||||
type errorRegisterClient struct {
|
||||
mockClient
|
||||
}
|
||||
|
||||
// RegisterAccount always returns an error.
|
||||
func (c errorRegisterClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
|
||||
return goacmedns.Account{}, errorClientErr
|
||||
}
|
||||
|
||||
// mockStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// returns static account data and ignores Save.
|
||||
type mockStorage struct {
|
||||
accounts map[string]goacmedns.Account
|
||||
}
|
||||
|
||||
// Save does nothing.
|
||||
func (m mockStorage) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put stores an account for the given domain in m.accounts.
|
||||
func (m mockStorage) Put(domain string, acct goacmedns.Account) error {
|
||||
m.accounts[domain] = acct
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch retrieves an account for the given domain from m.accounts or returns
|
||||
// goacmedns.ErrDomainNotFound.
|
||||
func (m mockStorage) Fetch(domain string) (goacmedns.Account, error) {
|
||||
if acct, ok := m.accounts[domain]; ok {
|
||||
return acct, nil
|
||||
}
|
||||
return goacmedns.Account{}, goacmedns.ErrDomainNotFound
|
||||
}
|
||||
|
||||
// FetchAll returns all of m.accounts.
|
||||
func (m mockStorage) FetchAll() map[string]goacmedns.Account {
|
||||
return m.accounts
|
||||
}
|
||||
|
||||
// errorPutStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// always returns errors from Put.
|
||||
type errorPutStorage struct {
|
||||
mockStorage
|
||||
}
|
||||
|
||||
// Put always errors.
|
||||
func (e errorPutStorage) Put(_ string, _ goacmedns.Account) error {
|
||||
return errorStorageErr
|
||||
}
|
||||
|
||||
// errorSaveStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// always returns errors from Save.
|
||||
type errorSaveStorage struct {
|
||||
mockStorage
|
||||
}
|
||||
|
||||
// Save always errors.
|
||||
func (e errorSaveStorage) Save() error {
|
||||
return errorStorageErr
|
||||
}
|
||||
|
||||
// errorFetchStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// always returns errors from Fetch.
|
||||
type errorFetchStorage struct {
|
||||
mockStorage
|
||||
}
|
||||
|
||||
// Fetch always errors.
|
||||
func (e errorFetchStorage) Fetch(_ string) (goacmedns.Account, error) {
|
||||
return goacmedns.Account{}, errorStorageErr
|
||||
}
|
||||
|
||||
// FetchAll is a nop for errorFetchStorage.
|
||||
func (e errorFetchStorage) FetchAll() map[string]goacmedns.Account {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestPresent tests that the ACME-DNS Present function for updating a DNS-01
|
||||
// challenge response TXT record works as expected.
|
||||
func TestPresent(t *testing.T) {
|
||||
@@ -277,7 +143,7 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
|
||||
// Call register for the example domain/fqdn.
|
||||
err = dp.register(egDomain, egFQDN)
|
||||
err = dp.register(context.Background(), egDomain, egFQDN)
|
||||
if test.ExpectedError != nil {
|
||||
assert.Equal(t, test.ExpectedError, err)
|
||||
} else {
|
||||
|
||||
3
providers/dns/acmedns/internal/fixtures/error.json
Normal file
3
providers/dns/acmedns/internal/fixtures/error.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"message": "There is an error"
|
||||
}
|
||||
16
providers/dns/acmedns/internal/fixtures/fetch-all.json
Normal file
16
providers/dns/acmedns/internal/fixtures/fetch-all.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"a": {
|
||||
"fulldomain": "foo.example.com",
|
||||
"subdomain": "foo",
|
||||
"username": "user",
|
||||
"password": "secret",
|
||||
"server_url": "https://example.com"
|
||||
},
|
||||
"b": {
|
||||
"fulldomain": "bar.example.com",
|
||||
"subdomain": "bar",
|
||||
"username": "user",
|
||||
"password": "secret",
|
||||
"server_url": "https://example.com"
|
||||
}
|
||||
}
|
||||
7
providers/dns/acmedns/internal/fixtures/fetch.json
Normal file
7
providers/dns/acmedns/internal/fixtures/fetch.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"fulldomain": "foo.example.com",
|
||||
"subdomain": "foo",
|
||||
"username": "user",
|
||||
"password": "secret",
|
||||
"server_url": "https://example.com"
|
||||
}
|
||||
139
providers/dns/acmedns/internal/http_storage.go
Normal file
139
providers/dns/acmedns/internal/http_storage.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
||||
"github.com/nrdcg/goacmedns"
|
||||
"github.com/nrdcg/goacmedns/storage"
|
||||
)
|
||||
|
||||
var _ goacmedns.Storage = (*HTTPStorage)(nil)
|
||||
|
||||
// HTTPStorage is an implementation of [acmedns.Storage] over HTTP.
|
||||
type HTTPStorage struct {
|
||||
client *http.Client
|
||||
baseURL *url.URL
|
||||
}
|
||||
|
||||
// NewHTTPStorage created a new [HTTPStorage].
|
||||
func NewHTTPStorage(baseURL string) (*HTTPStorage, error) {
|
||||
endpoint, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HTTPStorage{
|
||||
client: &http.Client{Timeout: 2 * time.Minute},
|
||||
baseURL: endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *HTTPStorage) Save(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HTTPStorage) Put(ctx context.Context, domain string, account goacmedns.Account) error {
|
||||
req, err := newJSONRequest(ctx, http.MethodPost, s.baseURL.JoinPath(domain), account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
return s.do(req, nil)
|
||||
}
|
||||
|
||||
func (s *HTTPStorage) Fetch(ctx context.Context, domain string) (goacmedns.Account, error) {
|
||||
req, err := newJSONRequest(ctx, http.MethodGet, s.baseURL.JoinPath(domain), nil)
|
||||
if err != nil {
|
||||
return goacmedns.Account{}, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
var account goacmedns.Account
|
||||
|
||||
err = s.do(req, &account)
|
||||
if err != nil {
|
||||
return goacmedns.Account{}, err
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s *HTTPStorage) FetchAll(ctx context.Context) (map[string]goacmedns.Account, error) {
|
||||
req, err := newJSONRequest(ctx, http.MethodGet, s.baseURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mapping map[string]goacmedns.Account
|
||||
|
||||
err = s.do(req, &mapping)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapping, nil
|
||||
}
|
||||
|
||||
func (s *HTTPStorage) do(req *http.Request, result any) error {
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return errutils.NewHTTPDoError(req, err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return storage.ErrDomainNotFound
|
||||
}
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return errutils.NewUnexpectedResponseStatusCodeError(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
|
||||
}
|
||||
139
providers/dns/acmedns/internal/http_storage_test.go
Normal file
139
providers/dns/acmedns/internal/http_storage_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/nrdcg/goacmedns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTest(t *testing.T, pattern, filename string, statusCode int) *HTTPStorage {
|
||||
t.Helper()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||
if filename == "" {
|
||||
rw.WriteHeader(statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Open(filepath.Join("fixtures", filename))
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
rw.WriteHeader(statusCode)
|
||||
_, err = io.Copy(rw, file)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
storage, err := NewHTTPStorage(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
storage.client = server.Client()
|
||||
|
||||
return storage
|
||||
}
|
||||
|
||||
func TestHTTPStorage_Fetch(t *testing.T) {
|
||||
storage := setupTest(t, "GET /example.com", "fetch.json", http.StatusOK)
|
||||
|
||||
account, err := storage.Fetch(context.Background(), "example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := goacmedns.Account{
|
||||
FullDomain: "foo.example.com",
|
||||
SubDomain: "foo",
|
||||
Username: "user",
|
||||
Password: "secret",
|
||||
ServerURL: "https://example.com",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, account)
|
||||
}
|
||||
|
||||
func TestHTTPStorage_Fetch_error(t *testing.T) {
|
||||
storage := setupTest(t, "GET /example.com", "error.json", http.StatusInternalServerError)
|
||||
|
||||
_, err := storage.Fetch(context.Background(), "example.com")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPStorage_FetchAll(t *testing.T) {
|
||||
storage := setupTest(t, "GET /", "fetch-all.json", http.StatusOK)
|
||||
|
||||
account, err := storage.FetchAll(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]goacmedns.Account{
|
||||
"a": {
|
||||
FullDomain: "foo.example.com",
|
||||
SubDomain: "foo",
|
||||
Username: "user",
|
||||
Password: "secret",
|
||||
ServerURL: "https://example.com",
|
||||
},
|
||||
"b": {
|
||||
FullDomain: "bar.example.com",
|
||||
SubDomain: "bar",
|
||||
Username: "user",
|
||||
Password: "secret",
|
||||
ServerURL: "https://example.com",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, account)
|
||||
}
|
||||
|
||||
func TestHTTPStorage_FetchAll_error(t *testing.T) {
|
||||
storage := setupTest(t, "GET /", "error.json", http.StatusInternalServerError)
|
||||
|
||||
_, err := storage.FetchAll(context.Background())
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPStorage_Put(t *testing.T) {
|
||||
storage := setupTest(t, "POST /example.com", "", http.StatusOK)
|
||||
|
||||
account := goacmedns.Account{
|
||||
FullDomain: "foo.example.com",
|
||||
SubDomain: "foo",
|
||||
Username: "user",
|
||||
Password: "secret",
|
||||
ServerURL: "https://example.com",
|
||||
}
|
||||
|
||||
err := storage.Put(context.Background(), "example.com", account)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPStorage_Put_error(t *testing.T) {
|
||||
storage := setupTest(t, "POST /example.com", "error.json", http.StatusInternalServerError)
|
||||
|
||||
account := goacmedns.Account{
|
||||
FullDomain: "foo.example.com",
|
||||
SubDomain: "foo",
|
||||
Username: "user",
|
||||
Password: "secret",
|
||||
ServerURL: "https://example.com",
|
||||
}
|
||||
|
||||
err := storage.Put(context.Background(), "example.com", account)
|
||||
require.Error(t, err)
|
||||
}
|
||||
70
providers/dns/acmedns/internal/readme.md
Normal file
70
providers/dns/acmedns/internal/readme.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# HTTP Storage
|
||||
|
||||
## Fetch
|
||||
|
||||
### Request
|
||||
|
||||
Endpoint: `GET <BaseURL>/<domain>`
|
||||
|
||||
### Response
|
||||
|
||||
Response status code 200.
|
||||
|
||||
Response body (account):
|
||||
|
||||
```json
|
||||
{
|
||||
"fulldomain": "foo.example.com",
|
||||
"subdomain": "foo",
|
||||
"username": "user",
|
||||
"password": "secret",
|
||||
"server_url": "https://example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Fetch All
|
||||
|
||||
### Request
|
||||
|
||||
Endpoint: `GET <BaseURL>`
|
||||
|
||||
### Response
|
||||
|
||||
Response status code 200.
|
||||
|
||||
Response body (domain/account mapping):
|
||||
|
||||
```json
|
||||
{
|
||||
"foo.example.com": {
|
||||
"fulldomain": "foo.example.com",
|
||||
"subdomain": "foo",
|
||||
"username": "user",
|
||||
"password": "secret",
|
||||
"server_url": "https://example.com"
|
||||
},
|
||||
"bar.example.com": {
|
||||
"fulldomain": "bar.example.com",
|
||||
"subdomain": "bar",
|
||||
"username": "user",
|
||||
"password": "secret",
|
||||
"server_url": "https://example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Put
|
||||
|
||||
### Request
|
||||
|
||||
Endpoint: `POST <BaseURL>/<domain>`
|
||||
|
||||
### Response
|
||||
|
||||
Response status code 200.
|
||||
|
||||
No expected body.
|
||||
|
||||
## Save
|
||||
|
||||
No dedicated endpoint.
|
||||
143
providers/dns/acmedns/mock_test.go
Normal file
143
providers/dns/acmedns/mock_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/nrdcg/goacmedns"
|
||||
"github.com/nrdcg/goacmedns/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
// errorClientErr is used by the Client mocks that return an error.
|
||||
errorClientErr = errors.New("errorClient always errors")
|
||||
// errorStorageErr is used by the Storage mocks that return an error.
|
||||
errorStorageErr = errors.New("errorStorage always errors")
|
||||
)
|
||||
|
||||
var egTestAccount = goacmedns.Account{
|
||||
FullDomain: "acme-dns." + egDomain,
|
||||
SubDomain: "random-looking-junk." + egDomain,
|
||||
Username: "spooky.mulder",
|
||||
Password: "trustno1",
|
||||
}
|
||||
|
||||
// mockClient is a mock implementing the acmeDNSClient interface that always
|
||||
// returns a fixed goacmedns.Account from calls to Register.
|
||||
type mockClient struct {
|
||||
mockAccount goacmedns.Account
|
||||
}
|
||||
|
||||
// UpdateTXTRecord does nothing.
|
||||
func (c mockClient) UpdateTXTRecord(_ context.Context, _ goacmedns.Account, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterAccount returns c.mockAccount and no errors.
|
||||
func (c mockClient) RegisterAccount(_ context.Context, _ []string) (goacmedns.Account, error) {
|
||||
return c.mockAccount, nil
|
||||
}
|
||||
|
||||
// mockUpdateClient is a mock implementing the acmeDNSClient interface that
|
||||
// tracks the calls to UpdateTXTRecord in the records map.
|
||||
type mockUpdateClient struct {
|
||||
mockClient
|
||||
records map[goacmedns.Account]string
|
||||
}
|
||||
|
||||
// UpdateTXTRecord saves a record value to c.records for the given acct.
|
||||
func (c mockUpdateClient) UpdateTXTRecord(_ context.Context, acct goacmedns.Account, value string) error {
|
||||
c.records[acct] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// errorUpdateClient is a mock implementing the acmeDNSClient interface that always
|
||||
// returns errors from errorUpdateClient.
|
||||
type errorUpdateClient struct {
|
||||
mockClient
|
||||
}
|
||||
|
||||
// UpdateTXTRecord always returns an error.
|
||||
func (c errorUpdateClient) UpdateTXTRecord(_ context.Context, _ goacmedns.Account, _ string) error {
|
||||
return errorClientErr
|
||||
}
|
||||
|
||||
// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
|
||||
// returns errors from RegisterAccount.
|
||||
type errorRegisterClient struct {
|
||||
mockClient
|
||||
}
|
||||
|
||||
// RegisterAccount always returns an error.
|
||||
func (c errorRegisterClient) RegisterAccount(_ context.Context, _ []string) (goacmedns.Account, error) {
|
||||
return goacmedns.Account{}, errorClientErr
|
||||
}
|
||||
|
||||
// mockStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// returns static account data and ignores Save.
|
||||
type mockStorage struct {
|
||||
accounts map[string]goacmedns.Account
|
||||
}
|
||||
|
||||
// Save does nothing.
|
||||
func (m mockStorage) Save(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put stores an account for the given domain in m.accounts.
|
||||
func (m mockStorage) Put(_ context.Context, domain string, acct goacmedns.Account) error {
|
||||
m.accounts[domain] = acct
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch retrieves an account for the given domain from m.accounts or returns
|
||||
// goacmedns.ErrDomainNotFound.
|
||||
func (m mockStorage) Fetch(_ context.Context, domain string) (goacmedns.Account, error) {
|
||||
if acct, ok := m.accounts[domain]; ok {
|
||||
return acct, nil
|
||||
}
|
||||
return goacmedns.Account{}, storage.ErrDomainNotFound
|
||||
}
|
||||
|
||||
// FetchAll returns all of m.accounts.
|
||||
func (m mockStorage) FetchAll(_ context.Context) (map[string]goacmedns.Account, error) {
|
||||
return m.accounts, nil
|
||||
}
|
||||
|
||||
// errorPutStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// always returns errors from Put.
|
||||
type errorPutStorage struct {
|
||||
mockStorage
|
||||
}
|
||||
|
||||
// Put always errors.
|
||||
func (e errorPutStorage) Put(_ context.Context, _ string, _ goacmedns.Account) error {
|
||||
return errorStorageErr
|
||||
}
|
||||
|
||||
// errorSaveStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// always returns errors from Save.
|
||||
type errorSaveStorage struct {
|
||||
mockStorage
|
||||
}
|
||||
|
||||
// Save always errors.
|
||||
func (e errorSaveStorage) Save(_ context.Context) error {
|
||||
return errorStorageErr
|
||||
}
|
||||
|
||||
// errorFetchStorage is a mock implementing the goacmedns.Storage interface that
|
||||
// always returns errors from Fetch.
|
||||
type errorFetchStorage struct {
|
||||
mockStorage
|
||||
}
|
||||
|
||||
// Fetch always errors.
|
||||
func (e errorFetchStorage) Fetch(_ context.Context, _ string) (goacmedns.Account, error) {
|
||||
return goacmedns.Account{}, errorStorageErr
|
||||
}
|
||||
|
||||
// FetchAll is a nop for errorFetchStorage.
|
||||
func (e errorFetchStorage) FetchAll(_ context.Context) (map[string]goacmedns.Account, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user