04e2d74406
This commit adds a new DNS provider for [acme-dns](https://github.com/joohoi/acme-dns) to allow Lego to set DNS-01 challenge response TXT with an ACME-DNS server automatically. ACME-DNS allows ceding minimal zone editing permissions to the ACME client and can be useful when the primary DNS provider for the zone does not allow scripting/API access but can set a CNAME to an ACME-DNS server. Lower level ACME-DNS API calls & account loading/storing is handled by the `github.com/cpu/goacmedns` library. The provider loads existing ACME-DNS accounts from the specified JSON file on disk. Any accounts the provider registers on behalf of the user will also be saved to this JSON file. When required, the provider handles registering accounts with the ACME-DNS server domains that do not already have an ACME-DNS account. This will halt issuance with an error prompting the user to set the one-time manual CNAME required to delegate the DNS-01 challenge record to the ACME-DNS server. Subsequent runs will use the account from disk and assume the CNAME is in-place.
89 lines
3 KiB
Go
89 lines
3 KiB
Go
package goacmedns
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io/ioutil"
|
|
"os"
|
|
)
|
|
|
|
// Storage is an interface describing the required functions for an ACME DNS
|
|
// Account storage mechanism.
|
|
type Storage interface {
|
|
// Save will persist the `Account` data that has been `Put` so far
|
|
Save() error
|
|
// Put will add an `Account` for the given domain to the storage. It may not
|
|
// be persisted until `Save` is called.
|
|
Put(string, Account) error
|
|
// Fetch will retrieve an `Account` for the given domain from the storage. If
|
|
// the provided domain does not have an `Account` saved in the storage
|
|
// `ErrDomainNotFound` will be returned
|
|
Fetch(string) (Account, error)
|
|
}
|
|
|
|
var (
|
|
// ErrDomainNotFound is returned from `Fetch` when the provided domain is not
|
|
// present in the storage.
|
|
ErrDomainNotFound = errors.New("requested domain is not present in storage")
|
|
)
|
|
|
|
// fileStorage implements the `Storage` interface and persists `Accounts` to
|
|
// a JSON file on disk.
|
|
type fileStorage struct {
|
|
// path is the filepath that the `accounts` are persisted to when the `Save`
|
|
// function is called.
|
|
path string
|
|
// mode is the file mode used when the `path` JSON file must be created
|
|
mode os.FileMode
|
|
// accounts holds the `Account` data that has been `Put` into the storage
|
|
accounts map[string]Account
|
|
}
|
|
|
|
// NewFileStorage returns a `Storage` implementation backed by JSON content
|
|
// saved into the provided `path` on disk. The file at `path` will be created if
|
|
// required. When creating a new file the provided `mode` is used to set the
|
|
// permissions.
|
|
func NewFileStorage(path string, mode os.FileMode) Storage {
|
|
fs := fileStorage{
|
|
path: path,
|
|
mode: mode,
|
|
accounts: make(map[string]Account),
|
|
}
|
|
// Opportunistically try to load the account data. Return an empty account if
|
|
// any errors occur.
|
|
if jsonData, err := ioutil.ReadFile(path); err == nil {
|
|
if err := json.Unmarshal(jsonData, &fs.accounts); err != nil {
|
|
return fs
|
|
}
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// Save persists the `Account` data to the fileStorage's configured path. The
|
|
// file at that path will be created with the fileStorage's mode if required.
|
|
func (f fileStorage) Save() error {
|
|
if serialized, err := json.Marshal(f.accounts); err != nil {
|
|
return err
|
|
} else if err = ioutil.WriteFile(f.path, serialized, f.mode); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Put saves an `Account` for the given `Domain` into the in-memory accounts of
|
|
// the fileStorage instance. The `Account` data will not be written to disk
|
|
// until the `Save` function is called
|
|
func (f fileStorage) Put(domain string, acct Account) error {
|
|
f.accounts[domain] = acct
|
|
return nil
|
|
}
|
|
|
|
// Fetch retrieves the `Account` object for the given `domain` from the
|
|
// fileStorage in-memory accounts. If the `domain` provided does not have an
|
|
// `Account` in the storage an `ErrDomainNotFound` error is returned.
|
|
func (f fileStorage) Fetch(domain string) (Account, error) {
|
|
if acct, exists := f.accounts[domain]; exists {
|
|
return acct, nil
|
|
}
|
|
return Account{}, ErrDomainNotFound
|
|
}
|