diff --git a/Gopkg.lock b/Gopkg.lock index f6718f283..598f11735 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -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" diff --git a/vendor/github.com/cpu/goacmedns/LICENSE b/vendor/github.com/cpu/goacmedns/LICENSE new file mode 100644 index 000000000..3215897de --- /dev/null +++ b/vendor/github.com/cpu/goacmedns/LICENSE @@ -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. diff --git a/vendor/github.com/cpu/goacmedns/account.go b/vendor/github.com/cpu/goacmedns/account.go new file mode 100644 index 000000000..9a1d9c81a --- /dev/null +++ b/vendor/github.com/cpu/goacmedns/account.go @@ -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 +} diff --git a/vendor/github.com/cpu/goacmedns/client.go b/vendor/github.com/cpu/goacmedns/client.go new file mode 100644 index 000000000..8be70306e --- /dev/null +++ b/vendor/github.com/cpu/goacmedns/client.go @@ -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 +} diff --git a/vendor/github.com/cpu/goacmedns/storage.go b/vendor/github.com/cpu/goacmedns/storage.go new file mode 100644 index 000000000..6e0186b0c --- /dev/null +++ b/vendor/github.com/cpu/goacmedns/storage.go @@ -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 +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/acmedns/acmedns.go b/vendor/github.com/xenolf/lego/providers/dns/acmedns/acmedns.go new file mode 100644 index 000000000..cce0d8d87 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/acmedns/acmedns.go @@ -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, + } +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go b/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go index 96881c19d..ec8c31875 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go +++ b/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go @@ -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": diff --git a/vendor/github.com/xenolf/lego/providers/dns/duckdns/duckdns.go b/vendor/github.com/xenolf/lego/providers/dns/duckdns/duckdns.go index e00f71370..a8e25ba8b 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/duckdns/duckdns.go +++ b/vendor/github.com/xenolf/lego/providers/dns/duckdns/duckdns.go @@ -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) -} diff --git a/vendor/github.com/xenolf/lego/providers/dns/exec/doc.go b/vendor/github.com/xenolf/lego/providers/dns/exec/doc.go new file mode 100644 index 000000000..9aa53fccc --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/exec/doc.go @@ -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 diff --git a/vendor/github.com/xenolf/lego/providers/dns/exec/exec.go b/vendor/github.com/xenolf/lego/providers/dns/exec/exec.go index 9bd97d03e..ea3a43983 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/exec/exec.go +++ b/vendor/github.com/xenolf/lego/providers/dns/exec/exec.go @@ -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 { - 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 + var args []string + if d.config.Mode == "RAW" { + args = []string{"present", "--", domain, token, keyAuth} + } else { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + 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 { - 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 + var args []string + if d.config.Mode == "RAW" { + args = []string{"cleanup", "--", domain, token, keyAuth} + } else { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + args = []string{"cleanup", 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 } diff --git a/vendor/github.com/xenolf/lego/providers/dns/gcloud/googlecloud.go b/vendor/github.com/xenolf/lego/providers/dns/gcloud/googlecloud.go index 2999a79dc..0f169677a 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/gcloud/googlecloud.go +++ b/vendor/github.com/xenolf/lego/providers/dns/gcloud/googlecloud.go @@ -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,16 +156,12 @@ 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 } - 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 @@ -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 } diff --git a/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go b/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go index d37da4cdb..148747bde 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go +++ b/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go @@ -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) diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go b/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go index adc15401c..d7cc4c719 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go +++ b/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go @@ -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 + client *route53.Route53 + config *Config } // customRetryer implements the client.Retryer interface by composing the @@ -65,35 +81,49 @@ 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 } client := route53.New(session) return &DNSProvider{ - client: client, - hostedZoneID: hostedZoneID, + client: client, + 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)