Update lego

This commit is contained in:
Michael 2018-07-23 17:30:03 +02:00 committed by Traefiker Bot
parent c8ae97fd38
commit aabebb2185
13 changed files with 696 additions and 112 deletions

9
Gopkg.lock generated
View file

@ -316,6 +316,12 @@
revision = "48702e0da86bd25e76cfef347e2adeb434a0d0a6"
version = "v14"
[[projects]]
branch = "master"
name = "github.com/cpu/goacmedns"
packages = ["."]
revision = "565ecf2a84df654865cc102705ac160a3b04fc01"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
@ -1299,6 +1305,7 @@
"log",
"platform/config/env",
"providers/dns",
"providers/dns/acmedns",
"providers/dns/auroradns",
"providers/dns/azure",
"providers/dns/bluecat",
@ -1334,7 +1341,7 @@
"providers/dns/vegadns",
"providers/dns/vultr"
]
revision = "e0d512138c43e3f056a41cd7a5beff662ec130d3"
revision = "8b6701514cc0a6285a327908f3f9ce05bcacbffd"
[[projects]]
branch = "master"

21
vendor/github.com/cpu/goacmedns/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Daniel McCarney
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
vendor/github.com/cpu/goacmedns/account.go generated vendored Normal file
View file

@ -0,0 +1,11 @@
package goacmedns
// Account is a struct that holds the registration response from an ACME-DNS
// server. It represents an API username/key that can be used to update TXT
// records for the account's subdomain.
type Account struct {
FullDomain string
SubDomain string
Username string
Password string
}

191
vendor/github.com/cpu/goacmedns/client.go generated vendored Normal file
View file

@ -0,0 +1,191 @@
package goacmedns
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"runtime"
"time"
)
const (
// ua is a custom user-agent identifier
ua = "goacmedns"
)
// userAgent returns a string that can be used as a HTTP request `User-Agent`
// header. It includes the `ua` string alongside the OS and architecture of the
// system.
func userAgent() string {
return fmt.Sprintf("%s (%s; %s)", ua, runtime.GOOS, runtime.GOARCH)
}
var (
// defaultTimeout is used for the httpClient Timeout settings
defaultTimeout = 30 * time.Second
// httpClient is a `http.Client` that is customized with the `defaultTimeout`
httpClient = http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: defaultTimeout,
KeepAlive: defaultTimeout,
}).Dial,
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
ExpectContinueTimeout: 1 * time.Second,
},
}
)
// postAPI makes an HTTP POST request to the given URL, sending the given body
// and attaching the requested custom headers to the request. If there is no
// error the HTTP response body and HTTP response object are returned, otherwise
// an error is returned.. All POST requests include a `User-Agent` header
// populated with the `userAgent` function and a `Content-Type` header of
// `application/json`.
func postAPI(url string, body []byte, headers map[string]string) ([]byte, *http.Response, error) {
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
fmt.Printf("Failed to make req: %s\n", err.Error())
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent())
for h, v := range headers {
req.Header.Set(h, v)
}
resp, err := httpClient.Do(req)
if err != nil {
fmt.Printf("Failed to do req: %s\n", err.Error())
return nil, resp, err
}
defer func() { _ = resp.Body.Close() }()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read body: %s\n", err.Error())
return nil, resp, err
}
return respBody, resp, nil
}
// ClientError represents an error from the ACME-DNS server. It holds
// a `Message` describing the operation the client was doing, a `HTTPStatus`
// code returned by the server, and the `Body` of the HTTP Response from the
// server.
type ClientError struct {
// Message is a string describing the client operation that failed
Message string
// HTTPStatus is the HTTP status code the ACME DNS server returned
HTTPStatus int
// Body is the response body the ACME DNS server returned
Body []byte
}
// Error collects all of the ClientError fields into a single string
func (e ClientError) Error() string {
return fmt.Sprintf("%s : status code %d response: %s",
e.Message, e.HTTPStatus, string(e.Body))
}
// newClientError creates a ClientError instance populated with the given
// arguments
func newClientError(msg string, respCode int, respBody []byte) ClientError {
return ClientError{
Message: msg,
HTTPStatus: respCode,
Body: respBody,
}
}
// Client is a struct that can be used to interact with an ACME DNS server to
// register accounts and update TXT records.
type Client struct {
// baseURL is the address of the ACME DNS server
baseURL string
}
// NewClient returns a Client configured to interact with the ACME DNS server at
// the given URL.
func NewClient(url string) Client {
return Client{
baseURL: url,
}
}
// RegisterAccount creates an Account with the ACME DNS server. The optional
// `allowFrom` argument is used to constrain which CIDR ranges can use the
// created Account.
func (c Client) RegisterAccount(allowFrom []string) (Account, error) {
var body []byte
if len(allowFrom) > 0 {
req := struct {
AllowFrom []string
}{
AllowFrom: allowFrom,
}
reqBody, err := json.Marshal(req)
if err != nil {
return Account{}, err
}
body = reqBody
}
url := fmt.Sprintf("%s/register", c.baseURL)
respBody, resp, err := postAPI(url, body, nil)
if err != nil {
return Account{}, err
}
if resp.StatusCode != http.StatusCreated {
return Account{}, newClientError(
"failed to register account", resp.StatusCode, respBody)
}
var acct Account
err = json.Unmarshal(respBody, &acct)
if err != nil {
return Account{}, err
}
return acct, nil
}
// UpdateTXTRecord updates a TXT record with the ACME DNS server to the `value`
// provided using the `account` specified.
func (c Client) UpdateTXTRecord(account Account, value string) error {
update := struct {
SubDomain string
Txt string
}{
SubDomain: account.SubDomain,
Txt: value,
}
updateBody, err := json.Marshal(update)
if err != nil {
fmt.Printf("Failed to marshal update: %s\n", update)
return err
}
headers := map[string]string{
"X-Api-User": account.Username,
"X-Api-Key": account.Password,
}
url := fmt.Sprintf("%s/update", c.baseURL)
respBody, resp, err := postAPI(url, updateBody, headers)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return newClientError(
"failed to update txt record", resp.StatusCode, respBody)
}
return nil
}

89
vendor/github.com/cpu/goacmedns/storage.go generated vendored Normal file
View file

@ -0,0 +1,89 @@
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
}

View file

@ -0,0 +1,170 @@
// Package acmedns implements a DNS provider for solving DNS-01 challenges using
// Joohoi's acme-dns project. For more information see the ACME-DNS homepage:
// https://github.com/joohoi/acme-dns
package acmedns
import (
"errors"
"fmt"
"github.com/cpu/goacmedns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const (
// envNamespace is the prefix for ACME-DNS environment variables.
envNamespace = "ACME_DNS_"
// apiBaseEnvVar is the environment variable name for the ACME-DNS API address
// (e.g. https://acmedns.your-domain.com).
apiBaseEnvVar = envNamespace + "API_BASE"
// storagePathEnvVar 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.
storagePathEnvVar = envNamespace + "STORAGE_PATH"
)
// acmeDNSClient is an interface describing the goacmedns.Client functions
// the DNSProvider uses. It makes it easier for tests to shim a mock Client into
// the DNSProvider.
type acmeDNSClient interface {
// UpdateTXTRecord updates the provided account's TXT record to the given
// value or returns an error.
UpdateTXTRecord(goacmedns.Account, string) error
// RegisterAccount registers and returns a new account with the given
// allowFrom restriction or returns an error.
RegisterAccount([]string) (goacmedns.Account, error)
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface for
// an ACME-DNS server.
type DNSProvider struct {
client acmeDNSClient
storage goacmedns.Storage
}
// NewDNSProvider creates an ACME-DNS provider using file based account storage.
// Its configuration is loaded from the environment by reading apiBaseEnvVar and
// storagePathEnvVar.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(apiBaseEnvVar, storagePathEnvVar)
if err != nil {
return nil, fmt.Errorf("acme-dns: %v", err)
}
client := goacmedns.NewClient(values[apiBaseEnvVar])
storage := goacmedns.NewFileStorage(values[storagePathEnvVar], 0600)
return NewDNSProviderClient(client, 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")
}
if storage == nil {
return nil, errors.New("ACME-DNS Storage must be not nil")
}
return &DNSProvider{
client: client,
storage: storage,
}, nil
}
// ErrCNAMERequired is returned by Present when the Domain indicated had no
// existing ACME-DNS account in the Storage and additional setup is required.
// The user must create a CNAME in the DNS zone for Domain that aliases FQDN
// to Target in order to complete setup for the ACME-DNS account that was
// created.
type ErrCNAMERequired struct {
// The Domain that is being issued for.
Domain string
// The alias of the CNAME (left hand DNS label).
FQDN string
// The RDATA of the CNAME (right hand side, canonical name).
Target string
}
// Error returns a descriptive message for the ErrCNAMERequired instance telling
// the user that a CNAME needs to be added to the DNS zone of c.Domain before
// the ACME-DNS hook will work. The CNAME to be created should be of the form:
// {{ c.FQDN }} CNAME {{ c.Target }}
func (e ErrCNAMERequired) Error() string {
return fmt.Sprintf("acme-dns: new account created for %q. "+
"To complete setup for %q you must provision the following "+
"CNAME in your DNS zone and re-run this provider when it is "+
"in place:\n"+
"%s CNAME %s.",
e.Domain, e.Domain, e.FQDN, e.Target)
}
// Present creates a TXT record to fulfil the DNS-01 challenge. If there is an
// existing account for the domain in the provider's storage then it will be
// used to set the challenge response TXT record with the ACME-DNS server and
// issuance will continue. If there is not an account for the given domain
// present in the DNSProvider storage 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 {
// Compute the challenge response FQDN and TXT value for the domain based
// on the keyAuth.
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
// Check if credentials were previously saved for this domain.
account, err := d.storage.Fetch(domain)
// Errors other than goacmeDNS.ErrDomainNotFound are unexpected.
if err != nil && err != goacmedns.ErrDomainNotFound {
return err
}
if err == goacmedns.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, fqdn)
}
// Update the acme-dns TXT record.
return d.client.UpdateTXTRecord(account, value)
}
// CleanUp removes the record matching the specified parameters. It is not
// implemented for the ACME-DNS provider.
func (d *DNSProvider) CleanUp(_, _, _ string) error {
// ACME-DNS doesn't support the notion of removing a record. For users of
// ACME-DNS it is expected the stale records remain in-place.
return nil
}
// register creates a new ACME-DNS account for the given domain. 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 {
// TODO(@cpu): Read CIDR whitelists from the environment
newAcct, err := d.client.RegisterAccount(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)
if err != nil {
return err
}
err = d.storage.Save()
if err != nil {
return err
}
// Stop issuance by returning an error. The user needs to perform a manual
// one-time CNAME setup in their DNS zone to complete the setup of the new
// account we created.
return ErrCNAMERequired{
Domain: domain,
FQDN: fqdn,
Target: newAcct.FullDomain,
}
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/providers/dns/acmedns"
"github.com/xenolf/lego/providers/dns/auroradns"
"github.com/xenolf/lego/providers/dns/azure"
"github.com/xenolf/lego/providers/dns/bluecat"
@ -43,6 +44,8 @@ import (
// NewDNSChallengeProviderByName Factory for DNS providers
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
switch name {
case "acme-dns":
return acmedns.NewDNSProvider()
case "azure":
return azure.NewDNSProvider()
case "auroradns":

View file

@ -30,26 +30,32 @@ func NewDNSProvider() (*DNSProvider, error) {
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for http://duckdns.org .
func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) {
if duckdnsToken == "" {
func NewDNSProviderCredentials(token string) (*DNSProvider, error) {
if token == "" {
return nil, errors.New("DuckDNS: credentials missing")
}
return &DNSProvider{token: duckdnsToken}, nil
return &DNSProvider{token: token}, nil
}
// makeDuckdnsURL creates a url to clear the set or unset the TXT record.
// txt == "" will clear the TXT record.
func makeDuckdnsURL(domain, token, txt string) string {
requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token)
if txt == "" {
return requestBase + "&clear=true"
}
return requestBase + "&txt=" + txt
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
return updateTxtRecord(domain, d.token, txtRecord, false)
}
func issueDuckdnsRequest(url string) error {
response, err := acme.HTTPClient.Get(url)
// CleanUp clears DuckDNS TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return updateTxtRecord(domain, d.token, "", true)
}
// updateTxtRecord Update the domains TXT record
// To update the TXT record we just need to make one simple get request.
// In DuckDNS you only have one TXT record shared with the domain and all sub domains.
func updateTxtRecord(domain, token, txt string, clear bool) error {
u := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s&clear=%t&txt=%s", domain, token, clear, txt)
response, err := acme.HTTPClient.Get(u)
if err != nil {
return err
}
@ -59,26 +65,10 @@ func issueDuckdnsRequest(url string) error {
if err != nil {
return err
}
body := string(bodyBytes)
if body != "OK" {
return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url)
return fmt.Errorf("request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]", body, u)
}
return nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// In duckdns you only have one TXT record shared with
// the domain and all sub domains.
//
// To update the TXT record we just need to make one simple get request.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
url := makeDuckdnsURL(domain, d.token, txtRecord)
return issueDuckdnsRequest(url)
}
// CleanUp clears duckdns TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
url := makeDuckdnsURL(domain, d.token, "")
return issueDuckdnsRequest(url)
}

View file

@ -0,0 +1,42 @@
/*
Package exec implements a manual DNS provider which runs a program for adding/removing the DNS record.
The file name of the external program is specified in the environment variable `EXEC_PATH`.
When it is run by lego, three command-line parameters are passed to it:
The action ("present" or "cleanup"), the fully-qualified domain name, the value for the record and the TTL.
For example, requesting a certificate for the domain 'foo.example.com' can be achieved by calling lego as follows:
EXEC_PATH=./update-dns.sh \
lego --dns exec \
--domains foo.example.com \
--email invalid@example.com run
It will then call the program './update-dns.sh' with like this:
./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120"
The program then needs to make sure the record is inserted.
When it returns an error via a non-zero exit code, lego aborts.
When the record is to be removed again,
the program is called with the first command-line parameter set to "cleanup" instead of "present".
If you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`:
EXEC_MODE=RAW \
EXEC_PATH=./update-dns.sh \
lego --dns exec \
--domains foo.example.com \
--email invalid@example.com run
It will then call the program './update-dns.sh' like this:
./update-dns.sh "present" "foo.example.com." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8"
NOTE:
The `--` is because the token MAY start with a `-`, and the called program may try and interpret a - as indicating a flag.
In the case of urfave, which is commonly used,
you can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely.
*/
package exec

View file

@ -1,78 +1,100 @@
// Package exec implements a manual DNS provider which runs a program for
// adding/removing the DNS record.
//
// The file name of the external program is specified in the environment
// variable EXEC_PATH. When it is run by lego, three command-line parameters
// are passed to it: The action ("present" or "cleanup"), the fully-qualified domain
// name, the value for the record and the TTL.
//
// For example, requesting a certificate for the domain 'foo.example.com' can
// be achieved by calling lego as follows:
//
// EXEC_PATH=./update-dns.sh \
// lego --dns exec \
// --domains foo.example.com \
// --email invalid@example.com run
//
// It will then call the program './update-dns.sh' with like this:
//
// ./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120"
//
// The program then needs to make sure the record is inserted. When it returns
// an error via a non-zero exit code, lego aborts.
//
// When the record is to be removed again, the program is called with the first
// command-line parameter set to "cleanup" instead of "present".
package exec
import (
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/log"
"github.com/xenolf/lego/platform/config/env"
)
// Config Provider configuration.
type Config struct {
Program string
Mode string
}
// DNSProvider adds and removes the record for the DNS challenge by calling a
// program with command-line parameters.
type DNSProvider struct {
program string
config *Config
}
// NewDNSProvider returns a new DNS provider which runs the program in the
// environment variable EXEC_PATH for adding and removing the DNS record.
func NewDNSProvider() (*DNSProvider, error) {
s := os.Getenv("EXEC_PATH")
if s == "" {
return nil, errors.New("environment variable EXEC_PATH not set")
values, err := env.Get("EXEC_PATH")
if err != nil {
return nil, fmt.Errorf("exec: %v", err)
}
return NewDNSProviderProgram(s)
return NewDNSProviderConfig(&Config{
Program: values["EXEC_PATH"],
Mode: os.Getenv("EXEC_MODE"),
})
}
// NewDNSProviderConfig returns a new DNS provider which runs the given configuration
// for adding and removing the DNS record.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration is nil")
}
return &DNSProvider{config: config}, nil
}
// NewDNSProviderProgram returns a new DNS provider which runs the given program
// for adding and removing the DNS record.
// Deprecated: use NewDNSProviderConfig instead
func NewDNSProviderProgram(program string) (*DNSProvider, error) {
return &DNSProvider{program: program}, nil
if len(program) == 0 {
return nil, errors.New("the program is undefined")
}
return NewDNSProviderConfig(&Config{Program: program})
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
var args []string
if d.config.Mode == "RAW" {
args = []string{"present", "--", domain, token, keyAuth}
} else {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
cmd := exec.Command(d.program, "present", fqdn, value, strconv.Itoa(ttl))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
args = []string{"present", fqdn, value, strconv.Itoa(ttl)}
}
return cmd.Run()
cmd := exec.Command(d.config.Program, args...)
output, err := cmd.CombinedOutput()
if len(output) > 0 {
log.Println(string(output))
}
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
var args []string
if d.config.Mode == "RAW" {
args = []string{"cleanup", "--", domain, token, keyAuth}
} else {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
cmd := exec.Command(d.program, "cleanup", fqdn, value, strconv.Itoa(ttl))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
args = []string{"cleanup", fqdn, value, strconv.Itoa(ttl)}
}
cmd := exec.Command(d.config.Program, args...)
output, err := cmd.CombinedOutput()
if len(output) > 0 {
log.Println(string(output))
}
return err
}

View file

@ -115,13 +115,13 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
}
// Look for existing records.
list, err := d.client.ResourceRecordSets.List(d.project, zone).Name(fqdn).Type("TXT").Do()
existing, err := d.findTxtRecords(zone, fqdn)
if err != nil {
return err
}
if len(list.Rrsets) > 0 {
if len(existing) > 0 {
// Attempt to delete the existing records when adding our new one.
change.Deletions = list.Rrsets
change.Deletions = existing
}
chg, err := d.client.Changes.Create(d.project, zone, change).Do()
@ -156,18 +156,14 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return err
}
for _, rec := range records {
change := &dns.Change{
Deletions: []*dns.ResourceRecordSet{rec},
}
_, err = d.client.Changes.Create(d.project, zone, change).Do()
if err != nil {
return err
}
}
if len(records) == 0 {
return nil
}
_, err = d.client.Changes.Create(d.project, zone, &dns.Change{Deletions: records}).Do()
return err
}
// Timeout customizes the timeout values used by the ACME package for checking
// DNS record validity.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
@ -198,17 +194,10 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) {
func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
recs, err := d.client.ResourceRecordSets.List(d.project, zone).Do()
recs, err := d.client.ResourceRecordSets.List(d.project, zone).Name(fqdn).Type("TXT").Do()
if err != nil {
return nil, err
}
var found []*dns.ResourceRecordSet
for _, r := range recs.Rrsets {
if r.Type == "TXT" && r.Name == fqdn {
found = append(found, r)
}
}
return found, nil
return recs.Rrsets, nil
}

View file

@ -5,6 +5,7 @@ package ns1
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/xenolf/lego/acme"
@ -75,7 +76,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
func (d *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) {
zone, _, err := d.client.Zones.Get(domain)
authZone, err := getAuthZone(domain)
if err != nil {
return nil, err
}
zone, _, err := d.client.Zones.Get(authZone)
if err != nil {
return nil, err
}
@ -83,6 +89,19 @@ func (d *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) {
return zone, nil
}
func getAuthZone(fqdn string) (string, error) {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
if strings.HasSuffix(authZone, ".") {
authZone = authZone[:len(authZone)-len(".")]
}
return authZone, err
}
func (d *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record {
name := acme.UnFqdn(fqdn)

View file

@ -3,6 +3,7 @@
package route53
import (
"errors"
"fmt"
"math/rand"
"os"
@ -17,15 +18,30 @@ import (
"github.com/xenolf/lego/acme"
)
const (
maxRetries = 5
route53TTL = 10
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
MaxRetries int
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HostedZoneID string
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
MaxRetries: 5,
TTL: 10,
PropagationTimeout: time.Minute * 2,
PollingInterval: time.Second * 4,
HostedZoneID: os.Getenv("AWS_HOSTED_ZONE_ID"),
}
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *route53.Route53
hostedZoneID string
config *Config
}
// customRetryer implements the client.Retryer interface by composing the
@ -65,12 +81,20 @@ func (d customRetryer) RetryRules(r *request.Request) time.Duration {
//
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
func NewDNSProvider() (*DNSProvider, error) {
hostedZoneID := os.Getenv("AWS_HOSTED_ZONE_ID")
return NewDNSProviderConfig(NewDefaultConfig())
}
// NewDNSProviderConfig takes a given config ans returns a custom configured
// DNSProvider instance
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the Route53 DNS provider is nil")
}
r := customRetryer{}
r.NumMaxRetries = maxRetries
config := request.WithRetryer(aws.NewConfig(), r)
session, err := session.NewSessionWithOptions(session.Options{Config: *config})
r.NumMaxRetries = config.MaxRetries
sessionCfg := request.WithRetryer(aws.NewConfig(), r)
session, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg})
if err != nil {
return nil, err
}
@ -78,22 +102,28 @@ func NewDNSProvider() (*DNSProvider, error) {
return &DNSProvider{
client: client,
hostedZoneID: hostedZoneID,
config: config,
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation.
func (r *DNSProvider) Timeout() (timeout, interval time.Duration) {
return r.config.PropagationTimeout, r.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("UPSERT", fqdn, value, route53TTL)
return r.changeRecord("UPSERT", fqdn, value, r.config.TTL)
}
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("DELETE", fqdn, value, route53TTL)
return r.changeRecord("DELETE", fqdn, value, r.config.TTL)
}
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
@ -123,7 +153,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
statusID := resp.ChangeInfo.Id
return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) {
return acme.WaitFor(r.config.PropagationTimeout, r.config.PollingInterval, func() (bool, error) {
reqParams := &route53.GetChangeInput{
Id: statusID,
}
@ -139,8 +169,8 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
}
func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
if r.hostedZoneID != "" {
return r.hostedZoneID, nil
if r.config.HostedZoneID != "" {
return r.config.HostedZoneID, nil
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)