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/docs/configuration/acme.md b/docs/configuration/acme.md index f293ab7c3..07c10678f 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -269,7 +269,7 @@ Here is a list of supported `provider`s, that can automate the DNS verification, | [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet | | [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet | | manual | - | none, but you need to run Træfik interactively, turn on `acmeLogging` to see instructions and press Enter. | YES | -| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | Not tested yet | +| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | YES | | [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet | | [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | Not tested yet | | [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | Not tested yet | diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index f2ff8d853..d6f0eb42f 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -17,7 +17,7 @@ The config files used in this guide can be found in the [examples directory](htt ### Role Based Access Control configuration (Kubernetes 1.6+ only) -Kubernetes introduces [Role Based Access Control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) in 1.6+ to allow fine-grained control of Kubernetes resources and API. +Kubernetes introduces [Role Based Access Control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) in 1.6+ to allow fine-grained control of Kubernetes resources and API. If your cluster is configured with RBAC, you will need to authorize Træfik to use the Kubernetes API. There are two ways to set up the proper permission: Via namespace-specific RoleBindings or a single, global ClusterRoleBinding. diff --git a/script/docs-verify-docker-image/validate.sh b/script/docs-verify-docker-image/validate.sh index 17563a0d3..aaf3da0ec 100644 --- a/script/docs-verify-docker-image/validate.sh +++ b/script/docs-verify-docker-image/validate.sh @@ -18,6 +18,7 @@ find "${PATH_TO_SITE}" -type f -not -path "/app/site/theme/*" \ | xargs -0 -r -P "${NUMBER_OF_CPUS}" -I '{}' \ htmlproofer \ --check-html \ + --only_4xx \ --alt_ignore="/traefik.logo.png/" \ --url-ignore "/localhost:/,/127.0.0.1:/,/fonts.gstatic.com/,/.minikube/,/github.com\/containous\/traefik\/*edit*/,/github.com\/containous\/traefik\/$/" \ '{}' 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)