summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/xenolf/lego
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/xenolf/lego')
-rw-r--r--vendor/github.com/xenolf/lego/.gitcookies.encbin0 -> 480 bytes
-rw-r--r--vendor/github.com/xenolf/lego/.gitignore4
-rw-r--r--vendor/github.com/xenolf/lego/.travis.yml12
-rw-r--r--vendor/github.com/xenolf/lego/CHANGELOG.md94
-rw-r--r--vendor/github.com/xenolf/lego/CONTRIBUTING.md32
-rw-r--r--vendor/github.com/xenolf/lego/Dockerfile14
-rw-r--r--vendor/github.com/xenolf/lego/LICENSE21
-rw-r--r--vendor/github.com/xenolf/lego/README.md257
-rw-r--r--vendor/github.com/xenolf/lego/account.go109
-rw-r--r--vendor/github.com/xenolf/lego/acme/challenges.go16
-rw-r--r--vendor/github.com/xenolf/lego/acme/client.go804
-rw-r--r--vendor/github.com/xenolf/lego/acme/client_test.go198
-rw-r--r--vendor/github.com/xenolf/lego/acme/crypto.go332
-rw-r--r--vendor/github.com/xenolf/lego/acme/crypto_test.go93
-rw-r--r--vendor/github.com/xenolf/lego/acme/dns_challenge.go282
-rw-r--r--vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go53
-rw-r--r--vendor/github.com/xenolf/lego/acme/dns_challenge_test.go185
-rw-r--r--vendor/github.com/xenolf/lego/acme/error.go86
-rw-r--r--vendor/github.com/xenolf/lego/acme/http.go117
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_challenge.go41
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_challenge_server.go79
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_challenge_test.go57
-rw-r--r--vendor/github.com/xenolf/lego/acme/http_test.go100
-rw-r--r--vendor/github.com/xenolf/lego/acme/jws.go115
-rw-r--r--vendor/github.com/xenolf/lego/acme/messages.go117
-rw-r--r--vendor/github.com/xenolf/lego/acme/pop_challenge.go1
-rw-r--r--vendor/github.com/xenolf/lego/acme/provider.go28
-rw-r--r--vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go67
-rw-r--r--vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go62
-rw-r--r--vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go65
-rw-r--r--vendor/github.com/xenolf/lego/acme/utils.go29
-rw-r--r--vendor/github.com/xenolf/lego/acme/utils_test.go26
-rw-r--r--vendor/github.com/xenolf/lego/cli.go214
-rw-r--r--vendor/github.com/xenolf/lego/cli_handlers.go444
-rw-r--r--vendor/github.com/xenolf/lego/configuration.go76
-rw-r--r--vendor/github.com/xenolf/lego/crypto.go56
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go223
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go80
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go166
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go117
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go141
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go79
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go248
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go37
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go274
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go53
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go472
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go939
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go158
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go85
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/linode/linode.go131
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go317
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go416
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go402
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go159
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go103
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/pdns/README.md7
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go343
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go80
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go129
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go244
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go39
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/route53.go171
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go70
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go87
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go38
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go127
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go65
-rw-r--r--vendor/github.com/xenolf/lego/providers/http/webroot/webroot.go58
-rw-r--r--vendor/github.com/xenolf/lego/providers/http/webroot/webroot_test.go46
70 files changed, 10390 insertions, 0 deletions
diff --git a/vendor/github.com/xenolf/lego/.gitcookies.enc b/vendor/github.com/xenolf/lego/.gitcookies.enc
new file mode 100644
index 000000000..09c303c94
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/.gitcookies.enc
Binary files differ
diff --git a/vendor/github.com/xenolf/lego/.gitignore b/vendor/github.com/xenolf/lego/.gitignore
new file mode 100644
index 000000000..74d32f0ab
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/.gitignore
@@ -0,0 +1,4 @@
+lego.exe
+lego
+.lego
+.idea
diff --git a/vendor/github.com/xenolf/lego/.travis.yml b/vendor/github.com/xenolf/lego/.travis.yml
new file mode 100644
index 000000000..f1af03bd6
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/.travis.yml
@@ -0,0 +1,12 @@
+language: go
+go:
+- 1.6.3
+- 1.7
+- tip
+install:
+- go get -t ./...
+script:
+- go vet ./...
+- go test -v ./...
+before_install:
+- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && openssl aes-256-cbc -K $encrypted_26c593b079d9_key -iv $encrypted_26c593b079d9_iv -in .gitcookies.enc -out .gitcookies -d || true'
diff --git a/vendor/github.com/xenolf/lego/CHANGELOG.md b/vendor/github.com/xenolf/lego/CHANGELOG.md
new file mode 100644
index 000000000..c43c4a936
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/CHANGELOG.md
@@ -0,0 +1,94 @@
+# Changelog
+
+## [0.3.1] - 2016-04-19
+
+### Added:
+- lib: A new DNS provider for Vultr.
+
+### Fixed:
+- lib: DNS Provider for DigitalOcean could not handle subdomains properly.
+- lib: handleHTTPError should only try to JSON decode error messages with the right content type.
+- lib: The propagation checker for the DNS challenge would not retry on send errors.
+
+
+## [0.3.0] - 2016-03-19
+
+### Added:
+- CLI: The `--dns` switch. To include the DNS challenge for consideration. When using this switch, all other solvers are disabled. Supported are the following solvers: cloudflare, digitalocean, dnsimple, dyn, gandi, googlecloud, namecheap, route53, rfc2136 and manual.
+- CLI: The `--accept-tos` switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you.
+- CLI: The `--webroot` switch. The HTTP-01 challenge may now be completed by dropping a file into a webroot. When using this switch, all other solvers are disabled.
+- CLI: The `--key-type` switch. This replaces the `--rsa-key-size` switch and supports the following key types: EC256, EC384, RSA2048, RSA4096 and RSA8192.
+- CLI: The `--dnshelp` switch. This displays a more in-depth help topic for DNS solvers.
+- CLI: The `--no-bundle` sub switch for the `run` and `renew` commands. When this switch is set, the CLI will not bundle the issuer certificate with your certificate.
+- lib: A new type for challenge identifiers `Challenge`
+- lib: A new interface for custom challenge providers `acme.ChallengeProvider`
+- lib: A new interface for DNS-01 providers to allow for custom timeouts for the validation function `acme.ChallengeProviderTimeout`
+- lib: SetChallengeProvider function. Pass a challenge identifier and a Provider to replace the default behaviour of a challenge.
+- lib: The DNS-01 challenge has been implemented with modular solvers using the `ChallengeProvider` interface. Included solvers are: cloudflare, digitalocean, dnsimple, gandi, namecheap, route53, rfc2136 and manual.
+- lib: The `acme.KeyType` type was added and is used for the configuration of crypto parameters for RSA and EC keys. Valid KeyTypes are: EC256, EC384, RSA2048, RSA4096 and RSA8192.
+
+### Changed
+- lib: ExcludeChallenges now expects to be passed an array of `Challenge` types.
+- lib: HTTP-01 now supports custom solvers using the `ChallengeProvider` interface.
+- lib: TLS-SNI-01 now supports custom solvers using the `ChallengeProvider` interface.
+- lib: The `GetPrivateKey` function in the `acme.User` interface is now expected to return a `crypto.PrivateKey` instead of an `rsa.PrivateKey` for EC compat.
+- lib: The `acme.NewClient` function now expects an `acme.KeyType` instead of the keyBits parameter.
+
+### Removed
+- CLI: The `rsa-key-size` switch was removed in favor of `key-type` to support EC keys.
+
+### Fixed
+- lib: Fixed a race condition in HTTP-01
+- lib: Fixed an issue where status codes on ACME challenge responses could lead to no action being taken.
+- lib: Fixed a regression when calling the Renew function with a SAN certificate.
+
+## [0.2.0] - 2016-01-09
+
+### Added:
+- CLI: The `--exclude` or `-x` switch. To exclude a challenge from being solved.
+- CLI: The `--http` switch. To set the listen address and port of HTTP based challenges. Supports `host:port` and `:port` for any interface.
+- CLI: The `--tls` switch. To set the listen address and port of TLS based challenges. Supports `host:port` and `:port` for any interface.
+- CLI: The `--reuse-key` switch for the `renew` operation. This lets you reuse an existing private key for renewals.
+- lib: ExcludeChallenges function. Pass an array of challenge identifiers to exclude them from solving.
+- lib: SetHTTPAddress function. Pass a port to set the listen port for HTTP based challenges.
+- lib: SetTLSAddress function. Pass a port to set the listen port of TLS based challenges.
+- lib: acme.UserAgent variable. Use this to customize the user agent on all requests sent by lego.
+
+### Changed:
+- lib: NewClient does no longer accept the optPort parameter
+- lib: ObtainCertificate now returns a SAN certificate if you pass more then one domain.
+- lib: GetOCSPForCert now returns the parsed OCSP response instead of just the status.
+- lib: ObtainCertificate has a new parameter `privKey crypto.PrivateKey` which lets you reuse an existing private key for new certificates.
+- lib: RenewCertificate now expects the PrivateKey property of the CertificateResource to be set only if you want to reuse the key.
+
+### Removed:
+- CLI: The `--port` switch was removed.
+- lib: RenewCertificate does no longer offer to also revoke your old certificate.
+
+### Fixed:
+- CLI: Fix logic using the `--days` parameter for renew
+
+## [0.1.1] - 2015-12-18
+
+### Added:
+- CLI: Added a way to automate renewal through a cronjob using the --days parameter to renew
+
+### Changed:
+- lib: Improved log output on challenge failures.
+
+### Fixed:
+- CLI: The short parameter for domains would not get accepted
+- CLI: The cli did not return proper exit codes on error library errors.
+- lib: RenewCertificate did not properly renew SAN certificates.
+
+### Security
+- lib: Fix possible DOS on GetOCSPForCert
+
+## [0.1.0] - 2015-12-03
+- Initial release
+
+[0.3.1]: https://github.com/xenolf/lego/compare/v0.3.0...v0.3.1
+[0.3.0]: https://github.com/xenolf/lego/compare/v0.2.0...v0.3.0
+[0.2.0]: https://github.com/xenolf/lego/compare/v0.1.1...v0.2.0
+[0.1.1]: https://github.com/xenolf/lego/compare/v0.1.0...v0.1.1
+[0.1.0]: https://github.com/xenolf/lego/tree/v0.1.0
diff --git a/vendor/github.com/xenolf/lego/CONTRIBUTING.md b/vendor/github.com/xenolf/lego/CONTRIBUTING.md
new file mode 100644
index 000000000..9939a5ab3
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/CONTRIBUTING.md
@@ -0,0 +1,32 @@
+# How to contribute to lego
+
+Contributions in the form of patches and proposals are essential to keep lego great and to make it even better.
+To ensure a great and easy experience for everyone, please review the few guidelines in this document.
+
+## Bug reports
+
+- Use the issue search to see if the issue has already been reported.
+- Also look for closed issues to see if your issue has already been fixed.
+- If both of the above do not apply create a new issue and include as much information as possible.
+
+Bug reports should include all information a person could need to reproduce your problem without the need to
+follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour.
+
+## Feature proposals and requests
+
+Feature requests are welcome and should be discussed in an issue.
+Please keep proposals focused on one thing at a time and be as detailed as possible.
+It is up to you to make a strong point about your proposal and convince us of the merits and the added complexity of this feature.
+
+## Pull requests
+
+Patches, new features and improvements are a great way to help the project.
+Please keep them focused on one thing and do not include unrelated commits.
+
+All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests.
+
+If you want to contribute a significant pull request (with a non-trivial workload for you) please **ask first**. We do not want you to spend
+a lot of time on something the project's developers might not want to merge into the project.
+
+**IMPORTANT**: By submitting a patch, you agree to allow the project
+owners to license your work under the terms of the [MIT License](LICENSE).
diff --git a/vendor/github.com/xenolf/lego/Dockerfile b/vendor/github.com/xenolf/lego/Dockerfile
new file mode 100644
index 000000000..3749dfcee
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/Dockerfile
@@ -0,0 +1,14 @@
+FROM alpine:3.4
+
+ENV GOPATH /go
+
+RUN apk update && apk add ca-certificates go git && \
+ rm -rf /var/cache/apk/* && \
+ go get -u github.com/xenolf/lego && \
+ cd /go/src/github.com/xenolf/lego && \
+ go build -o /usr/bin/lego . && \
+ apk del ca-certificates go git && \
+ rm -rf /var/cache/apk/* && \
+ rm -rf /go
+
+ENTRYPOINT [ "/usr/bin/lego" ]
diff --git a/vendor/github.com/xenolf/lego/LICENSE b/vendor/github.com/xenolf/lego/LICENSE
new file mode 100644
index 000000000..17460b716
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Sebastian Erhart
+
+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/xenolf/lego/README.md b/vendor/github.com/xenolf/lego/README.md
new file mode 100644
index 000000000..136bc5548
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/README.md
@@ -0,0 +1,257 @@
+# lego
+Let's Encrypt client and ACME library written in Go
+
+[![GoDoc](https://godoc.org/github.com/xenolf/lego/acme?status.svg)](https://godoc.org/github.com/xenolf/lego/acme)
+[![Build Status](https://travis-ci.org/xenolf/lego.svg?branch=master)](https://travis-ci.org/xenolf/lego)
+[![Dev Chat](https://img.shields.io/badge/dev%20chat-gitter-blue.svg?label=dev+chat)](https://gitter.im/xenolf/lego)
+
+#### General
+This is a work in progress. Please do *NOT* run this on a production server and please report any bugs you find!
+
+#### Installation
+lego supports both binary installs and install from source.
+
+To get the binary just download the latest release for your OS/Arch from [the release page](https://github.com/xenolf/lego/releases)
+and put the binary somewhere convenient. lego does not assume anything about the location you run it from.
+
+To install from source, just run
+```
+go get -u github.com/xenolf/lego
+```
+
+To build lego inside a Docker container, just run
+```
+docker build -t lego .
+```
+
+#### Features
+
+- Register with CA
+- Obtain certificates, both from scratch or with an existing CSR
+- Renew certificates
+- Revoke certificates
+- Robust implementation of all ACME challenges
+ - HTTP (http-01)
+ - TLS with Server Name Indication (tls-sni-01)
+ - DNS (dns-01)
+- SAN certificate support
+- Comes with multiple optional [DNS providers](https://github.com/xenolf/lego/tree/master/providers/dns)
+- [Custom challenge solvers](https://github.com/xenolf/lego/wiki/Writing-a-Challenge-Solver)
+- Certificate bundling
+- OCSP helper function
+
+Please keep in mind that CLI switches and APIs are still subject to change.
+
+When using the standard `--path` option, all certificates and account configurations are saved to a folder *.lego* in the current working directory.
+
+#### Sudo
+The CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges.
+To run the CLI without sudo, you have four options:
+
+- Use setcap 'cap_net_bind_service=+ep' /path/to/program
+- Pass the `--http` or/and the `--tls` option and specify a custom port to bind to. In this case you have to forward port 80/443 to these custom ports (see [Port Usage](#port-usage)).
+- Pass the `--webroot` option and specify the path to your webroot folder. In this case the challenge will be written in a file in `.well-known/acme-challenge/` inside your webroot.
+- Pass the `--dns` option and specify a DNS provider.
+
+#### Port Usage
+By default lego assumes it is able to bind to ports 80 and 443 to solve challenges.
+If this is not possible in your environment, you can use the `--http` and `--tls` options to instruct
+lego to listen on that interface:port for any incoming challenges.
+
+If you are using this option, make sure you proxy all of the following traffic to these ports.
+
+HTTP Port:
+- All plaintext HTTP requests to port 80 which begin with a request path of `/.well-known/acme-challenge/` for the HTTP challenge.
+
+TLS Port:
+- All TLS handshakes on port 443 for the TLS-SNI challenge.
+
+This traffic redirection is only needed as long as lego solves challenges. As soon as you have received your certificates you can deactivate the forwarding.
+
+#### Usage
+
+```
+NAME:
+ lego - Let's Encrypt client written in Go
+
+USAGE:
+ lego [global options] command [command options] [arguments...]
+
+VERSION:
+ 0.3.1
+
+COMMANDS:
+ run Register an account, then create and install a certificate
+ revoke Revoke a certificate
+ renew Renew a certificate
+ dnshelp Shows additional help for the --dns global option
+ help, h Shows a list of commands or help for one command
+
+GLOBAL OPTIONS:
+ --domains, -d [--domains option --domains option] Add domains to the process
+ --csr, -c Certificate signing request filename, if an external CSR is to be used
+ --server, -s "https://acme-v01.api.letsencrypt.org/directory" CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.
+ --email, -m Email used for registration and recovery contact.
+ --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
+ --key-type, -k "rsa2048" Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384
+ --path "${CWD}/.lego" Directory to use for storing the data
+ --exclude, -x [--exclude option --exclude option] Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01".
+ --webroot Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge
+ --http Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port
+ --tls Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port
+ --dns Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.
+ --help, -h show help
+ --version, -v print the version
+```
+
+##### CLI Example
+
+Assumes the `lego` binary has permission to bind to ports 80 and 443. You can get a pre-built binary from the [releases](https://github.com/xenolf/lego/releases) page.
+If your environment does not allow you to bind to these ports, please read [Port Usage](#port-usage).
+
+Obtain a certificate:
+
+```bash
+$ lego --email="foo@bar.com" --domains="example.com" run
+```
+
+(Find your certificate in the `.lego` folder of current working directory.)
+
+To renew the certificate:
+
+```bash
+$ lego --email="foo@bar.com" --domains="example.com" renew
+```
+
+Obtain a certificate using the DNS challenge and AWS Route 53:
+
+```bash
+$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run
+```
+
+Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead.
+
+Obtain a certificate given a certificate signing request (CSR) generated by something else:
+
+```bash
+$ lego --email="foo@bar.com" --csr=/path/to/csr.pem run
+```
+
+(lego will infer the domains to be validated based on the contents of the CSR, so make sure the CSR's Common Name and optional SubjectAltNames are set correctly.)
+
+lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead:
+
+```bash
+$ lego --server=https://acme-staging.api.letsencrypt.org/directory …
+```
+
+#### DNS Challenge API Details
+
+##### AWS Route 53
+
+The following AWS IAM policy document describes the permissions required for lego to complete the DNS challenge.
+Replace `<INSERT_YOUR_HOSTED_ZONE_ID_HERE>` with the Route 53 zone ID of the domain you are authorizing.
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "route53:GetChange",
+ "route53:ListHostedZonesByName"
+ ],
+ "Resource": [
+ "*"
+ ]
+ },
+ {
+ "Effect": "Allow",
+ "Action": [
+ "route53:ChangeResourceRecordSets"
+ ],
+ "Resource": [
+ "arn:aws:route53:::hostedzone/<INSERT_YOUR_HOSTED_ZONE_ID_HERE>"
+ ]
+ }
+ ]
+}
+```
+
+#### ACME Library Usage
+
+A valid, but bare-bones example use of the acme package:
+
+```go
+// You'll need a user or account type that implements acme.User
+type MyUser struct {
+ Email string
+ Registration *acme.RegistrationResource
+ key crypto.PrivateKey
+}
+func (u MyUser) GetEmail() string {
+ return u.Email
+}
+func (u MyUser) GetRegistration() *acme.RegistrationResource {
+ return u.Registration
+}
+func (u MyUser) GetPrivateKey() crypto.PrivateKey {
+ return u.key
+}
+
+// Create a user. New accounts need an email and private key to start.
+const rsaKeySize = 2048
+privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
+if err != nil {
+ log.Fatal(err)
+}
+myUser := MyUser{
+ Email: "you@yours.com",
+ key: privateKey,
+}
+
+// A client facilitates communication with the CA server. This CA URL is
+// configured for a local dev instance of Boulder running in Docker in a VM.
+client, err := acme.NewClient("http://192.168.99.100:4000", &myUser, acme.RSA2048)
+if err != nil {
+ log.Fatal(err)
+}
+
+// We specify an http port of 5002 and an tls port of 5001 on all interfaces
+// because we aren't running as root and can't bind a listener to port 80 and 443
+// (used later when we attempt to pass challenges). Keep in mind that we still
+// need to proxy challenge traffic to port 5002 and 5001.
+client.SetHTTPAddress(":5002")
+client.SetTLSAddress(":5001")
+
+// New users will need to register
+reg, err := client.Register()
+if err != nil {
+ log.Fatal(err)
+}
+myUser.Registration = reg
+
+// SAVE THE USER.
+
+// The client has a URL to the current Let's Encrypt Subscriber
+// Agreement. The user will need to agree to it.
+err = client.AgreeToTOS()
+if err != nil {
+ log.Fatal(err)
+}
+
+// The acme library takes care of completing the challenges to obtain the certificate(s).
+// The domains must resolve to this machine or you have to use the DNS challenge.
+bundle := false
+certificates, failures := client.ObtainCertificate([]string{"mydomain.com"}, bundle, nil)
+if len(failures) > 0 {
+ log.Fatal(failures)
+}
+
+// Each certificate comes back with the cert bytes, the bytes of the client's
+// private key, and a certificate URL. SAVE THESE TO DISK.
+fmt.Printf("%#v\n", certificates)
+
+// ... all done.
+```
diff --git a/vendor/github.com/xenolf/lego/account.go b/vendor/github.com/xenolf/lego/account.go
new file mode 100644
index 000000000..34856e16f
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/account.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "crypto"
+ "encoding/json"
+ "io/ioutil"
+ "os"
+ "path"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// Account represents a users local saved credentials
+type Account struct {
+ Email string `json:"email"`
+ key crypto.PrivateKey
+ Registration *acme.RegistrationResource `json:"registration"`
+
+ conf *Configuration
+}
+
+// NewAccount creates a new account for an email address
+func NewAccount(email string, conf *Configuration) *Account {
+ accKeysPath := conf.AccountKeysPath(email)
+ // TODO: move to function in configuration?
+ accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key"
+ if err := checkFolder(accKeysPath); err != nil {
+ logger().Fatalf("Could not check/create directory for account %s: %v", email, err)
+ }
+
+ var privKey crypto.PrivateKey
+ if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
+
+ logger().Printf("No key found for account %s. Generating a curve P384 EC key.", email)
+ privKey, err = generatePrivateKey(accKeyPath)
+ if err != nil {
+ logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err)
+ }
+
+ logger().Printf("Saved key to %s", accKeyPath)
+ } else {
+ privKey, err = loadPrivateKey(accKeyPath)
+ if err != nil {
+ logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err)
+ }
+ }
+
+ accountFile := path.Join(conf.AccountPath(email), "account.json")
+ if _, err := os.Stat(accountFile); os.IsNotExist(err) {
+ return &Account{Email: email, key: privKey, conf: conf}
+ }
+
+ fileBytes, err := ioutil.ReadFile(accountFile)
+ if err != nil {
+ logger().Fatalf("Could not load file for account %s -> %v", email, err)
+ }
+
+ var acc Account
+ err = json.Unmarshal(fileBytes, &acc)
+ if err != nil {
+ logger().Fatalf("Could not parse file for account %s -> %v", email, err)
+ }
+
+ acc.key = privKey
+ acc.conf = conf
+
+ if acc.Registration == nil {
+ logger().Fatalf("Could not load account for %s. Registration is nil.", email)
+ }
+
+ if acc.conf == nil {
+ logger().Fatalf("Could not load account for %s. Configuration is nil.", email)
+ }
+
+ return &acc
+}
+
+/** Implementation of the acme.User interface **/
+
+// GetEmail returns the email address for the account
+func (a *Account) GetEmail() string {
+ return a.Email
+}
+
+// GetPrivateKey returns the private RSA account key.
+func (a *Account) GetPrivateKey() crypto.PrivateKey {
+ return a.key
+}
+
+// GetRegistration returns the server registration
+func (a *Account) GetRegistration() *acme.RegistrationResource {
+ return a.Registration
+}
+
+/** End **/
+
+// Save the account to disk
+func (a *Account) Save() error {
+ jsonBytes, err := json.MarshalIndent(a, "", "\t")
+ if err != nil {
+ return err
+ }
+
+ return ioutil.WriteFile(
+ path.Join(a.conf.AccountPath(a.Email), "account.json"),
+ jsonBytes,
+ 0600,
+ )
+}
diff --git a/vendor/github.com/xenolf/lego/acme/challenges.go b/vendor/github.com/xenolf/lego/acme/challenges.go
new file mode 100644
index 000000000..857900507
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/challenges.go
@@ -0,0 +1,16 @@
+package acme
+
+// Challenge is a string that identifies a particular type and version of ACME challenge.
+type Challenge string
+
+const (
+ // HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
+ // Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
+ HTTP01 = Challenge("http-01")
+ // TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni
+ // Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge
+ TLSSNI01 = Challenge("tls-sni-01")
+ // DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
+ // Note: DNS01Record returns a DNS record which will fulfill this challenge
+ DNS01 = Challenge("dns-01")
+)
diff --git a/vendor/github.com/xenolf/lego/acme/client.go b/vendor/github.com/xenolf/lego/acme/client.go
new file mode 100644
index 000000000..5eae8d26a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/client.go
@@ -0,0 +1,804 @@
+// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers.
+package acme
+
+import (
+ "crypto"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var (
+ // Logger is an optional custom logger.
+ Logger *log.Logger
+)
+
+// logf writes a log entry. It uses Logger if not
+// nil, otherwise it uses the default log.Logger.
+func logf(format string, args ...interface{}) {
+ if Logger != nil {
+ Logger.Printf(format, args...)
+ } else {
+ log.Printf(format, args...)
+ }
+}
+
+// User interface is to be implemented by users of this library.
+// It is used by the client type to get user specific information.
+type User interface {
+ GetEmail() string
+ GetRegistration() *RegistrationResource
+ GetPrivateKey() crypto.PrivateKey
+}
+
+// Interface for all challenge solvers to implement.
+type solver interface {
+ Solve(challenge challenge, domain string) error
+}
+
+type validateFunc func(j *jws, domain, uri string, chlng challenge) error
+
+// Client is the user-friendy way to ACME
+type Client struct {
+ directory directory
+ user User
+ jws *jws
+ keyType KeyType
+ issuerCert []byte
+ solvers map[Challenge]solver
+}
+
+// NewClient creates a new ACME client on behalf of the user. The client will depend on
+// the ACME directory located at caDirURL for the rest of its actions. A private
+// key of type keyType (see KeyType contants) will be generated when requesting a new
+// certificate if one isn't provided.
+func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) {
+ privKey := user.GetPrivateKey()
+ if privKey == nil {
+ return nil, errors.New("private key was nil")
+ }
+
+ var dir directory
+ if _, err := getJSON(caDirURL, &dir); err != nil {
+ return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
+ }
+
+ if dir.NewRegURL == "" {
+ return nil, errors.New("directory missing new registration URL")
+ }
+ if dir.NewAuthzURL == "" {
+ return nil, errors.New("directory missing new authz URL")
+ }
+ if dir.NewCertURL == "" {
+ return nil, errors.New("directory missing new certificate URL")
+ }
+ if dir.RevokeCertURL == "" {
+ return nil, errors.New("directory missing revoke certificate URL")
+ }
+
+ jws := &jws{privKey: privKey, directoryURL: caDirURL}
+
+ // REVIEW: best possibility?
+ // Add all available solvers with the right index as per ACME
+ // spec to this map. Otherwise they won`t be found.
+ solvers := make(map[Challenge]solver)
+ solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
+ solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}}
+
+ return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
+}
+
+// SetChallengeProvider specifies a custom provider that will make the solution available
+func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error {
+ switch challenge {
+ case HTTP01:
+ c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
+ case TLSSNI01:
+ c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p}
+ case DNS01:
+ c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
+ default:
+ return fmt.Errorf("Unknown challenge %v", challenge)
+ }
+ return nil
+}
+
+// SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges.
+// If this option is not used, the default port 80 and all interfaces will be used.
+// To only specify a port and no interface use the ":port" notation.
+func (c *Client) SetHTTPAddress(iface string) error {
+ host, port, err := net.SplitHostPort(iface)
+ if err != nil {
+ return err
+ }
+
+ if chlng, ok := c.solvers[HTTP01]; ok {
+ chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port)
+ }
+
+ return nil
+}
+
+// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges.
+// If this option is not used, the default port 443 and all interfaces will be used.
+// To only specify a port and no interface use the ":port" notation.
+func (c *Client) SetTLSAddress(iface string) error {
+ host, port, err := net.SplitHostPort(iface)
+ if err != nil {
+ return err
+ }
+
+ if chlng, ok := c.solvers[TLSSNI01]; ok {
+ chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port)
+ }
+ return nil
+}
+
+// ExcludeChallenges explicitly removes challenges from the pool for solving.
+func (c *Client) ExcludeChallenges(challenges []Challenge) {
+ // Loop through all challenges and delete the requested one if found.
+ for _, challenge := range challenges {
+ delete(c.solvers, challenge)
+ }
+}
+
+// Register the current account to the ACME server.
+func (c *Client) Register() (*RegistrationResource, error) {
+ if c == nil || c.user == nil {
+ return nil, errors.New("acme: cannot register a nil client or user")
+ }
+ logf("[INFO] acme: Registering account for %s", c.user.GetEmail())
+
+ regMsg := registrationMessage{
+ Resource: "new-reg",
+ }
+ if c.user.GetEmail() != "" {
+ regMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
+ } else {
+ regMsg.Contact = []string{}
+ }
+
+ var serverReg Registration
+ var regURI string
+ hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg)
+ if err != nil {
+ remoteErr, ok := err.(RemoteError)
+ if ok && remoteErr.StatusCode == 409 {
+ regURI = hdr.Get("Location")
+ regMsg = registrationMessage{
+ Resource: "reg",
+ }
+ if hdr, err = postJSON(c.jws, regURI, regMsg, &serverReg); err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, err
+ }
+ }
+
+ reg := &RegistrationResource{Body: serverReg}
+
+ links := parseLinks(hdr["Link"])
+
+ if regURI == "" {
+ regURI = hdr.Get("Location")
+ }
+ reg.URI = regURI
+ if links["terms-of-service"] != "" {
+ reg.TosURL = links["terms-of-service"]
+ }
+
+ if links["next"] != "" {
+ reg.NewAuthzURL = links["next"]
+ } else {
+ return nil, errors.New("acme: The server did not return 'next' link to proceed")
+ }
+
+ return reg, nil
+}
+
+// DeleteRegistration deletes the client's user registration from the ACME
+// server.
+func (c *Client) DeleteRegistration() error {
+ if c == nil || c.user == nil {
+ return errors.New("acme: cannot unregister a nil client or user")
+ }
+ logf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
+
+ regMsg := registrationMessage{
+ Resource: "reg",
+ Delete: true,
+ }
+
+ _, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// QueryRegistration runs a POST request on the client's registration and
+// returns the result.
+//
+// This is similar to the Register function, but acting on an existing
+// registration link and resource.
+func (c *Client) QueryRegistration() (*RegistrationResource, error) {
+ if c == nil || c.user == nil {
+ return nil, errors.New("acme: cannot query the registration of a nil client or user")
+ }
+ // Log the URL here instead of the email as the email may not be set
+ logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
+
+ regMsg := registrationMessage{
+ Resource: "reg",
+ }
+
+ var serverReg Registration
+ hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg)
+ if err != nil {
+ return nil, err
+ }
+
+ reg := &RegistrationResource{Body: serverReg}
+
+ links := parseLinks(hdr["Link"])
+ // Location: header is not returned so this needs to be populated off of
+ // existing URI
+ reg.URI = c.user.GetRegistration().URI
+ if links["terms-of-service"] != "" {
+ reg.TosURL = links["terms-of-service"]
+ }
+
+ if links["next"] != "" {
+ reg.NewAuthzURL = links["next"]
+ } else {
+ return nil, errors.New("acme: No new-authz link in response to registration query")
+ }
+
+ return reg, nil
+}
+
+// AgreeToTOS updates the Client registration and sends the agreement to
+// the server.
+func (c *Client) AgreeToTOS() error {
+ reg := c.user.GetRegistration()
+
+ reg.Body.Agreement = c.user.GetRegistration().TosURL
+ reg.Body.Resource = "reg"
+ _, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
+ return err
+}
+
+// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it.
+// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key
+// for this CSR is not required.
+// If bundle is true, the []byte contains both the issuer certificate and
+// your issued certificate as a bundle.
+// This function will never return a partial certificate. If one domain in the list fails,
+// the whole certificate will fail.
+func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) {
+ // figure out what domains it concerns
+ // start with the common name
+ domains := []string{csr.Subject.CommonName}
+
+ // loop over the SubjectAltName DNS names
+DNSNames:
+ for _, sanName := range csr.DNSNames {
+ for _, existingName := range domains {
+ if existingName == sanName {
+ // duplicate; skip this name
+ continue DNSNames
+ }
+ }
+
+ // name is unique
+ domains = append(domains, sanName)
+ }
+
+ if bundle {
+ logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
+ } else {
+ logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
+ }
+
+ challenges, failures := c.getChallenges(domains)
+ // If any challenge fails - return. Do not generate partial SAN certificates.
+ if len(failures) > 0 {
+ return CertificateResource{}, failures
+ }
+
+ errs := c.solveChallenges(challenges)
+ // If any challenge fails - return. Do not generate partial SAN certificates.
+ if len(errs) > 0 {
+ return CertificateResource{}, errs
+ }
+
+ logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
+
+ cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil)
+ if err != nil {
+ for _, chln := range challenges {
+ failures[chln.Domain] = err
+ }
+ }
+
+ // Add the CSR to the certificate so that it can be used for renewals.
+ cert.CSR = pemEncode(&csr)
+
+ return cert, failures
+}
+
+// ObtainCertificate tries to obtain a single certificate using all domains passed into it.
+// The first domain in domains is used for the CommonName field of the certificate, all other
+// domains are added using the Subject Alternate Names extension. A new private key is generated
+// for every invocation of this function. If you do not want that you can supply your own private key
+// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one.
+// If bundle is true, the []byte contains both the issuer certificate and
+// your issued certificate as a bundle.
+// This function will never return a partial certificate. If one domain in the list fails,
+// the whole certificate will fail.
+func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey) (CertificateResource, map[string]error) {
+ if bundle {
+ logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
+ } else {
+ logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
+ }
+
+ challenges, failures := c.getChallenges(domains)
+ // If any challenge fails - return. Do not generate partial SAN certificates.
+ if len(failures) > 0 {
+ return CertificateResource{}, failures
+ }
+
+ errs := c.solveChallenges(challenges)
+ // If any challenge fails - return. Do not generate partial SAN certificates.
+ if len(errs) > 0 {
+ return CertificateResource{}, errs
+ }
+
+ logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
+
+ cert, err := c.requestCertificate(challenges, bundle, privKey)
+ if err != nil {
+ for _, chln := range challenges {
+ failures[chln.Domain] = err
+ }
+ }
+
+ return cert, failures
+}
+
+// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
+func (c *Client) RevokeCertificate(certificate []byte) error {
+ certificates, err := parsePEMBundle(certificate)
+ if err != nil {
+ return err
+ }
+
+ x509Cert := certificates[0]
+ if x509Cert.IsCA {
+ return fmt.Errorf("Certificate bundle starts with a CA certificate")
+ }
+
+ encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
+
+ _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil)
+ return err
+}
+
+// RenewCertificate takes a CertificateResource and tries to renew the certificate.
+// If the renewal process succeeds, the new certificate will ge returned in a new CertResource.
+// Please be aware that this function will return a new certificate in ANY case that is not an error.
+// If the server does not provide us with a new cert on a GET request to the CertURL
+// this function will start a new-cert flow where a new certificate gets generated.
+// If bundle is true, the []byte contains both the issuer certificate and
+// your issued certificate as a bundle.
+// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil.
+func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (CertificateResource, error) {
+ // Input certificate is PEM encoded. Decode it here as we may need the decoded
+ // cert later on in the renewal process. The input may be a bundle or a single certificate.
+ certificates, err := parsePEMBundle(cert.Certificate)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ x509Cert := certificates[0]
+ if x509Cert.IsCA {
+ return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
+ }
+
+ // This is just meant to be informal for the user.
+ timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
+ logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
+
+ // The first step of renewal is to check if we get a renewed cert
+ // directly from the cert URL.
+ resp, err := httpGet(cert.CertURL)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ defer resp.Body.Close()
+ serverCertBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ serverCert, err := x509.ParseCertificate(serverCertBytes)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ // If the server responds with a different certificate we are effectively renewed.
+ // TODO: Further test if we can actually use the new certificate (Our private key works)
+ if !x509Cert.Equal(serverCert) {
+ logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain)
+ issuedCert := pemEncode(derCertificateBytes(serverCertBytes))
+ // If bundle is true, we want to return a certificate bundle.
+ // To do this, we need the issuer certificate.
+ if bundle {
+ // The issuer certificate link is always supplied via an "up" link
+ // in the response headers of a new certificate.
+ links := parseLinks(resp.Header["Link"])
+ issuerCert, err := c.getIssuerCertificate(links["up"])
+ if err != nil {
+ // If we fail to acquire the issuer cert, return the issued certificate - do not fail.
+ logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err)
+ } else {
+ // Success - append the issuer cert to the issued cert.
+ issuerCert = pemEncode(derCertificateBytes(issuerCert))
+ issuedCert = append(issuedCert, issuerCert...)
+ }
+ }
+
+ cert.Certificate = issuedCert
+ return cert, nil
+ }
+
+ // If the certificate is the same, then we need to request a new certificate.
+ // Start by checking to see if the certificate was based off a CSR, and
+ // use that if it's defined.
+ if len(cert.CSR) > 0 {
+ csr, err := pemDecodeTox509CSR(cert.CSR)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
+ return newCert, failures[cert.Domain]
+ }
+
+ var privKey crypto.PrivateKey
+ if cert.PrivateKey != nil {
+ privKey, err = parsePEMPrivateKey(cert.PrivateKey)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ }
+
+ var domains []string
+ var failures map[string]error
+ // check for SAN certificate
+ if len(x509Cert.DNSNames) > 1 {
+ domains = append(domains, x509Cert.Subject.CommonName)
+ for _, sanDomain := range x509Cert.DNSNames {
+ if sanDomain == x509Cert.Subject.CommonName {
+ continue
+ }
+ domains = append(domains, sanDomain)
+ }
+ } else {
+ domains = append(domains, x509Cert.Subject.CommonName)
+ }
+
+ newCert, failures := c.ObtainCertificate(domains, bundle, privKey)
+ return newCert, failures[cert.Domain]
+}
+
+// Looks through the challenge combinations to find a solvable match.
+// Then solves the challenges in series and returns.
+func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error {
+ // loop through the resources, basically through the domains.
+ failures := make(map[string]error)
+ for _, authz := range challenges {
+ if authz.Body.Status == "valid" {
+ // Boulder might recycle recent validated authz (see issue #267)
+ logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain)
+ continue
+ }
+ // no solvers - no solving
+ if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
+ for i, solver := range solvers {
+ // TODO: do not immediately fail if one domain fails to validate.
+ err := solver.Solve(authz.Body.Challenges[i], authz.Domain)
+ if err != nil {
+ failures[authz.Domain] = err
+ }
+ }
+ } else {
+ failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain)
+ }
+ }
+
+ return failures
+}
+
+// Checks all combinations from the server and returns an array of
+// solvers which should get executed in series.
+func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver {
+ for _, combination := range auth.Combinations {
+ solvers := make(map[int]solver)
+ for _, idx := range combination {
+ if solver, ok := c.solvers[auth.Challenges[idx].Type]; ok {
+ solvers[idx] = solver
+ } else {
+ logf("[INFO][%s] acme: Could not find solver for: %s", domain, auth.Challenges[idx].Type)
+ }
+ }
+
+ // If we can solve the whole combination, return the solvers
+ if len(solvers) == len(combination) {
+ return solvers
+ }
+ }
+ return nil
+}
+
+// Get the challenges needed to proof our identifier to the ACME server.
+func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) {
+ resc, errc := make(chan authorizationResource), make(chan domainError)
+
+ for _, domain := range domains {
+ go func(domain string) {
+ authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}}
+ var authz authorization
+ hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz)
+ if err != nil {
+ errc <- domainError{Domain: domain, Error: err}
+ return
+ }
+
+ links := parseLinks(hdr["Link"])
+ if links["next"] == "" {
+ logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
+ return
+ }
+
+ resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain}
+ }(domain)
+ }
+
+ responses := make(map[string]authorizationResource)
+ failures := make(map[string]error)
+ for i := 0; i < len(domains); i++ {
+ select {
+ case res := <-resc:
+ responses[res.Domain] = res
+ case err := <-errc:
+ failures[err.Domain] = err.Error
+ }
+ }
+
+ challenges := make([]authorizationResource, 0, len(responses))
+ for _, domain := range domains {
+ if challenge, ok := responses[domain]; ok {
+ challenges = append(challenges, challenge)
+ }
+ }
+
+ close(resc)
+ close(errc)
+
+ return challenges, failures
+}
+
+func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey) (CertificateResource, error) {
+ if len(authz) == 0 {
+ return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
+ }
+
+ var err error
+ if privKey == nil {
+ privKey, err = generatePrivateKey(c.keyType)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ }
+
+ // determine certificate name(s) based on the authorization resources
+ commonName := authz[0]
+ var san []string
+ for _, auth := range authz[1:] {
+ san = append(san, auth.Domain)
+ }
+
+ // TODO: should the CSR be customizable?
+ csr, err := generateCsr(privKey, commonName.Domain, san)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey))
+}
+
+func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) {
+ commonName := authz[0]
+
+ var authURLs []string
+ for _, auth := range authz[1:] {
+ authURLs = append(authURLs, auth.AuthURL)
+ }
+
+ csrString := base64.URLEncoding.EncodeToString(csr)
+ jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ resp, err := c.jws.post(commonName.NewCertURL, jsonBytes)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ cerRes := CertificateResource{
+ Domain: commonName.Domain,
+ CertURL: resp.Header.Get("Location"),
+ PrivateKey: privateKeyPem}
+
+ for {
+ switch resp.StatusCode {
+ case 201, 202:
+ cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
+ resp.Body.Close()
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ // The server returns a body with a length of zero if the
+ // certificate was not ready at the time this request completed.
+ // Otherwise the body is the certificate.
+ if len(cert) > 0 {
+
+ cerRes.CertStableURL = resp.Header.Get("Content-Location")
+ cerRes.AccountRef = c.user.GetRegistration().URI
+
+ issuedCert := pemEncode(derCertificateBytes(cert))
+ // If bundle is true, we want to return a certificate bundle.
+ // To do this, we need the issuer certificate.
+ if bundle {
+ // The issuer certificate link is always supplied via an "up" link
+ // in the response headers of a new certificate.
+ links := parseLinks(resp.Header["Link"])
+ issuerCert, err := c.getIssuerCertificate(links["up"])
+ if err != nil {
+ // If we fail to acquire the issuer cert, return the issued certificate - do not fail.
+ logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err)
+ } else {
+ // Success - append the issuer cert to the issued cert.
+ issuerCert = pemEncode(derCertificateBytes(issuerCert))
+ issuedCert = append(issuedCert, issuerCert...)
+ }
+ }
+
+ cerRes.Certificate = issuedCert
+ logf("[INFO][%s] Server responded with a certificate.", commonName.Domain)
+ return cerRes, nil
+ }
+
+ // The certificate was granted but is not yet issued.
+ // Check retry-after and loop.
+ ra := resp.Header.Get("Retry-After")
+ retryAfter, err := strconv.Atoi(ra)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+
+ logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter)
+ time.Sleep(time.Duration(retryAfter) * time.Second)
+
+ break
+ default:
+ return CertificateResource{}, handleHTTPError(resp)
+ }
+
+ resp, err = httpGet(cerRes.CertURL)
+ if err != nil {
+ return CertificateResource{}, err
+ }
+ }
+}
+
+// getIssuerCertificate requests the issuer certificate and caches it for
+// subsequent requests.
+func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
+ logf("[INFO] acme: Requesting issuer cert from %s", url)
+ if c.issuerCert != nil {
+ return c.issuerCert, nil
+ }
+
+ resp, err := httpGet(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = x509.ParseCertificate(issuerBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ c.issuerCert = issuerBytes
+ return issuerBytes, err
+}
+
+func parseLinks(links []string) map[string]string {
+ aBrkt := regexp.MustCompile("[<>]")
+ slver := regexp.MustCompile("(.+) *= *\"(.+)\"")
+ linkMap := make(map[string]string)
+
+ for _, link := range links {
+
+ link = aBrkt.ReplaceAllString(link, "")
+ parts := strings.Split(link, ";")
+
+ matches := slver.FindStringSubmatch(parts[1])
+ if len(matches) > 0 {
+ linkMap[matches[2]] = parts[0]
+ }
+ }
+
+ return linkMap
+}
+
+// validate makes the ACME server start validating a
+// challenge response, only returning once it is done.
+func validate(j *jws, domain, uri string, chlng challenge) error {
+ var challengeResponse challenge
+
+ hdr, err := postJSON(j, uri, chlng, &challengeResponse)
+ if err != nil {
+ return err
+ }
+
+ // After the path is sent, the ACME server will access our server.
+ // Repeatedly check the server for an updated status on our request.
+ for {
+ switch challengeResponse.Status {
+ case "valid":
+ logf("[INFO][%s] The server validated our request", domain)
+ return nil
+ case "pending":
+ break
+ case "invalid":
+ return handleChallengeError(challengeResponse)
+ default:
+ return errors.New("The server returned an unexpected state.")
+ }
+
+ ra, err := strconv.Atoi(hdr.Get("Retry-After"))
+ if err != nil {
+ // The ACME server MUST return a Retry-After.
+ // If it doesn't, we'll just poll hard.
+ ra = 1
+ }
+ time.Sleep(time.Duration(ra) * time.Second)
+
+ hdr, err = getJSON(uri, &challengeResponse)
+ if err != nil {
+ return err
+ }
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/client_test.go b/vendor/github.com/xenolf/lego/acme/client_test.go
new file mode 100644
index 000000000..e309554f3
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/client_test.go
@@ -0,0 +1,198 @@
+package acme
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/json"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestNewClient(t *testing.T) {
+ keyBits := 32 // small value keeps test fast
+ keyType := RSA2048
+ key, err := rsa.GenerateKey(rand.Reader, keyBits)
+ if err != nil {
+ t.Fatal("Could not generate test key:", err)
+ }
+ user := mockUser{
+ email: "test@test.com",
+ regres: new(RegistrationResource),
+ privatekey: key,
+ }
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
+ w.Write(data)
+ }))
+
+ client, err := NewClient(ts.URL, user, keyType)
+ if err != nil {
+ t.Fatalf("Could not create client: %v", err)
+ }
+
+ if client.jws == nil {
+ t.Fatalf("Expected client.jws to not be nil")
+ }
+ if expected, actual := key, client.jws.privKey; actual != expected {
+ t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual)
+ }
+
+ if client.keyType != keyType {
+ t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
+ }
+
+ if expected, actual := 2, len(client.solvers); actual != expected {
+ t.Fatalf("Expected %d solver(s), got %d", expected, actual)
+ }
+}
+
+func TestClientOptPort(t *testing.T) {
+ keyBits := 32 // small value keeps test fast
+ key, err := rsa.GenerateKey(rand.Reader, keyBits)
+ if err != nil {
+ t.Fatal("Could not generate test key:", err)
+ }
+ user := mockUser{
+ email: "test@test.com",
+ regres: new(RegistrationResource),
+ privatekey: key,
+ }
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
+ w.Write(data)
+ }))
+
+ optPort := "1234"
+ optHost := ""
+ client, err := NewClient(ts.URL, user, RSA2048)
+ if err != nil {
+ t.Fatalf("Could not create client: %v", err)
+ }
+ client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
+ client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
+
+ httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
+ if !ok {
+ t.Fatal("Expected http-01 solver to be httpChallenge type")
+ }
+ if httpSolver.jws != client.jws {
+ t.Error("Expected http-01 to have same jws as client")
+ }
+ if got := httpSolver.provider.(*HTTPProviderServer).port; got != optPort {
+ t.Errorf("Expected http-01 to have port %s but was %s", optPort, got)
+ }
+ if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
+ t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
+ }
+
+ httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge)
+ if !ok {
+ t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
+ }
+ if httpsSolver.jws != client.jws {
+ t.Error("Expected tls-sni-01 to have same jws as client")
+ }
+ if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
+ t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
+ }
+ if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost {
+ t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
+ }
+
+ // test setting different host
+ optHost = "127.0.0.1"
+ client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
+ client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
+
+ if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
+ t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
+ }
+ if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
+ t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
+ }
+}
+
+func TestValidate(t *testing.T) {
+ var statuses []string
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Minimal stub ACME server for validation.
+ w.Header().Add("Replay-Nonce", "12345")
+ w.Header().Add("Retry-After", "0")
+ switch r.Method {
+ case "HEAD":
+ case "POST":
+ st := statuses[0]
+ statuses = statuses[1:]
+ writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
+
+ case "GET":
+ st := statuses[0]
+ statuses = statuses[1:]
+ writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
+
+ default:
+ http.Error(w, r.Method, http.StatusMethodNotAllowed)
+ }
+ }))
+ defer ts.Close()
+
+ privKey, _ := rsa.GenerateKey(rand.Reader, 512)
+ j := &jws{privKey: privKey, directoryURL: ts.URL}
+
+ tsts := []struct {
+ name string
+ statuses []string
+ want string
+ }{
+ {"POST-unexpected", []string{"weird"}, "unexpected"},
+ {"POST-valid", []string{"valid"}, ""},
+ {"POST-invalid", []string{"invalid"}, "Error Detail"},
+ {"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
+ {"GET-valid", []string{"pending", "valid"}, ""},
+ {"GET-invalid", []string{"pending", "invalid"}, "Error Detail"},
+ }
+
+ for _, tst := range tsts {
+ statuses = tst.statuses
+ if err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"}); err == nil && tst.want != "" {
+ t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
+ } else if err != nil && !strings.Contains(err.Error(), tst.want) {
+ t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
+ }
+ }
+}
+
+// writeJSONResponse marshals the body as JSON and writes it to the response.
+func writeJSONResponse(w http.ResponseWriter, body interface{}) {
+ bs, err := json.Marshal(body)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ if _, err := w.Write(bs); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+// stubValidate is like validate, except it does nothing.
+func stubValidate(j *jws, domain, uri string, chlng challenge) error {
+ return nil
+}
+
+type mockUser struct {
+ email string
+ regres *RegistrationResource
+ privatekey *rsa.PrivateKey
+}
+
+func (u mockUser) GetEmail() string { return u.email }
+func (u mockUser) GetRegistration() *RegistrationResource { return u.regres }
+func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey }
diff --git a/vendor/github.com/xenolf/lego/acme/crypto.go b/vendor/github.com/xenolf/lego/acme/crypto.go
new file mode 100644
index 000000000..af97f5d1e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/crypto.go
@@ -0,0 +1,332 @@
+package acme
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "math/big"
+ "net/http"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/ocsp"
+)
+
+// KeyType represents the key algo as well as the key size or curve to use.
+type KeyType string
+type derCertificateBytes []byte
+
+// Constants for all key types we support.
+const (
+ EC256 = KeyType("P256")
+ EC384 = KeyType("P384")
+ RSA2048 = KeyType("2048")
+ RSA4096 = KeyType("4096")
+ RSA8192 = KeyType("8192")
+)
+
+const (
+ // OCSPGood means that the certificate is valid.
+ OCSPGood = ocsp.Good
+ // OCSPRevoked means that the certificate has been deliberately revoked.
+ OCSPRevoked = ocsp.Revoked
+ // OCSPUnknown means that the OCSP responder doesn't know about the certificate.
+ OCSPUnknown = ocsp.Unknown
+ // OCSPServerFailed means that the OCSP responder failed to process the request.
+ OCSPServerFailed = ocsp.ServerFailed
+)
+
+// GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response,
+// the parsed response, and an error, if any. The returned []byte can be passed directly
+// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the
+// issued certificate, this function will try to get the issuer certificate from the
+// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return
+// values are nil, the OCSP status may be assumed OCSPUnknown.
+func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
+ certificates, err := parsePEMBundle(bundle)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // We expect the certificate slice to be ordered downwards the chain.
+ // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
+ // which should always be the first two certificates. If there's no
+ // OCSP server listed in the leaf cert, there's nothing to do. And if
+ // we have only one certificate so far, we need to get the issuer cert.
+ issuedCert := certificates[0]
+ if len(issuedCert.OCSPServer) == 0 {
+ return nil, nil, errors.New("no OCSP server specified in cert")
+ }
+ if len(certificates) == 1 {
+ // TODO: build fallback. If this fails, check the remaining array entries.
+ if len(issuedCert.IssuingCertificateURL) == 0 {
+ return nil, nil, errors.New("no issuing certificate URL")
+ }
+
+ resp, err := httpGet(issuedCert.IssuingCertificateURL[0])
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ issuerCert, err := x509.ParseCertificate(issuerBytes)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Insert it into the slice on position 0
+ // We want it ordered right SRV CRT -> CA
+ certificates = append(certificates, issuerCert)
+ }
+ issuerCert := certificates[1]
+
+ // Finally kick off the OCSP request.
+ ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ reader := bytes.NewReader(ocspReq)
+ req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer req.Body.Close()
+
+ ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024))
+ ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return ocspResBytes, ocspRes, nil
+}
+
+func getKeyAuthorization(token string, key interface{}) (string, error) {
+ var publicKey crypto.PublicKey
+ switch k := key.(type) {
+ case *ecdsa.PrivateKey:
+ publicKey = k.Public()
+ case *rsa.PrivateKey:
+ publicKey = k.Public()
+ }
+
+ // Generate the Key Authorization for the challenge
+ jwk := keyAsJWK(publicKey)
+ if jwk == nil {
+ return "", errors.New("Could not generate JWK from key.")
+ }
+ thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
+ if err != nil {
+ return "", err
+ }
+
+ // unpad the base64URL
+ keyThumb := base64.URLEncoding.EncodeToString(thumbBytes)
+ index := strings.Index(keyThumb, "=")
+ if index != -1 {
+ keyThumb = keyThumb[:index]
+ }
+
+ return token + "." + keyThumb, nil
+}
+
+// parsePEMBundle parses a certificate bundle from top to bottom and returns
+// a slice of x509 certificates. This function will error if no certificates are found.
+func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
+ var certificates []*x509.Certificate
+ var certDERBlock *pem.Block
+
+ for {
+ certDERBlock, bundle = pem.Decode(bundle)
+ if certDERBlock == nil {
+ break
+ }
+
+ if certDERBlock.Type == "CERTIFICATE" {
+ cert, err := x509.ParseCertificate(certDERBlock.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ certificates = append(certificates, cert)
+ }
+ }
+
+ if len(certificates) == 0 {
+ return nil, errors.New("No certificates were found while parsing the bundle.")
+ }
+
+ return certificates, nil
+}
+
+func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
+ keyBlock, _ := pem.Decode(key)
+
+ switch keyBlock.Type {
+ case "RSA PRIVATE KEY":
+ return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
+ case "EC PRIVATE KEY":
+ return x509.ParseECPrivateKey(keyBlock.Bytes)
+ default:
+ return nil, errors.New("Unknown PEM header value")
+ }
+}
+
+func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
+
+ switch keyType {
+ case EC256:
+ return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ case EC384:
+ return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+ case RSA2048:
+ return rsa.GenerateKey(rand.Reader, 2048)
+ case RSA4096:
+ return rsa.GenerateKey(rand.Reader, 4096)
+ case RSA8192:
+ return rsa.GenerateKey(rand.Reader, 8192)
+ }
+
+ return nil, fmt.Errorf("Invalid KeyType: %s", keyType)
+}
+
+func generateCsr(privateKey crypto.PrivateKey, domain string, san []string) ([]byte, error) {
+ template := x509.CertificateRequest{
+ Subject: pkix.Name{
+ CommonName: domain,
+ },
+ }
+
+ if len(san) > 0 {
+ template.DNSNames = san
+ }
+
+ return x509.CreateCertificateRequest(rand.Reader, &template, privateKey)
+}
+
+func pemEncode(data interface{}) []byte {
+ var pemBlock *pem.Block
+ switch key := data.(type) {
+ case *ecdsa.PrivateKey:
+ keyBytes, _ := x509.MarshalECPrivateKey(key)
+ pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
+ case *rsa.PrivateKey:
+ pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
+ break
+ case *x509.CertificateRequest:
+ pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
+ break
+ case derCertificateBytes:
+ pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
+ }
+
+ return pem.EncodeToMemory(pemBlock)
+}
+
+func pemDecode(data []byte) (*pem.Block, error) {
+ pemBlock, _ := pem.Decode(data)
+ if pemBlock == nil {
+ return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?")
+ }
+
+ return pemBlock, nil
+}
+
+func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
+ pemBlock, err := pemDecode(pem)
+ if pemBlock == nil {
+ return nil, err
+ }
+
+ return x509.ParseCertificate(pemBlock.Bytes)
+}
+
+func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
+ pemBlock, err := pemDecode(pem)
+ if pemBlock == nil {
+ return nil, err
+ }
+
+ if pemBlock.Type != "CERTIFICATE REQUEST" {
+ return nil, fmt.Errorf("PEM block is not a certificate request")
+ }
+
+ return x509.ParseCertificateRequest(pemBlock.Bytes)
+}
+
+// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate.
+// The certificate has to be PEM encoded. Any other encodings like DER will fail.
+func GetPEMCertExpiration(cert []byte) (time.Time, error) {
+ pemBlock, err := pemDecode(cert)
+ if pemBlock == nil {
+ return time.Time{}, err
+ }
+
+ return getCertExpiration(pemBlock.Bytes)
+}
+
+// getCertExpiration returns the "NotAfter" date of a DER encoded certificate.
+func getCertExpiration(cert []byte) (time.Time, error) {
+ pCert, err := x509.ParseCertificate(cert)
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ return pCert.NotAfter, nil
+}
+
+func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
+ derBytes, err := generateDerCert(privKey, time.Time{}, domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
+}
+
+func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ return nil, err
+ }
+
+ if expiration.IsZero() {
+ expiration = time.Now().Add(365)
+ }
+
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ CommonName: "ACME Challenge TEMP",
+ },
+ NotBefore: time.Now(),
+ NotAfter: expiration,
+
+ KeyUsage: x509.KeyUsageKeyEncipherment,
+ BasicConstraintsValid: true,
+ DNSNames: []string{domain},
+ }
+
+ return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
+}
+
+func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser {
+ return http.MaxBytesReader(nil, rd, numBytes)
+}
diff --git a/vendor/github.com/xenolf/lego/acme/crypto_test.go b/vendor/github.com/xenolf/lego/acme/crypto_test.go
new file mode 100644
index 000000000..d2fc5088b
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/crypto_test.go
@@ -0,0 +1,93 @@
+package acme
+
+import (
+ "bytes"
+ "crypto/rand"
+ "crypto/rsa"
+ "testing"
+ "time"
+)
+
+func TestGeneratePrivateKey(t *testing.T) {
+ key, err := generatePrivateKey(RSA2048)
+ if err != nil {
+ t.Error("Error generating private key:", err)
+ }
+ if key == nil {
+ t.Error("Expected key to not be nil, but it was")
+ }
+}
+
+func TestGenerateCSR(t *testing.T) {
+ key, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatal("Error generating private key:", err)
+ }
+
+ csr, err := generateCsr(key, "fizz.buzz", nil)
+ if err != nil {
+ t.Error("Error generating CSR:", err)
+ }
+ if csr == nil || len(csr) == 0 {
+ t.Error("Expected CSR with data, but it was nil or length 0")
+ }
+}
+
+func TestPEMEncode(t *testing.T) {
+ buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
+
+ reader := MockRandReader{b: buf}
+ key, err := rsa.GenerateKey(reader, 32)
+ if err != nil {
+ t.Fatal("Error generating private key:", err)
+ }
+
+ data := pemEncode(key)
+
+ if data == nil {
+ t.Fatal("Expected result to not be nil, but it was")
+ }
+ if len(data) != 127 {
+ t.Errorf("Expected PEM encoding to be length 127, but it was %d", len(data))
+ }
+}
+
+func TestPEMCertExpiration(t *testing.T) {
+ privKey, err := generatePrivateKey(RSA2048)
+ if err != nil {
+ t.Fatal("Error generating private key:", err)
+ }
+
+ expiration := time.Now().Add(365)
+ expiration = expiration.Round(time.Second)
+ certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com")
+ if err != nil {
+ t.Fatal("Error generating cert:", err)
+ }
+
+ buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
+
+ // Some random string should return an error.
+ if ctime, err := GetPEMCertExpiration(buf.Bytes()); err == nil {
+ t.Errorf("Expected getCertExpiration to return an error for garbage string but returned %v", ctime)
+ }
+
+ // A DER encoded certificate should return an error.
+ if _, err := GetPEMCertExpiration(certBytes); err == nil {
+ t.Errorf("Expected getCertExpiration to return an error for DER certificates but returned none.")
+ }
+
+ // A PEM encoded certificate should work ok.
+ pemCert := pemEncode(derCertificateBytes(certBytes))
+ if ctime, err := GetPEMCertExpiration(pemCert); err != nil || !ctime.Equal(expiration.UTC()) {
+ t.Errorf("Expected getCertExpiration to return %v but returned %v. Error: %v", expiration, ctime, err)
+ }
+}
+
+type MockRandReader struct {
+ b *bytes.Buffer
+}
+
+func (r MockRandReader) Read(p []byte) (int, error) {
+ return r.b.Read(p)
+}
diff --git a/vendor/github.com/xenolf/lego/acme/dns_challenge.go b/vendor/github.com/xenolf/lego/acme/dns_challenge.go
new file mode 100644
index 000000000..c5fd354a1
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/dns_challenge.go
@@ -0,0 +1,282 @@
+package acme
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/publicsuffix"
+)
+
+type preCheckDNSFunc func(fqdn, value string) (bool, error)
+
+var (
+ // PreCheckDNS checks DNS propagation before notifying ACME that
+ // the DNS challenge is ready.
+ PreCheckDNS preCheckDNSFunc = checkDNSPropagation
+ fqdnToZone = map[string]string{}
+)
+
+var RecursiveNameservers = []string{
+ "google-public-dns-a.google.com:53",
+ "google-public-dns-b.google.com:53",
+}
+
+// DNSTimeout is used to override the default DNS timeout of 10 seconds.
+var DNSTimeout = 10 * time.Second
+
+// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge
+func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
+ keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
+ // base64URL encoding without padding
+ keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
+ value = strings.TrimRight(keyAuthSha, "=")
+ ttl = 120
+ fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
+ return
+}
+
+// dnsChallenge implements the dns-01 challenge according to ACME 7.5
+type dnsChallenge struct {
+ jws *jws
+ validate validateFunc
+ provider ChallengeProvider
+}
+
+func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
+ logf("[INFO][%s] acme: Trying to solve DNS-01", domain)
+
+ if s.provider == nil {
+ return errors.New("No DNS Provider configured")
+ }
+
+ // Generate the Key Authorization for the challenge
+ keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
+ if err != nil {
+ return err
+ }
+
+ err = s.provider.Present(domain, chlng.Token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("Error presenting token: %s", err)
+ }
+ defer func() {
+ err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
+ if err != nil {
+ log.Printf("Error cleaning up %s: %v ", domain, err)
+ }
+ }()
+
+ fqdn, value, _ := DNS01Record(domain, keyAuth)
+
+ logf("[INFO][%s] Checking DNS record propagation...", domain)
+
+ var timeout, interval time.Duration
+ switch provider := s.provider.(type) {
+ case ChallengeProviderTimeout:
+ timeout, interval = provider.Timeout()
+ default:
+ timeout, interval = 60*time.Second, 2*time.Second
+ }
+
+ err = WaitFor(timeout, interval, func() (bool, error) {
+ return PreCheckDNS(fqdn, value)
+ })
+ if err != nil {
+ return err
+ }
+
+ return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
+}
+
+// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
+func checkDNSPropagation(fqdn, value string) (bool, error) {
+ // Initial attempt to resolve at the recursive NS
+ r, err := dnsQuery(fqdn, dns.TypeTXT, RecursiveNameservers, true)
+ if err != nil {
+ return false, err
+ }
+ if r.Rcode == dns.RcodeSuccess {
+ // If we see a CNAME here then use the alias
+ for _, rr := range r.Answer {
+ if cn, ok := rr.(*dns.CNAME); ok {
+ if cn.Hdr.Name == fqdn {
+ fqdn = cn.Target
+ break
+ }
+ }
+ }
+ }
+
+ authoritativeNss, err := lookupNameservers(fqdn)
+ if err != nil {
+ return false, err
+ }
+
+ return checkAuthoritativeNss(fqdn, value, authoritativeNss)
+}
+
+// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
+func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
+ for _, ns := range nameservers {
+ r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false)
+ if err != nil {
+ return false, err
+ }
+
+ if r.Rcode != dns.RcodeSuccess {
+ return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
+ }
+
+ var found bool
+ for _, rr := range r.Answer {
+ if txt, ok := rr.(*dns.TXT); ok {
+ if strings.Join(txt.Txt, "") == value {
+ found = true
+ break
+ }
+ }
+ }
+
+ if !found {
+ return false, fmt.Errorf("NS %s did not return the expected TXT record", ns)
+ }
+ }
+
+ return true, nil
+}
+
+// dnsQuery will query a nameserver, iterating through the supplied servers as it retries
+// The nameserver should include a port, to facilitate testing where we talk to a mock dns server.
+func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) {
+ m := new(dns.Msg)
+ m.SetQuestion(fqdn, rtype)
+ m.SetEdns0(4096, false)
+
+ if !recursive {
+ m.RecursionDesired = false
+ }
+
+ // Will retry the request based on the number of servers (n+1)
+ for i := 1; i <= len(nameservers)+1; i++ {
+ ns := nameservers[i%len(nameservers)]
+ udp := &dns.Client{Net: "udp", Timeout: DNSTimeout}
+ in, _, err = udp.Exchange(m, ns)
+
+ if err == dns.ErrTruncated {
+ tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout}
+ // If the TCP request suceeds, the err will reset to nil
+ in, _, err = tcp.Exchange(m, ns)
+ }
+
+ if err == nil {
+ break
+ }
+ }
+ return
+}
+
+// lookupNameservers returns the authoritative nameservers for the given fqdn.
+func lookupNameservers(fqdn string) ([]string, error) {
+ var authoritativeNss []string
+
+ zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
+ if err != nil {
+ return nil, fmt.Errorf("Could not determine the zone: %v", err)
+ }
+
+ r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, rr := range r.Answer {
+ if ns, ok := rr.(*dns.NS); ok {
+ authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
+ }
+ }
+
+ if len(authoritativeNss) > 0 {
+ return authoritativeNss, nil
+ }
+ return nil, fmt.Errorf("Could not determine authoritative nameservers")
+}
+
+// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the
+// domain labels until the nameserver returns a SOA record in the answer section.
+func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
+ // Do we have it cached?
+ if zone, ok := fqdnToZone[fqdn]; ok {
+ return zone, nil
+ }
+
+ labelIndexes := dns.Split(fqdn)
+ for _, index := range labelIndexes {
+ domain := fqdn[index:]
+ // Give up if we have reached the TLD
+ if isTLD(domain) {
+ break
+ }
+
+ in, err := dnsQuery(domain, dns.TypeSOA, nameservers, true)
+ if err != nil {
+ return "", err
+ }
+
+ // Any response code other than NOERROR and NXDOMAIN is treated as error
+ if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess {
+ return "", fmt.Errorf("Unexpected response code '%s' for %s",
+ dns.RcodeToString[in.Rcode], domain)
+ }
+
+ // Check if we got a SOA RR in the answer section
+ if in.Rcode == dns.RcodeSuccess {
+ for _, ans := range in.Answer {
+ if soa, ok := ans.(*dns.SOA); ok {
+ zone := soa.Hdr.Name
+ fqdnToZone[fqdn] = zone
+ return zone, nil
+ }
+ }
+ }
+ }
+
+ return "", fmt.Errorf("Could not find the start of authority")
+}
+
+func isTLD(domain string) bool {
+ publicsuffix, _ := publicsuffix.PublicSuffix(UnFqdn(domain))
+ if publicsuffix == UnFqdn(domain) {
+ return true
+ }
+ return false
+}
+
+// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
+func ClearFqdnCache() {
+ fqdnToZone = map[string]string{}
+}
+
+// ToFqdn converts the name into a fqdn appending a trailing dot.
+func ToFqdn(name string) string {
+ n := len(name)
+ if n == 0 || name[n-1] == '.' {
+ return name
+ }
+ return name + "."
+}
+
+// UnFqdn converts the fqdn into a name removing the trailing dot.
+func UnFqdn(name string) string {
+ n := len(name)
+ if n != 0 && name[n-1] == '.' {
+ return name[:n-1]
+ }
+ return name
+}
diff --git a/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go b/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
new file mode 100644
index 000000000..240384e60
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
@@ -0,0 +1,53 @@
+package acme
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+)
+
+const (
+ dnsTemplate = "%s %d IN TXT \"%s\""
+)
+
+// DNSProviderManual is an implementation of the ChallengeProvider interface
+type DNSProviderManual struct{}
+
+// NewDNSProviderManual returns a DNSProviderManual instance.
+func NewDNSProviderManual() (*DNSProviderManual, error) {
+ return &DNSProviderManual{}, nil
+}
+
+// Present prints instructions for manually creating the TXT record
+func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := DNS01Record(domain, keyAuth)
+ dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
+
+ authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone)
+ logf("[INFO] acme: %s", dnsRecord)
+ logf("[INFO] acme: Press 'Enter' when you are done")
+
+ reader := bufio.NewReader(os.Stdin)
+ _, _ = reader.ReadString('\n')
+ return nil
+}
+
+// CleanUp prints instructions for manually removing the TXT record
+func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, ttl := DNS01Record(domain, keyAuth)
+ dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, "...")
+
+ authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone)
+ logf("[INFO] acme: %s", dnsRecord)
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/acme/dns_challenge_test.go b/vendor/github.com/xenolf/lego/acme/dns_challenge_test.go
new file mode 100644
index 000000000..6e448854b
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/dns_challenge_test.go
@@ -0,0 +1,185 @@
+package acme
+
+import (
+ "bufio"
+ "crypto/rand"
+ "crypto/rsa"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+)
+
+var lookupNameserversTestsOK = []struct {
+ fqdn string
+ nss []string
+}{
+ {"books.google.com.ng.",
+ []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
+ },
+ {"www.google.com.",
+ []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
+ },
+ {"physics.georgetown.edu.",
+ []string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."},
+ },
+}
+
+var lookupNameserversTestsErr = []struct {
+ fqdn string
+ error string
+}{
+ // invalid tld
+ {"_null.n0n0.",
+ "Could not determine the zone",
+ },
+ // invalid domain
+ {"_null.com.",
+ "Could not determine the zone",
+ },
+ // invalid domain
+ {"in-valid.co.uk.",
+ "Could not determine the zone",
+ },
+}
+
+var findZoneByFqdnTests = []struct {
+ fqdn string
+ zone string
+}{
+ {"mail.google.com.", "google.com."}, // domain is a CNAME
+ {"foo.google.com.", "google.com."}, // domain is a non-existent subdomain
+}
+
+var checkAuthoritativeNssTests = []struct {
+ fqdn, value string
+ ns []string
+ ok bool
+}{
+ // TXT RR w/ expected value
+ {"8.8.8.8.asn.routeviews.org.", "151698.8.8.024", []string{"asnums.routeviews.org."},
+ true,
+ },
+ // No TXT RR
+ {"ns1.google.com.", "", []string{"ns2.google.com."},
+ false,
+ },
+}
+
+var checkAuthoritativeNssTestsErr = []struct {
+ fqdn, value string
+ ns []string
+ error string
+}{
+ // TXT RR /w unexpected value
+ {"8.8.8.8.asn.routeviews.org.", "fe01=", []string{"asnums.routeviews.org."},
+ "did not return the expected TXT record",
+ },
+ // No TXT RR
+ {"ns1.google.com.", "fe01=", []string{"ns2.google.com."},
+ "did not return the expected TXT record",
+ },
+}
+
+func TestDNSValidServerResponse(t *testing.T) {
+ PreCheckDNS = func(fqdn, value string) (bool, error) {
+ return true, nil
+ }
+ privKey, _ := rsa.GenerateKey(rand.Reader, 512)
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Replay-Nonce", "12345")
+ w.Write([]byte("{\"type\":\"dns01\",\"status\":\"valid\",\"uri\":\"http://some.url\",\"token\":\"http8\"}"))
+ }))
+
+ manualProvider, _ := NewDNSProviderManual()
+ jws := &jws{privKey: privKey, directoryURL: ts.URL}
+ solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
+ clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"}
+
+ go func() {
+ time.Sleep(time.Second * 2)
+ f := bufio.NewWriter(os.Stdout)
+ defer f.Flush()
+ f.WriteString("\n")
+ }()
+
+ if err := solver.Solve(clientChallenge, "example.com"); err != nil {
+ t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
+ }
+}
+
+func TestPreCheckDNS(t *testing.T) {
+ ok, err := PreCheckDNS("acme-staging.api.letsencrypt.org", "fe01=")
+ if err != nil || !ok {
+ t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org")
+ }
+}
+
+func TestLookupNameserversOK(t *testing.T) {
+ for _, tt := range lookupNameserversTestsOK {
+ nss, err := lookupNameservers(tt.fqdn)
+ if err != nil {
+ t.Fatalf("#%s: got %q; want nil", tt.fqdn, err)
+ }
+
+ sort.Strings(nss)
+ sort.Strings(tt.nss)
+
+ if !reflect.DeepEqual(nss, tt.nss) {
+ t.Errorf("#%s: got %v; want %v", tt.fqdn, nss, tt.nss)
+ }
+ }
+}
+
+func TestLookupNameserversErr(t *testing.T) {
+ for _, tt := range lookupNameserversTestsErr {
+ _, err := lookupNameservers(tt.fqdn)
+ if err == nil {
+ t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
+ }
+
+ if !strings.Contains(err.Error(), tt.error) {
+ t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
+ continue
+ }
+ }
+}
+
+func TestFindZoneByFqdn(t *testing.T) {
+ for _, tt := range findZoneByFqdnTests {
+ res, err := FindZoneByFqdn(tt.fqdn, RecursiveNameservers)
+ if err != nil {
+ t.Errorf("FindZoneByFqdn failed for %s: %v", tt.fqdn, err)
+ }
+ if res != tt.zone {
+ t.Errorf("%s: got %s; want %s", tt.fqdn, res, tt.zone)
+ }
+ }
+}
+
+func TestCheckAuthoritativeNss(t *testing.T) {
+ for _, tt := range checkAuthoritativeNssTests {
+ ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
+ if ok != tt.ok {
+ t.Errorf("%s: got %t; want %t", tt.fqdn, ok, tt.ok)
+ }
+ }
+}
+
+func TestCheckAuthoritativeNssErr(t *testing.T) {
+ for _, tt := range checkAuthoritativeNssTestsErr {
+ _, err := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
+ if err == nil {
+ t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
+ }
+ if !strings.Contains(err.Error(), tt.error) {
+ t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
+ continue
+ }
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/error.go b/vendor/github.com/xenolf/lego/acme/error.go
new file mode 100644
index 000000000..2aa690b33
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/error.go
@@ -0,0 +1,86 @@
+package acme
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+const (
+ tosAgreementError = "Must agree to subscriber agreement before any further actions"
+)
+
+// RemoteError is the base type for all errors specific to the ACME protocol.
+type RemoteError struct {
+ StatusCode int `json:"status,omitempty"`
+ Type string `json:"type"`
+ Detail string `json:"detail"`
+}
+
+func (e RemoteError) Error() string {
+ return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail)
+}
+
+// TOSError represents the error which is returned if the user needs to
+// accept the TOS.
+// TODO: include the new TOS url if we can somehow obtain it.
+type TOSError struct {
+ RemoteError
+}
+
+type domainError struct {
+ Domain string
+ Error error
+}
+
+type challengeError struct {
+ RemoteError
+ records []validationRecord
+}
+
+func (c challengeError) Error() string {
+
+ var errStr string
+ for _, validation := range c.records {
+ errStr = errStr + fmt.Sprintf("\tValidation for %s:%s\n\tResolved to:\n\t\t%s\n\tUsed: %s\n\n",
+ validation.Hostname, validation.Port, strings.Join(validation.ResolvedAddresses, "\n\t\t"), validation.UsedAddress)
+ }
+
+ return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr)
+}
+
+func handleHTTPError(resp *http.Response) error {
+ var errorDetail RemoteError
+
+ contenType := resp.Header.Get("Content-Type")
+ // try to decode the content as JSON
+ if contenType == "application/json" || contenType == "application/problem+json" {
+ decoder := json.NewDecoder(resp.Body)
+ err := decoder.Decode(&errorDetail)
+ if err != nil {
+ return err
+ }
+ } else {
+ detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
+ if err != nil {
+ return err
+ }
+
+ errorDetail.Detail = string(detailBytes)
+ }
+
+ errorDetail.StatusCode = resp.StatusCode
+
+ // Check for errors we handle specifically
+ if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError {
+ return TOSError{errorDetail}
+ }
+
+ return errorDetail
+}
+
+func handleChallengeError(chlng challenge) error {
+ return challengeError{chlng.Error, chlng.ValidationRecords}
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http.go b/vendor/github.com/xenolf/lego/acme/http.go
new file mode 100644
index 000000000..180db786d
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http.go
@@ -0,0 +1,117 @@
+package acme
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "runtime"
+ "strings"
+ "time"
+)
+
+// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
+var UserAgent string
+
+// HTTPClient is an HTTP client with a reasonable timeout value.
+var HTTPClient = http.Client{Timeout: 10 * time.Second}
+
+const (
+ // defaultGoUserAgent is the Go HTTP package user agent string. Too
+ // bad it isn't exported. If it changes, we should update it here, too.
+ defaultGoUserAgent = "Go-http-client/1.1"
+
+ // ourUserAgent is the User-Agent of this underlying library package.
+ ourUserAgent = "xenolf-acme"
+)
+
+// httpHead performs a HEAD request with a proper User-Agent string.
+// The response body (resp.Body) is already closed when this function returns.
+func httpHead(url string) (resp *http.Response, err error) {
+ req, err := http.NewRequest("HEAD", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("User-Agent", userAgent())
+
+ resp, err = HTTPClient.Do(req)
+ if err != nil {
+ return resp, err
+ }
+ resp.Body.Close()
+ return resp, err
+}
+
+// httpPost performs a POST request with a proper User-Agent string.
+// Callers should close resp.Body when done reading from it.
+func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
+ req, err := http.NewRequest("POST", url, body)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", bodyType)
+ req.Header.Set("User-Agent", userAgent())
+
+ return HTTPClient.Do(req)
+}
+
+// httpGet performs a GET request with a proper User-Agent string.
+// Callers should close resp.Body when done reading from it.
+func httpGet(url string) (resp *http.Response, err error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("User-Agent", userAgent())
+
+ return HTTPClient.Do(req)
+}
+
+// getJSON performs an HTTP GET request and parses the response body
+// as JSON, into the provided respBody object.
+func getJSON(uri string, respBody interface{}) (http.Header, error) {
+ resp, err := httpGet(uri)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get %q: %v", uri, err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= http.StatusBadRequest {
+ return resp.Header, handleHTTPError(resp)
+ }
+
+ return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
+}
+
+// postJSON performs an HTTP POST request and parses the response body
+// as JSON, into the provided respBody object.
+func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
+ jsonBytes, err := json.Marshal(reqBody)
+ if err != nil {
+ return nil, errors.New("Failed to marshal network message...")
+ }
+
+ resp, err := j.post(uri, jsonBytes)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= http.StatusBadRequest {
+ return resp.Header, handleHTTPError(resp)
+ }
+
+ if respBody == nil {
+ return resp.Header, nil
+ }
+
+ return resp.Header, json.NewDecoder(resp.Body).Decode(respBody)
+}
+
+// userAgent builds and returns the User-Agent string to use in requests.
+func userAgent() string {
+ ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent)
+ return strings.TrimSpace(ua)
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http_challenge.go b/vendor/github.com/xenolf/lego/acme/http_challenge.go
new file mode 100644
index 000000000..95cb1fd81
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_challenge.go
@@ -0,0 +1,41 @@
+package acme
+
+import (
+ "fmt"
+ "log"
+)
+
+type httpChallenge struct {
+ jws *jws
+ validate validateFunc
+ provider ChallengeProvider
+}
+
+// HTTP01ChallengePath returns the URL path for the `http-01` challenge
+func HTTP01ChallengePath(token string) string {
+ return "/.well-known/acme-challenge/" + token
+}
+
+func (s *httpChallenge) Solve(chlng challenge, domain string) error {
+
+ logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
+
+ // Generate the Key Authorization for the challenge
+ keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
+ if err != nil {
+ return err
+ }
+
+ err = s.provider.Present(domain, chlng.Token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("[%s] error presenting token: %v", domain, err)
+ }
+ defer func() {
+ err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
+ if err != nil {
+ log.Printf("[%s] error cleaning up: %v", domain, err)
+ }
+ }()
+
+ return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http_challenge_server.go b/vendor/github.com/xenolf/lego/acme/http_challenge_server.go
new file mode 100644
index 000000000..42541380c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_challenge_server.go
@@ -0,0 +1,79 @@
+package acme
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "strings"
+)
+
+// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
+// It may be instantiated without using the NewHTTPProviderServer function if
+// you want only to use the default values.
+type HTTPProviderServer struct {
+ iface string
+ port string
+ done chan bool
+ listener net.Listener
+}
+
+// NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port.
+// Setting iface and / or port to an empty string will make the server fall back to
+// the "any" interface and port 80 respectively.
+func NewHTTPProviderServer(iface, port string) *HTTPProviderServer {
+ return &HTTPProviderServer{iface: iface, port: port}
+}
+
+// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests.
+func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
+ if s.port == "" {
+ s.port = "80"
+ }
+
+ var err error
+ s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
+ if err != nil {
+ return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
+ }
+
+ s.done = make(chan bool)
+ go s.serve(domain, token, keyAuth)
+ return nil
+}
+
+// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)`
+func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error {
+ if s.listener == nil {
+ return nil
+ }
+ s.listener.Close()
+ <-s.done
+ return nil
+}
+
+func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
+ path := HTTP01ChallengePath(token)
+
+ // The handler validates the HOST header and request type.
+ // For validation it then writes the token the server returned with the challenge
+ mux := http.NewServeMux()
+ mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
+ w.Header().Add("Content-Type", "text/plain")
+ w.Write([]byte(keyAuth))
+ logf("[INFO][%s] Served key authentication", domain)
+ } else {
+ logf("[INFO] Received request for domain %s with method %s", r.Host, r.Method)
+ w.Write([]byte("TEST"))
+ }
+ })
+
+ httpServer := &http.Server{
+ Handler: mux,
+ }
+ // Once httpServer is shut down we don't want any lingering
+ // connections, so disable KeepAlives.
+ httpServer.SetKeepAlivesEnabled(false)
+ httpServer.Serve(s.listener)
+ s.done <- true
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http_challenge_test.go b/vendor/github.com/xenolf/lego/acme/http_challenge_test.go
new file mode 100644
index 000000000..fdd8f4d27
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_challenge_test.go
@@ -0,0 +1,57 @@
+package acme
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "io/ioutil"
+ "strings"
+ "testing"
+)
+
+func TestHTTPChallenge(t *testing.T) {
+ privKey, _ := rsa.GenerateKey(rand.Reader, 512)
+ j := &jws{privKey: privKey}
+ clientChallenge := challenge{Type: HTTP01, Token: "http1"}
+ mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
+ uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
+ resp, err := httpGet(uri)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if want := "text/plain"; resp.Header.Get("Content-Type") != want {
+ t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ bodyStr := string(body)
+
+ if bodyStr != chlng.KeyAuthorization {
+ t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization)
+ }
+
+ return nil
+ }
+ solver := &httpChallenge{jws: j, validate: mockValidate, provider: &HTTPProviderServer{port: "23457"}}
+
+ if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
+ t.Errorf("Solve error: got %v, want nil", err)
+ }
+}
+
+func TestHTTPChallengeInvalidPort(t *testing.T) {
+ privKey, _ := rsa.GenerateKey(rand.Reader, 128)
+ j := &jws{privKey: privKey}
+ clientChallenge := challenge{Type: HTTP01, Token: "http2"}
+ solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
+
+ if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
+ t.Errorf("Solve error: got %v, want error", err)
+ } else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
+ t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/http_test.go b/vendor/github.com/xenolf/lego/acme/http_test.go
new file mode 100644
index 000000000..33a48a331
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/http_test.go
@@ -0,0 +1,100 @@
+package acme
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestHTTPHeadUserAgent(t *testing.T) {
+ var ua, method string
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ua = r.Header.Get("User-Agent")
+ method = r.Method
+ }))
+ defer ts.Close()
+
+ _, err := httpHead(ts.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if method != "HEAD" {
+ t.Errorf("Expected method to be HEAD, got %s", method)
+ }
+ if !strings.Contains(ua, ourUserAgent) {
+ t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
+ }
+}
+
+func TestHTTPGetUserAgent(t *testing.T) {
+ var ua, method string
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ua = r.Header.Get("User-Agent")
+ method = r.Method
+ }))
+ defer ts.Close()
+
+ res, err := httpGet(ts.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+
+ if method != "GET" {
+ t.Errorf("Expected method to be GET, got %s", method)
+ }
+ if !strings.Contains(ua, ourUserAgent) {
+ t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
+ }
+}
+
+func TestHTTPPostUserAgent(t *testing.T) {
+ var ua, method string
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ua = r.Header.Get("User-Agent")
+ method = r.Method
+ }))
+ defer ts.Close()
+
+ res, err := httpPost(ts.URL, "text/plain", strings.NewReader("falalalala"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+
+ if method != "POST" {
+ t.Errorf("Expected method to be POST, got %s", method)
+ }
+ if !strings.Contains(ua, ourUserAgent) {
+ t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
+ }
+}
+
+func TestUserAgent(t *testing.T) {
+ ua := userAgent()
+
+ if !strings.Contains(ua, defaultGoUserAgent) {
+ t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
+ }
+ if !strings.Contains(ua, ourUserAgent) {
+ t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
+ }
+ if strings.HasSuffix(ua, " ") {
+ t.Errorf("UA should not have trailing spaces; got '%s'", ua)
+ }
+
+ // customize the UA by appending a value
+ UserAgent = "MyApp/1.2.3"
+ ua = userAgent()
+ if !strings.Contains(ua, defaultGoUserAgent) {
+ t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
+ }
+ if !strings.Contains(ua, ourUserAgent) {
+ t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
+ }
+ if !strings.Contains(ua, UserAgent) {
+ t.Errorf("Expected custom UA to contain %s, got '%s'", UserAgent, ua)
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/jws.go b/vendor/github.com/xenolf/lego/acme/jws.go
new file mode 100644
index 000000000..f70513e38
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/jws.go
@@ -0,0 +1,115 @@
+package acme
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "fmt"
+ "net/http"
+ "sync"
+
+ "gopkg.in/square/go-jose.v1"
+)
+
+type jws struct {
+ directoryURL string
+ privKey crypto.PrivateKey
+ nonces []string
+ sync.Mutex
+}
+
+func keyAsJWK(key interface{}) *jose.JsonWebKey {
+ switch k := key.(type) {
+ case *ecdsa.PublicKey:
+ return &jose.JsonWebKey{Key: k, Algorithm: "EC"}
+ case *rsa.PublicKey:
+ return &jose.JsonWebKey{Key: k, Algorithm: "RSA"}
+
+ default:
+ return nil
+ }
+}
+
+// Posts a JWS signed message to the specified URL
+func (j *jws) post(url string, content []byte) (*http.Response, error) {
+ signedContent, err := j.signContent(content)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
+ if err != nil {
+ return nil, err
+ }
+
+ j.getNonceFromResponse(resp)
+
+ return resp, err
+}
+
+func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
+
+ var alg jose.SignatureAlgorithm
+ switch k := j.privKey.(type) {
+ case *rsa.PrivateKey:
+ alg = jose.RS256
+ case *ecdsa.PrivateKey:
+ if k.Curve == elliptic.P256() {
+ alg = jose.ES256
+ } else if k.Curve == elliptic.P384() {
+ alg = jose.ES384
+ }
+ }
+
+ signer, err := jose.NewSigner(alg, j.privKey)
+ if err != nil {
+ return nil, err
+ }
+ signer.SetNonceSource(j)
+
+ signed, err := signer.Sign(content)
+ if err != nil {
+ return nil, err
+ }
+ return signed, nil
+}
+
+func (j *jws) getNonceFromResponse(resp *http.Response) error {
+ j.Lock()
+ defer j.Unlock()
+ nonce := resp.Header.Get("Replay-Nonce")
+ if nonce == "" {
+ return fmt.Errorf("Server did not respond with a proper nonce header.")
+ }
+
+ j.nonces = append(j.nonces, nonce)
+ return nil
+}
+
+func (j *jws) getNonce() error {
+ resp, err := httpHead(j.directoryURL)
+ if err != nil {
+ return err
+ }
+
+ return j.getNonceFromResponse(resp)
+}
+
+func (j *jws) Nonce() (string, error) {
+ nonce := ""
+ if len(j.nonces) == 0 {
+ err := j.getNonce()
+ if err != nil {
+ return nonce, err
+ }
+ }
+ if len(j.nonces) == 0 {
+ return "", fmt.Errorf("Can't get nonce")
+ }
+ j.Lock()
+ defer j.Unlock()
+ nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1]
+ return nonce, nil
+}
diff --git a/vendor/github.com/xenolf/lego/acme/messages.go b/vendor/github.com/xenolf/lego/acme/messages.go
new file mode 100644
index 000000000..0efeae674
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/messages.go
@@ -0,0 +1,117 @@
+package acme
+
+import (
+ "time"
+
+ "gopkg.in/square/go-jose.v1"
+)
+
+type directory struct {
+ NewAuthzURL string `json:"new-authz"`
+ NewCertURL string `json:"new-cert"`
+ NewRegURL string `json:"new-reg"`
+ RevokeCertURL string `json:"revoke-cert"`
+}
+
+type recoveryKeyMessage struct {
+ Length int `json:"length,omitempty"`
+ Client jose.JsonWebKey `json:"client,omitempty"`
+ Server jose.JsonWebKey `json:"client,omitempty"`
+}
+
+type registrationMessage struct {
+ Resource string `json:"resource"`
+ Contact []string `json:"contact"`
+ Delete bool `json:"delete,omitempty"`
+ // RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"`
+}
+
+// Registration is returned by the ACME server after the registration
+// The client implementation should save this registration somewhere.
+type Registration struct {
+ Resource string `json:"resource,omitempty"`
+ ID int `json:"id"`
+ Key jose.JsonWebKey `json:"key"`
+ Contact []string `json:"contact"`
+ Agreement string `json:"agreement,omitempty"`
+ Authorizations string `json:"authorizations,omitempty"`
+ Certificates string `json:"certificates,omitempty"`
+ // RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"`
+}
+
+// RegistrationResource represents all important informations about a registration
+// of which the client needs to keep track itself.
+type RegistrationResource struct {
+ Body Registration `json:"body,omitempty"`
+ URI string `json:"uri,omitempty"`
+ NewAuthzURL string `json:"new_authzr_uri,omitempty"`
+ TosURL string `json:"terms_of_service,omitempty"`
+}
+
+type authorizationResource struct {
+ Body authorization
+ Domain string
+ NewCertURL string
+ AuthURL string
+}
+
+type authorization struct {
+ Resource string `json:"resource,omitempty"`
+ Identifier identifier `json:"identifier"`
+ Status string `json:"status,omitempty"`
+ Expires time.Time `json:"expires,omitempty"`
+ Challenges []challenge `json:"challenges,omitempty"`
+ Combinations [][]int `json:"combinations,omitempty"`
+}
+
+type identifier struct {
+ Type string `json:"type"`
+ Value string `json:"value"`
+}
+
+type validationRecord struct {
+ URI string `json:"url,omitempty"`
+ Hostname string `json:"hostname,omitempty"`
+ Port string `json:"port,omitempty"`
+ ResolvedAddresses []string `json:"addressesResolved,omitempty"`
+ UsedAddress string `json:"addressUsed,omitempty"`
+}
+
+type challenge struct {
+ Resource string `json:"resource,omitempty"`
+ Type Challenge `json:"type,omitempty"`
+ Status string `json:"status,omitempty"`
+ URI string `json:"uri,omitempty"`
+ Token string `json:"token,omitempty"`
+ KeyAuthorization string `json:"keyAuthorization,omitempty"`
+ TLS bool `json:"tls,omitempty"`
+ Iterations int `json:"n,omitempty"`
+ Error RemoteError `json:"error,omitempty"`
+ ValidationRecords []validationRecord `json:"validationRecord,omitempty"`
+}
+
+type csrMessage struct {
+ Resource string `json:"resource,omitempty"`
+ Csr string `json:"csr"`
+ Authorizations []string `json:"authorizations"`
+}
+
+type revokeCertMessage struct {
+ Resource string `json:"resource"`
+ Certificate string `json:"certificate"`
+}
+
+// CertificateResource represents a CA issued certificate.
+// PrivateKey and Certificate are both already PEM encoded
+// and can be directly written to disk. Certificate may
+// be a certificate bundle, depending on the options supplied
+// to create it.
+type CertificateResource struct {
+ Domain string `json:"domain"`
+ CertURL string `json:"certUrl"`
+ CertStableURL string `json:"certStableUrl"`
+ AccountRef string `json:"accountRef,omitempty"`
+ PrivateKey []byte `json:"-"`
+ Certificate []byte `json:"-"`
+ CSR []byte `json:"-"`
+}
diff --git a/vendor/github.com/xenolf/lego/acme/pop_challenge.go b/vendor/github.com/xenolf/lego/acme/pop_challenge.go
new file mode 100644
index 000000000..8d2a213b0
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/pop_challenge.go
@@ -0,0 +1 @@
+package acme
diff --git a/vendor/github.com/xenolf/lego/acme/provider.go b/vendor/github.com/xenolf/lego/acme/provider.go
new file mode 100644
index 000000000..d177ff07a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/provider.go
@@ -0,0 +1,28 @@
+package acme
+
+import "time"
+
+// ChallengeProvider enables implementing a custom challenge
+// provider. Present presents the solution to a challenge available to
+// be solved. CleanUp will be called by the challenge if Present ends
+// in a non-error state.
+type ChallengeProvider interface {
+ Present(domain, token, keyAuth string) error
+ CleanUp(domain, token, keyAuth string) error
+}
+
+// ChallengeProviderTimeout allows for implementing a
+// ChallengeProvider where an unusually long timeout is required when
+// waiting for an ACME challenge to be satisfied, such as when
+// checking for DNS record progagation. If an implementor of a
+// ChallengeProvider provides a Timeout method, then the return values
+// of the Timeout method will be used when appropriate by the acme
+// package. The interval value is the time between checks.
+//
+// The default values used for timeout and interval are 60 seconds and
+// 2 seconds respectively. These are used when no Timeout method is
+// defined for the ChallengeProvider.
+type ChallengeProviderTimeout interface {
+ ChallengeProvider
+ Timeout() (timeout, interval time.Duration)
+}
diff --git a/vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go
new file mode 100644
index 000000000..34383cbfa
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go
@@ -0,0 +1,67 @@
+package acme
+
+import (
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "fmt"
+ "log"
+)
+
+type tlsSNIChallenge struct {
+ jws *jws
+ validate validateFunc
+ provider ChallengeProvider
+}
+
+func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
+ // FIXME: https://github.com/ietf-wg-acme/acme/pull/22
+ // Currently we implement this challenge to track boulder, not the current spec!
+
+ logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
+
+ // Generate the Key Authorization for the challenge
+ keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
+ if err != nil {
+ return err
+ }
+
+ err = t.provider.Present(domain, chlng.Token, keyAuth)
+ if err != nil {
+ return fmt.Errorf("[%s] error presenting token: %v", domain, err)
+ }
+ defer func() {
+ err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
+ if err != nil {
+ log.Printf("[%s] error cleaning up: %v", domain, err)
+ }
+ }()
+ return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
+}
+
+// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
+func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, string, error) {
+ // generate a new RSA key for the certificates
+ tempPrivKey, err := generatePrivateKey(RSA2048)
+ if err != nil {
+ return tls.Certificate{}, "", err
+ }
+ rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
+ rsaPrivPEM := pemEncode(rsaPrivKey)
+
+ zBytes := sha256.Sum256([]byte(keyAuth))
+ z := hex.EncodeToString(zBytes[:sha256.Size])
+ domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
+ tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
+ if err != nil {
+ return tls.Certificate{}, "", err
+ }
+
+ certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
+ if err != nil {
+ return tls.Certificate{}, "", err
+ }
+
+ return certificate, domain, nil
+}
diff --git a/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go
new file mode 100644
index 000000000..df00fbb5a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go
@@ -0,0 +1,62 @@
+package acme
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net"
+ "net/http"
+)
+
+// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge
+// It may be instantiated without using the NewTLSProviderServer function if
+// you want only to use the default values.
+type TLSProviderServer struct {
+ iface string
+ port string
+ done chan bool
+ listener net.Listener
+}
+
+// NewTLSProviderServer creates a new TLSProviderServer on the selected interface and port.
+// Setting iface and / or port to an empty string will make the server fall back to
+// the "any" interface and port 443 respectively.
+func NewTLSProviderServer(iface, port string) *TLSProviderServer {
+ return &TLSProviderServer{iface: iface, port: port}
+}
+
+// Present makes the keyAuth available as a cert
+func (s *TLSProviderServer) Present(domain, token, keyAuth string) error {
+ if s.port == "" {
+ s.port = "443"
+ }
+
+ cert, _, err := TLSSNI01ChallengeCert(keyAuth)
+ if err != nil {
+ return err
+ }
+
+ tlsConf := new(tls.Config)
+ tlsConf.Certificates = []tls.Certificate{cert}
+
+ s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf)
+ if err != nil {
+ return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
+ }
+
+ s.done = make(chan bool)
+ go func() {
+ http.Serve(s.listener, nil)
+ s.done <- true
+ }()
+ return nil
+}
+
+// CleanUp closes the HTTP server.
+func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error {
+ if s.listener == nil {
+ return nil
+ }
+ s.listener.Close()
+ <-s.done
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go
new file mode 100644
index 000000000..3aec74565
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go
@@ -0,0 +1,65 @@
+package acme
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func TestTLSSNIChallenge(t *testing.T) {
+ privKey, _ := rsa.GenerateKey(rand.Reader, 512)
+ j := &jws{privKey: privKey}
+ clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"}
+ mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
+ conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
+ InsecureSkipVerify: true,
+ })
+ if err != nil {
+ t.Errorf("Expected to connect to challenge server without an error. %s", err.Error())
+ }
+
+ // Expect the server to only return one certificate
+ connState := conn.ConnectionState()
+ if count := len(connState.PeerCertificates); count != 1 {
+ t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count)
+ }
+
+ remoteCert := connState.PeerCertificates[0]
+ if count := len(remoteCert.DNSNames); count != 1 {
+ t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count)
+ }
+
+ zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
+ z := hex.EncodeToString(zBytes[:sha256.Size])
+ domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
+
+ if remoteCert.DNSNames[0] != domain {
+ t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0])
+ }
+
+ return nil
+ }
+ solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}}
+
+ if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
+ t.Errorf("Solve error: got %v, want nil", err)
+ }
+}
+
+func TestTLSSNIChallengeInvalidPort(t *testing.T) {
+ privKey, _ := rsa.GenerateKey(rand.Reader, 128)
+ j := &jws{privKey: privKey}
+ clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"}
+ solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}}
+
+ if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
+ t.Errorf("Solve error: got %v, want error", err)
+ } else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
+ t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/utils.go b/vendor/github.com/xenolf/lego/acme/utils.go
new file mode 100644
index 000000000..2fa0db304
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/utils.go
@@ -0,0 +1,29 @@
+package acme
+
+import (
+ "fmt"
+ "time"
+)
+
+// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
+func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
+ var lastErr string
+ timeup := time.After(timeout)
+ for {
+ select {
+ case <-timeup:
+ return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr)
+ default:
+ }
+
+ stop, err := f()
+ if stop {
+ return nil
+ }
+ if err != nil {
+ lastErr = err.Error()
+ }
+
+ time.Sleep(interval)
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/acme/utils_test.go b/vendor/github.com/xenolf/lego/acme/utils_test.go
new file mode 100644
index 000000000..158af4116
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/acme/utils_test.go
@@ -0,0 +1,26 @@
+package acme
+
+import (
+ "testing"
+ "time"
+)
+
+func TestWaitForTimeout(t *testing.T) {
+ c := make(chan error)
+ go func() {
+ err := WaitFor(3*time.Second, 1*time.Second, func() (bool, error) {
+ return false, nil
+ })
+ c <- err
+ }()
+
+ timeout := time.After(4 * time.Second)
+ select {
+ case <-timeout:
+ t.Fatal("timeout exceeded")
+ case err := <-c:
+ if err == nil {
+ t.Errorf("expected timeout error; got %v", err)
+ }
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/cli.go b/vendor/github.com/xenolf/lego/cli.go
new file mode 100644
index 000000000..abdcf47de
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/cli.go
@@ -0,0 +1,214 @@
+// Let's Encrypt client to go!
+// CLI application for generating Let's Encrypt certificates using the ACME package.
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "strings"
+ "text/tabwriter"
+
+ "github.com/urfave/cli"
+ "github.com/xenolf/lego/acme"
+)
+
+// Logger is used to log errors; if nil, the default log.Logger is used.
+var Logger *log.Logger
+
+// logger is an helper function to retrieve the available logger
+func logger() *log.Logger {
+ if Logger == nil {
+ Logger = log.New(os.Stderr, "", log.LstdFlags)
+ }
+ return Logger
+}
+
+var gittag string
+
+func main() {
+ app := cli.NewApp()
+ app.Name = "lego"
+ app.Usage = "Let's Encrypt client written in Go"
+
+ version := "0.3.1"
+ if strings.HasPrefix(gittag, "v") {
+ version = gittag
+ }
+
+ app.Version = version
+
+ acme.UserAgent = "lego/" + app.Version
+
+ defaultPath := ""
+ cwd, err := os.Getwd()
+ if err == nil {
+ defaultPath = path.Join(cwd, ".lego")
+ }
+
+ app.Before = func(c *cli.Context) error {
+ if c.GlobalString("path") == "" {
+ logger().Fatal("Could not determine current working directory. Please pass --path.")
+ }
+ return nil
+ }
+
+ app.Commands = []cli.Command{
+ {
+ Name: "run",
+ Usage: "Register an account, then create and install a certificate",
+ Action: run,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "no-bundle",
+ Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
+ },
+ },
+ },
+ {
+ Name: "revoke",
+ Usage: "Revoke a certificate",
+ Action: revoke,
+ },
+ {
+ Name: "renew",
+ Usage: "Renew a certificate",
+ Action: renew,
+ Flags: []cli.Flag{
+ cli.IntFlag{
+ Name: "days",
+ Value: 0,
+ Usage: "The number of days left on a certificate to renew it.",
+ },
+ cli.BoolFlag{
+ Name: "reuse-key",
+ Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
+ },
+ cli.BoolFlag{
+ Name: "no-bundle",
+ Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
+ },
+ },
+ },
+ {
+ Name: "dnshelp",
+ Usage: "Shows additional help for the --dns global option",
+ Action: dnshelp,
+ },
+ }
+
+ app.Flags = []cli.Flag{
+ cli.StringSliceFlag{
+ Name: "domains, d",
+ Usage: "Add domains to the process",
+ },
+ cli.StringFlag{
+ Name: "csr, c",
+ Usage: "Certificate signing request filename, if an external CSR is to be used",
+ },
+ cli.StringFlag{
+ Name: "server, s",
+ Value: "https://acme-v01.api.letsencrypt.org/directory",
+ Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
+ },
+ cli.StringFlag{
+ Name: "email, m",
+ Usage: "Email used for registration and recovery contact.",
+ },
+ cli.BoolFlag{
+ Name: "accept-tos, a",
+ Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
+ },
+ cli.StringFlag{
+ Name: "key-type, k",
+ Value: "rsa2048",
+ Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384",
+ },
+ cli.StringFlag{
+ Name: "path",
+ Usage: "Directory to use for storing the data",
+ Value: defaultPath,
+ },
+ cli.StringSliceFlag{
+ Name: "exclude, x",
+ Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"tls-sni-01\".",
+ },
+ cli.StringFlag{
+ Name: "webroot",
+ Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge",
+ },
+ cli.StringFlag{
+ Name: "http",
+ Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port",
+ },
+ cli.StringFlag{
+ Name: "tls",
+ Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port",
+ },
+ cli.StringFlag{
+ Name: "dns",
+ Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.",
+ },
+ cli.IntFlag{
+ Name: "http-timeout",
+ Usage: "Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds.",
+ },
+ cli.IntFlag{
+ Name: "dns-timeout",
+ Usage: "Set the DNS timeout value to a specific value in seconds. The default is 10 seconds.",
+ },
+ cli.StringSliceFlag{
+ Name: "dns-resolvers",
+ Usage: "Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use Google's DNS resolvers.",
+ },
+ cli.BoolFlag{
+ Name: "pem",
+ Usage: "Generate a .pem file by concatanating the .key and .crt files together.",
+ },
+ }
+
+ err = app.Run(os.Args)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func dnshelp(c *cli.Context) error {
+ fmt.Printf(
+ `Credentials for DNS providers must be passed through environment variables.
+
+Here is an example bash command using the CloudFlare DNS provider:
+
+ $ CLOUDFLARE_EMAIL=foo@bar.com \
+ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \
+ lego --dns cloudflare --domains www.example.com --email me@bar.com run
+
+`)
+
+ w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
+ fmt.Fprintln(w, "Valid providers and their associated credential environment variables:")
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
+ fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
+ fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_API_KEY")
+ fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET")
+ fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY")
+ fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT")
+ fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY")
+ fmt.Fprintln(w, "\tmanual:\tnone")
+ fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
+ fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
+ fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION")
+ fmt.Fprintln(w, "\tdyn:\tDYN_CUSTOMER_NAME, DYN_USER_NAME, DYN_PASSWORD")
+ fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY")
+ fmt.Fprintln(w, "\tovh:\tOVH_ENDPOINT, OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY")
+ fmt.Fprintln(w, "\tpdns:\tPDNS_API_KEY, PDNS_API_URL")
+ w.Flush()
+
+ fmt.Println(`
+For a more detailed explanation of a DNS provider's credential variables,
+please consult their online documentation.`)
+
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/cli_handlers.go b/vendor/github.com/xenolf/lego/cli_handlers.go
new file mode 100644
index 000000000..29a1166d8
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/cli_handlers.go
@@ -0,0 +1,444 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/urfave/cli"
+ "github.com/xenolf/lego/acme"
+ "github.com/xenolf/lego/providers/dns/cloudflare"
+ "github.com/xenolf/lego/providers/dns/digitalocean"
+ "github.com/xenolf/lego/providers/dns/dnsimple"
+ "github.com/xenolf/lego/providers/dns/dnsmadeeasy"
+ "github.com/xenolf/lego/providers/dns/dyn"
+ "github.com/xenolf/lego/providers/dns/gandi"
+ "github.com/xenolf/lego/providers/dns/googlecloud"
+ "github.com/xenolf/lego/providers/dns/linode"
+ "github.com/xenolf/lego/providers/dns/namecheap"
+ "github.com/xenolf/lego/providers/dns/ovh"
+ "github.com/xenolf/lego/providers/dns/pdns"
+ "github.com/xenolf/lego/providers/dns/rfc2136"
+ "github.com/xenolf/lego/providers/dns/route53"
+ "github.com/xenolf/lego/providers/dns/vultr"
+ "github.com/xenolf/lego/providers/http/webroot"
+)
+
+func checkFolder(path string) error {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return os.MkdirAll(path, 0700)
+ }
+ return nil
+}
+
+func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
+
+ if c.GlobalIsSet("http-timeout") {
+ acme.HTTPClient = http.Client{Timeout: time.Duration(c.GlobalInt("http-timeout")) * time.Second}
+ }
+
+ if c.GlobalIsSet("dns-timeout") {
+ acme.DNSTimeout = time.Duration(c.GlobalInt("dns-timeout")) * time.Second
+ }
+
+ if len(c.GlobalStringSlice("dns-resolvers")) > 0 {
+ resolvers := []string{}
+ for _, resolver := range c.GlobalStringSlice("dns-resolvers") {
+ if !strings.Contains(resolver, ":") {
+ resolver += ":53"
+ }
+ resolvers = append(resolvers, resolver)
+ }
+ acme.RecursiveNameservers = resolvers
+ }
+
+ err := checkFolder(c.GlobalString("path"))
+ if err != nil {
+ logger().Fatalf("Could not check/create path: %s", err.Error())
+ }
+
+ conf := NewConfiguration(c)
+ if len(c.GlobalString("email")) == 0 {
+ logger().Fatal("You have to pass an account (email address) to the program using --email or -m")
+ }
+
+ //TODO: move to account struct? Currently MUST pass email.
+ acc := NewAccount(c.GlobalString("email"), conf)
+
+ keyType, err := conf.KeyType()
+ if err != nil {
+ logger().Fatal(err.Error())
+ }
+
+ client, err := acme.NewClient(c.GlobalString("server"), acc, keyType)
+ if err != nil {
+ logger().Fatalf("Could not create client: %s", err.Error())
+ }
+
+ if len(c.GlobalStringSlice("exclude")) > 0 {
+ client.ExcludeChallenges(conf.ExcludedSolvers())
+ }
+
+ if c.GlobalIsSet("webroot") {
+ provider, err := webroot.NewHTTPProvider(c.GlobalString("webroot"))
+ if err != nil {
+ logger().Fatal(err)
+ }
+
+ client.SetChallengeProvider(acme.HTTP01, provider)
+
+ // --webroot=foo indicates that the user specifically want to do a HTTP challenge
+ // infer that the user also wants to exclude all other challenges
+ client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01})
+ }
+ if c.GlobalIsSet("http") {
+ if strings.Index(c.GlobalString("http"), ":") == -1 {
+ logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.")
+ }
+ client.SetHTTPAddress(c.GlobalString("http"))
+ }
+
+ if c.GlobalIsSet("tls") {
+ if strings.Index(c.GlobalString("tls"), ":") == -1 {
+ logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.")
+ }
+ client.SetTLSAddress(c.GlobalString("tls"))
+ }
+
+ if c.GlobalIsSet("dns") {
+ var err error
+ var provider acme.ChallengeProvider
+ switch c.GlobalString("dns") {
+ case "cloudflare":
+ provider, err = cloudflare.NewDNSProvider()
+ case "digitalocean":
+ provider, err = digitalocean.NewDNSProvider()
+ case "dnsimple":
+ provider, err = dnsimple.NewDNSProvider()
+ case "dnsmadeeasy":
+ provider, err = dnsmadeeasy.NewDNSProvider()
+ case "dyn":
+ provider, err = dyn.NewDNSProvider()
+ case "gandi":
+ provider, err = gandi.NewDNSProvider()
+ case "gcloud":
+ provider, err = googlecloud.NewDNSProvider()
+ case "linode":
+ provider, err = linode.NewDNSProvider()
+ case "manual":
+ provider, err = acme.NewDNSProviderManual()
+ case "namecheap":
+ provider, err = namecheap.NewDNSProvider()
+ case "route53":
+ provider, err = route53.NewDNSProvider()
+ case "rfc2136":
+ provider, err = rfc2136.NewDNSProvider()
+ case "vultr":
+ provider, err = vultr.NewDNSProvider()
+ case "ovh":
+ provider, err = ovh.NewDNSProvider()
+ case "pdns":
+ provider, err = pdns.NewDNSProvider()
+ }
+
+ if err != nil {
+ logger().Fatal(err)
+ }
+
+ client.SetChallengeProvider(acme.DNS01, provider)
+
+ // --dns=foo indicates that the user specifically want to do a DNS challenge
+ // infer that the user also wants to exclude all other challenges
+ client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
+ }
+
+ return conf, acc, client
+}
+
+func saveCertRes(certRes acme.CertificateResource, conf *Configuration) {
+ // We store the certificate, private key and metadata in different files
+ // as web servers would not be able to work with a combined file.
+ certOut := path.Join(conf.CertPath(), certRes.Domain+".crt")
+ privOut := path.Join(conf.CertPath(), certRes.Domain+".key")
+ pemOut := path.Join(conf.CertPath(), certRes.Domain+".pem")
+ metaOut := path.Join(conf.CertPath(), certRes.Domain+".json")
+
+ err := ioutil.WriteFile(certOut, certRes.Certificate, 0600)
+ if err != nil {
+ logger().Fatalf("Unable to save Certificate for domain %s\n\t%s", certRes.Domain, err.Error())
+ }
+
+ if certRes.PrivateKey != nil {
+ // if we were given a CSR, we don't know the private key
+ err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
+ if err != nil {
+ logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error())
+ }
+
+ if conf.context.GlobalBool("pem") {
+ err = ioutil.WriteFile(pemOut, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil), 0600)
+ if err != nil {
+ logger().Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%s", certRes.Domain, err.Error())
+ }
+ }
+
+ } else if conf.context.GlobalBool("pem") {
+ // we don't have the private key; can't write the .pem file
+ logger().Fatalf("Unable to save pem without private key for domain %s\n\t%s; are you using a CSR?", certRes.Domain, err.Error())
+ }
+
+ jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
+ if err != nil {
+ logger().Fatalf("Unable to marshal CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
+ }
+
+ err = ioutil.WriteFile(metaOut, jsonBytes, 0600)
+ if err != nil {
+ logger().Fatalf("Unable to save CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
+ }
+}
+
+func handleTOS(c *cli.Context, client *acme.Client, acc *Account) {
+ // Check for a global accept override
+ if c.GlobalBool("accept-tos") {
+ err := client.AgreeToTOS()
+ if err != nil {
+ logger().Fatalf("Could not agree to TOS: %s", err.Error())
+ }
+
+ acc.Save()
+ return
+ }
+
+ reader := bufio.NewReader(os.Stdin)
+ logger().Printf("Please review the TOS at %s", acc.Registration.TosURL)
+
+ for {
+ logger().Println("Do you accept the TOS? Y/n")
+ text, err := reader.ReadString('\n')
+ if err != nil {
+ logger().Fatalf("Could not read from console: %s", err.Error())
+ }
+
+ text = strings.Trim(text, "\r\n")
+
+ if text == "n" {
+ logger().Fatal("You did not accept the TOS. Unable to proceed.")
+ }
+
+ if text == "Y" || text == "y" || text == "" {
+ err = client.AgreeToTOS()
+ if err != nil {
+ logger().Fatalf("Could not agree to TOS: %s", err.Error())
+ }
+ acc.Save()
+ break
+ }
+
+ logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.")
+ }
+}
+
+func readCSRFile(filename string) (*x509.CertificateRequest, error) {
+ bytes, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ raw := bytes
+
+ // see if we can find a PEM-encoded CSR
+ var p *pem.Block
+ rest := bytes
+ for {
+ // decode a PEM block
+ p, rest = pem.Decode(rest)
+
+ // did we fail?
+ if p == nil {
+ break
+ }
+
+ // did we get a CSR?
+ if p.Type == "CERTIFICATE REQUEST" {
+ raw = p.Bytes
+ }
+ }
+
+ // no PEM-encoded CSR
+ // assume we were given a DER-encoded ASN.1 CSR
+ // (if this assumption is wrong, parsing these bytes will fail)
+ return x509.ParseCertificateRequest(raw)
+}
+
+func run(c *cli.Context) error {
+ conf, acc, client := setup(c)
+ if acc.Registration == nil {
+ reg, err := client.Register()
+ if err != nil {
+ logger().Fatalf("Could not complete registration\n\t%s", err.Error())
+ }
+
+ acc.Registration = reg
+ acc.Save()
+
+ logger().Print("!!!! HEADS UP !!!!")
+ logger().Printf(`
+ Your account credentials have been saved in your Let's Encrypt
+ configuration directory at "%s".
+ You should make a secure backup of this folder now. This
+ configuration directory will also contain certificates and
+ private keys obtained from Let's Encrypt so making regular
+ backups of this folder is ideal.`, conf.AccountPath(c.GlobalString("email")))
+
+ }
+
+ // If the agreement URL is empty, the account still needs to accept the LE TOS.
+ if acc.Registration.Body.Agreement == "" {
+ handleTOS(c, client, acc)
+ }
+
+ // we require either domains or csr, but not both
+ hasDomains := len(c.GlobalStringSlice("domains")) > 0
+ hasCsr := len(c.GlobalString("csr")) > 0
+ if hasDomains && hasCsr {
+ logger().Fatal("Please specify either --domains/-d or --csr/-c, but not both")
+ }
+ if !hasDomains && !hasCsr {
+ logger().Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
+ }
+
+ var cert acme.CertificateResource
+ var failures map[string]error
+
+ if hasDomains {
+ // obtain a certificate, generating a new private key
+ cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil)
+ } else {
+ // read the CSR
+ csr, err := readCSRFile(c.GlobalString("csr"))
+ if err != nil {
+ // we couldn't read the CSR
+ failures = map[string]error{"csr": err}
+ } else {
+ // obtain a certificate for this CSR
+ cert, failures = client.ObtainCertificateForCSR(*csr, !c.Bool("no-bundle"))
+ }
+ }
+
+ if len(failures) > 0 {
+ for k, v := range failures {
+ logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())
+ }
+
+ // Make sure to return a non-zero exit code if ObtainSANCertificate
+ // returned at least one error. Due to us not returning partial
+ // certificate we can just exit here instead of at the end.
+ os.Exit(1)
+ }
+
+ err := checkFolder(conf.CertPath())
+ if err != nil {
+ logger().Fatalf("Could not check/create path: %s", err.Error())
+ }
+
+ saveCertRes(cert, conf)
+
+ return nil
+}
+
+func revoke(c *cli.Context) error {
+
+ conf, _, client := setup(c)
+
+ err := checkFolder(conf.CertPath())
+ if err != nil {
+ logger().Fatalf("Could not check/create path: %s", err.Error())
+ }
+
+ for _, domain := range c.GlobalStringSlice("domains") {
+ logger().Printf("Trying to revoke certificate for domain %s", domain)
+
+ certPath := path.Join(conf.CertPath(), domain+".crt")
+ certBytes, err := ioutil.ReadFile(certPath)
+
+ err = client.RevokeCertificate(certBytes)
+ if err != nil {
+ logger().Fatalf("Error while revoking the certificate for domain %s\n\t%s", domain, err.Error())
+ } else {
+ logger().Print("Certificate was revoked.")
+ }
+ }
+
+ return nil
+}
+
+func renew(c *cli.Context) error {
+ conf, _, client := setup(c)
+
+ if len(c.GlobalStringSlice("domains")) <= 0 {
+ logger().Fatal("Please specify at least one domain.")
+ }
+
+ domain := c.GlobalStringSlice("domains")[0]
+
+ // load the cert resource from files.
+ // We store the certificate, private key and metadata in different files
+ // as web servers would not be able to work with a combined file.
+ certPath := path.Join(conf.CertPath(), domain+".crt")
+ privPath := path.Join(conf.CertPath(), domain+".key")
+ metaPath := path.Join(conf.CertPath(), domain+".json")
+
+ certBytes, err := ioutil.ReadFile(certPath)
+ if err != nil {
+ logger().Fatalf("Error while loading the certificate for domain %s\n\t%s", domain, err.Error())
+ }
+
+ if c.IsSet("days") {
+ expTime, err := acme.GetPEMCertExpiration(certBytes)
+ if err != nil {
+ logger().Printf("Could not get Certification expiration for domain %s", domain)
+ }
+
+ if int(expTime.Sub(time.Now()).Hours()/24.0) > c.Int("days") {
+ return nil
+ }
+ }
+
+ metaBytes, err := ioutil.ReadFile(metaPath)
+ if err != nil {
+ logger().Fatalf("Error while loading the meta data for domain %s\n\t%s", domain, err.Error())
+ }
+
+ var certRes acme.CertificateResource
+ err = json.Unmarshal(metaBytes, &certRes)
+ if err != nil {
+ logger().Fatalf("Error while marshalling the meta data for domain %s\n\t%s", domain, err.Error())
+ }
+
+ if c.Bool("reuse-key") {
+ keyBytes, err := ioutil.ReadFile(privPath)
+ if err != nil {
+ logger().Fatalf("Error while loading the private key for domain %s\n\t%s", domain, err.Error())
+ }
+ certRes.PrivateKey = keyBytes
+ }
+
+ certRes.Certificate = certBytes
+
+ newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"))
+ if err != nil {
+ logger().Fatalf("%s", err.Error())
+ }
+
+ saveCertRes(newCert, conf)
+
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/configuration.go b/vendor/github.com/xenolf/lego/configuration.go
new file mode 100644
index 000000000..f92c1fe96
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/configuration.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/urfave/cli"
+ "github.com/xenolf/lego/acme"
+)
+
+// Configuration type from CLI and config files.
+type Configuration struct {
+ context *cli.Context
+}
+
+// NewConfiguration creates a new configuration from CLI data.
+func NewConfiguration(c *cli.Context) *Configuration {
+ return &Configuration{context: c}
+}
+
+// KeyType the type from which private keys should be generated
+func (c *Configuration) KeyType() (acme.KeyType, error) {
+ switch strings.ToUpper(c.context.GlobalString("key-type")) {
+ case "RSA2048":
+ return acme.RSA2048, nil
+ case "RSA4096":
+ return acme.RSA4096, nil
+ case "RSA8192":
+ return acme.RSA8192, nil
+ case "EC256":
+ return acme.EC256, nil
+ case "EC384":
+ return acme.EC384, nil
+ }
+
+ return "", fmt.Errorf("Unsupported KeyType: %s", c.context.GlobalString("key-type"))
+}
+
+// ExcludedSolvers is a list of solvers that are to be excluded.
+func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) {
+ for _, s := range c.context.GlobalStringSlice("exclude") {
+ cc = append(cc, acme.Challenge(s))
+ }
+ return
+}
+
+// ServerPath returns the OS dependent path to the data for a specific CA
+func (c *Configuration) ServerPath() string {
+ srv, _ := url.Parse(c.context.GlobalString("server"))
+ srvStr := strings.Replace(srv.Host, ":", "_", -1)
+ return strings.Replace(srvStr, "/", string(os.PathSeparator), -1)
+}
+
+// CertPath gets the path for certificates.
+func (c *Configuration) CertPath() string {
+ return path.Join(c.context.GlobalString("path"), "certificates")
+}
+
+// AccountsPath returns the OS dependent path to the
+// local accounts for a specific CA
+func (c *Configuration) AccountsPath() string {
+ return path.Join(c.context.GlobalString("path"), "accounts", c.ServerPath())
+}
+
+// AccountPath returns the OS dependent path to a particular account
+func (c *Configuration) AccountPath(acc string) string {
+ return path.Join(c.AccountsPath(), acc)
+}
+
+// AccountKeysPath returns the OS dependent path to the keys of a particular account
+func (c *Configuration) AccountKeysPath(acc string) string {
+ return path.Join(c.AccountPath(acc), "keys")
+}
diff --git a/vendor/github.com/xenolf/lego/crypto.go b/vendor/github.com/xenolf/lego/crypto.go
new file mode 100644
index 000000000..8b23e2fc1
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/crypto.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "io/ioutil"
+ "os"
+)
+
+func generatePrivateKey(file string) (crypto.PrivateKey, error) {
+
+ privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+
+ keyBytes, err := x509.MarshalECPrivateKey(privateKey)
+ if err != nil {
+ return nil, err
+ }
+
+ pemKey := pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
+
+ certOut, err := os.Create(file)
+ if err != nil {
+ return nil, err
+ }
+
+ pem.Encode(certOut, &pemKey)
+ certOut.Close()
+
+ return privateKey, nil
+}
+
+func loadPrivateKey(file string) (crypto.PrivateKey, error) {
+ keyBytes, err := ioutil.ReadFile(file)
+ if err != nil {
+ return nil, err
+ }
+
+ keyBlock, _ := pem.Decode(keyBytes)
+
+ switch keyBlock.Type {
+ case "RSA PRIVATE KEY":
+ return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
+ case "EC PRIVATE KEY":
+ return x509.ParseECPrivateKey(keyBlock.Bytes)
+ }
+
+ return nil, errors.New("Unknown private key type.")
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go
new file mode 100644
index 000000000..84952238d
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go
@@ -0,0 +1,223 @@
+// Package cloudflare implements a DNS provider for solving the DNS-01
+// challenge using cloudflare DNS.
+package cloudflare
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// CloudFlareAPIURL represents the API endpoint to call.
+// TODO: Unexport?
+const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4"
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+type DNSProvider struct {
+ authEmail string
+ authKey string
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for cloudflare.
+// Credentials must be passed in the environment variables: CLOUDFLARE_EMAIL
+// and CLOUDFLARE_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ email := os.Getenv("CLOUDFLARE_EMAIL")
+ key := os.Getenv("CLOUDFLARE_API_KEY")
+ return NewDNSProviderCredentials(email, key)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for cloudflare.
+func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
+ if email == "" || key == "" {
+ return nil, fmt.Errorf("CloudFlare credentials missing")
+ }
+
+ return &DNSProvider{
+ authEmail: email,
+ authKey: key,
+ }, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. Adjusting here to cope with spikes in propagation times.
+func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 120 * time.Second, 2 * time.Second
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge
+func (c *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zoneID, err := c.getHostedZoneID(fqdn)
+ if err != nil {
+ return err
+ }
+
+ rec := cloudFlareRecord{
+ Type: "TXT",
+ Name: acme.UnFqdn(fqdn),
+ Content: value,
+ TTL: 120,
+ }
+
+ body, err := json.Marshal(rec)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ record, err := c.findTxtRecord(fqdn)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
+ // HostedZone represents a CloudFlare DNS zone
+ type HostedZone struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ }
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return "", err
+ }
+
+ result, err := c.makeRequest("GET", "/zones?name="+acme.UnFqdn(authZone), nil)
+ if err != nil {
+ return "", err
+ }
+
+ var hostedZone []HostedZone
+ err = json.Unmarshal(result, &hostedZone)
+ if err != nil {
+ return "", err
+ }
+
+ if len(hostedZone) != 1 {
+ return "", fmt.Errorf("Zone %s not found in CloudFlare for domain %s", authZone, fqdn)
+ }
+
+ return hostedZone[0].ID, nil
+}
+
+func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
+ zoneID, err := c.getHostedZoneID(fqdn)
+ if err != nil {
+ return nil, err
+ }
+
+ result, err := c.makeRequest(
+ "GET",
+ fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)),
+ nil,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ var records []cloudFlareRecord
+ err = json.Unmarshal(result, &records)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, rec := range records {
+ if rec.Name == acme.UnFqdn(fqdn) {
+ return &rec, nil
+ }
+ }
+
+ return nil, fmt.Errorf("No existing record found for %s", fqdn)
+}
+
+func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
+ // APIError contains error details for failed requests
+ type APIError struct {
+ Code int `json:"code,omitempty"`
+ Message string `json:"message,omitempty"`
+ ErrorChain []APIError `json:"error_chain,omitempty"`
+ }
+
+ // APIResponse represents a response from CloudFlare API
+ type APIResponse struct {
+ Success bool `json:"success"`
+ Errors []*APIError `json:"errors"`
+ Result json.RawMessage `json:"result"`
+ }
+
+ req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareAPIURL, uri), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("X-Auth-Email", c.authEmail)
+ req.Header.Set("X-Auth-Key", c.authKey)
+ //req.Header.Set("User-Agent", userAgent())
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("Error querying Cloudflare API -> %v", err)
+ }
+
+ defer resp.Body.Close()
+
+ var r APIResponse
+ err = json.NewDecoder(resp.Body).Decode(&r)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.Success {
+ if len(r.Errors) > 0 {
+ errStr := ""
+ for _, apiErr := range r.Errors {
+ errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
+ for _, chainErr := range apiErr.ErrorChain {
+ errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
+ }
+ }
+ return nil, fmt.Errorf("Cloudflare API Error \n%s", errStr)
+ }
+ return nil, fmt.Errorf("Cloudflare API error")
+ }
+
+ return r.Result, nil
+}
+
+// cloudFlareRecord represents a CloudFlare DNS record
+type cloudFlareRecord struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Content string `json:"content"`
+ ID string `json:"id,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ ZoneID string `json:"zone_id,omitempty"`
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go
new file mode 100644
index 000000000..19b5a40b9
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go
@@ -0,0 +1,80 @@
+package cloudflare
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ cflareLiveTest bool
+ cflareEmail string
+ cflareAPIKey string
+ cflareDomain string
+)
+
+func init() {
+ cflareEmail = os.Getenv("CLOUDFLARE_EMAIL")
+ cflareAPIKey = os.Getenv("CLOUDFLARE_API_KEY")
+ cflareDomain = os.Getenv("CLOUDFLARE_DOMAIN")
+ if len(cflareEmail) > 0 && len(cflareAPIKey) > 0 && len(cflareDomain) > 0 {
+ cflareLiveTest = true
+ }
+}
+
+func restoreCloudFlareEnv() {
+ os.Setenv("CLOUDFLARE_EMAIL", cflareEmail)
+ os.Setenv("CLOUDFLARE_API_KEY", cflareAPIKey)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ os.Setenv("CLOUDFLARE_EMAIL", "")
+ os.Setenv("CLOUDFLARE_API_KEY", "")
+ _, err := NewDNSProviderCredentials("123", "123")
+ assert.NoError(t, err)
+ restoreCloudFlareEnv()
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("CLOUDFLARE_EMAIL", "test@example.com")
+ os.Setenv("CLOUDFLARE_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restoreCloudFlareEnv()
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("CLOUDFLARE_EMAIL", "")
+ os.Setenv("CLOUDFLARE_API_KEY", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "CloudFlare credentials missing")
+ restoreCloudFlareEnv()
+}
+
+func TestCloudFlarePresent(t *testing.T) {
+ if !cflareLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(cflareEmail, cflareAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.Present(cflareDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestCloudFlareCleanUp(t *testing.T) {
+ if !cflareLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 2)
+
+ provider, err := NewDNSProviderCredentials(cflareEmail, cflareAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(cflareDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go
new file mode 100644
index 000000000..da261b39a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go
@@ -0,0 +1,166 @@
+// Package digitalocean implements a DNS provider for solving the DNS-01
+// challenge using digitalocean DNS.
+package digitalocean
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+// that uses DigitalOcean's REST API to manage TXT records for a domain.
+type DNSProvider struct {
+ apiAuthToken string
+ recordIDs map[string]int
+ recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Digital
+// Ocean. Credentials must be passed in the environment variable:
+// DO_AUTH_TOKEN.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiAuthToken := os.Getenv("DO_AUTH_TOKEN")
+ return NewDNSProviderCredentials(apiAuthToken)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Digital Ocean.
+func NewDNSProviderCredentials(apiAuthToken string) (*DNSProvider, error) {
+ if apiAuthToken == "" {
+ return nil, fmt.Errorf("DigitalOcean credentials missing")
+ }
+ return &DNSProvider{
+ apiAuthToken: apiAuthToken,
+ recordIDs: make(map[string]int),
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ // txtRecordRequest represents the request body to DO's API to make a TXT record
+ type txtRecordRequest struct {
+ RecordType string `json:"type"`
+ Name string `json:"name"`
+ Data string `json:"data"`
+ }
+
+ // txtRecordResponse represents a response from DO's API after making a TXT record
+ type txtRecordResponse struct {
+ DomainRecord struct {
+ ID int `json:"id"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Data string `json:"data"`
+ } `json:"domain_record"`
+ }
+
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+
+ reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone)
+ reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value}
+ body, err := json.Marshal(reqData)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", reqURL, bytes.NewReader(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken))
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ var errInfo digitalOceanAPIError
+ json.NewDecoder(resp.Body).Decode(&errInfo)
+ return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
+ }
+
+ // Everything looks good; but we'll need the ID later to delete the record
+ var respData txtRecordResponse
+ err = json.NewDecoder(resp.Body).Decode(&respData)
+ if err != nil {
+ return err
+ }
+ d.recordIDsMu.Lock()
+ d.recordIDs[fqdn] = respData.DomainRecord.ID
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ // get the record's unique ID from when we created it
+ d.recordIDsMu.Lock()
+ recordID, ok := d.recordIDs[fqdn]
+ d.recordIDsMu.Unlock()
+ if !ok {
+ return fmt.Errorf("unknown record ID for '%s'", fqdn)
+ }
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+
+ reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", digitalOceanBaseURL, authZone, recordID)
+ req, err := http.NewRequest("DELETE", reqURL, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken))
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ var errInfo digitalOceanAPIError
+ json.NewDecoder(resp.Body).Decode(&errInfo)
+ return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
+ }
+
+ // Delete record ID from map
+ d.recordIDsMu.Lock()
+ delete(d.recordIDs, fqdn)
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+type digitalOceanAPIError struct {
+ ID string `json:"id"`
+ Message string `json:"message"`
+}
+
+var digitalOceanBaseURL = "https://api.digitalocean.com"
diff --git a/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go
new file mode 100644
index 000000000..7498508ba
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go
@@ -0,0 +1,117 @@
+package digitalocean
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+var fakeDigitalOceanAuth = "asdf1234"
+
+func TestDigitalOceanPresent(t *testing.T) {
+ var requestReceived bool
+
+ mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ requestReceived = true
+
+ if got, want := r.Method, "POST"; got != want {
+ t.Errorf("Expected method to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.URL.Path, "/v2/domains/example.com/records"; got != want {
+ t.Errorf("Expected path to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.Header.Get("Content-Type"), "application/json"; got != want {
+ t.Errorf("Expected Content-Type to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.Header.Get("Authorization"), "Bearer asdf1234"; got != want {
+ t.Errorf("Expected Authorization to be '%s' but got '%s'", want, got)
+ }
+
+ reqBody, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Fatalf("Error reading request body: %v", err)
+ }
+ if got, want := string(reqBody), `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"}`; got != want {
+ t.Errorf("Expected body data to be: `%s` but got `%s`", want, got)
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "domain_record": {
+ "id": 1234567,
+ "type": "TXT",
+ "name": "_acme-challenge",
+ "data": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI",
+ "priority": null,
+ "port": null,
+ "weight": null
+ }
+ }`)
+ }))
+ defer mock.Close()
+ digitalOceanBaseURL = mock.URL
+
+ doprov, err := NewDNSProviderCredentials(fakeDigitalOceanAuth)
+ if doprov == nil {
+ t.Fatal("Expected non-nil DigitalOcean provider, but was nil")
+ }
+ if err != nil {
+ t.Fatalf("Expected no error creating provider, but got: %v", err)
+ }
+
+ err = doprov.Present("example.com", "", "foobar")
+ if err != nil {
+ t.Fatalf("Expected no error creating TXT record, but got: %v", err)
+ }
+ if !requestReceived {
+ t.Error("Expected request to be received by mock backend, but it wasn't")
+ }
+}
+
+func TestDigitalOceanCleanUp(t *testing.T) {
+ var requestReceived bool
+
+ mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ requestReceived = true
+
+ if got, want := r.Method, "DELETE"; got != want {
+ t.Errorf("Expected method to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.URL.Path, "/v2/domains/example.com/records/1234567"; got != want {
+ t.Errorf("Expected path to be '%s' but got '%s'", want, got)
+ }
+ // NOTE: Even though the body is empty, DigitalOcean API docs still show setting this Content-Type...
+ if got, want := r.Header.Get("Content-Type"), "application/json"; got != want {
+ t.Errorf("Expected Content-Type to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.Header.Get("Authorization"), "Bearer asdf1234"; got != want {
+ t.Errorf("Expected Authorization to be '%s' but got '%s'", want, got)
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+ }))
+ defer mock.Close()
+ digitalOceanBaseURL = mock.URL
+
+ doprov, err := NewDNSProviderCredentials(fakeDigitalOceanAuth)
+ if doprov == nil {
+ t.Fatal("Expected non-nil DigitalOcean provider, but was nil")
+ }
+ if err != nil {
+ t.Fatalf("Expected no error creating provider, but got: %v", err)
+ }
+
+ doprov.recordIDsMu.Lock()
+ doprov.recordIDs["_acme-challenge.example.com."] = 1234567
+ doprov.recordIDsMu.Unlock()
+
+ err = doprov.CleanUp("example.com", "", "")
+ if err != nil {
+ t.Fatalf("Expected no error removing TXT record, but got: %v", err)
+ }
+ if !requestReceived {
+ t.Error("Expected request to be received by mock backend, but it wasn't")
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go
new file mode 100644
index 000000000..c903a35ce
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go
@@ -0,0 +1,141 @@
+// Package dnsimple implements a DNS provider for solving the DNS-01 challenge
+// using dnsimple DNS.
+package dnsimple
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/weppos/dnsimple-go/dnsimple"
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface.
+type DNSProvider struct {
+ client *dnsimple.Client
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for dnsimple.
+// Credentials must be passed in the environment variables: DNSIMPLE_EMAIL
+// and DNSIMPLE_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ email := os.Getenv("DNSIMPLE_EMAIL")
+ key := os.Getenv("DNSIMPLE_API_KEY")
+ return NewDNSProviderCredentials(email, key)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for dnsimple.
+func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
+ if email == "" || key == "" {
+ return nil, fmt.Errorf("DNSimple credentials missing")
+ }
+
+ return &DNSProvider{
+ client: dnsimple.NewClient(key, email),
+ }, nil
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge.
+func (c *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+
+ zoneID, zoneName, err := c.getHostedZone(domain)
+ if err != nil {
+ return err
+ }
+
+ recordAttributes := c.newTxtRecord(zoneName, fqdn, value, ttl)
+ _, _, err = c.client.Domains.CreateRecord(zoneID, *recordAttributes)
+ if err != nil {
+ return fmt.Errorf("DNSimple API call failed: %v", err)
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ records, err := c.findTxtRecords(domain, fqdn)
+ if err != nil {
+ return err
+ }
+
+ for _, rec := range records {
+ _, err := c.client.Domains.DeleteRecord(rec.DomainId, rec.Id)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *DNSProvider) getHostedZone(domain string) (string, string, error) {
+ zones, _, err := c.client.Domains.List()
+ if err != nil {
+ return "", "", fmt.Errorf("DNSimple API call failed: %v", err)
+ }
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return "", "", err
+ }
+
+ var hostedZone dnsimple.Domain
+ for _, zone := range zones {
+ if zone.Name == acme.UnFqdn(authZone) {
+ hostedZone = zone
+ }
+ }
+
+ if hostedZone.Id == 0 {
+ return "", "", fmt.Errorf("Zone %s not found in DNSimple for domain %s", authZone, domain)
+
+ }
+
+ return fmt.Sprintf("%v", hostedZone.Id), hostedZone.Name, nil
+}
+
+func (c *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnsimple.Record, error) {
+ zoneID, zoneName, err := c.getHostedZone(domain)
+ if err != nil {
+ return nil, err
+ }
+
+ var records []dnsimple.Record
+ result, _, err := c.client.Domains.ListRecords(zoneID, "", "TXT")
+ if err != nil {
+ return records, fmt.Errorf("DNSimple API call has failed: %v", err)
+ }
+
+ recordName := c.extractRecordName(fqdn, zoneName)
+ for _, record := range result {
+ if record.Name == recordName {
+ records = append(records, record)
+ }
+ }
+
+ return records, nil
+}
+
+func (c *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnsimple.Record {
+ name := c.extractRecordName(fqdn, zone)
+
+ return &dnsimple.Record{
+ Type: "TXT",
+ Name: name,
+ Content: value,
+ TTL: ttl,
+ }
+}
+
+func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
+ name := acme.UnFqdn(fqdn)
+ if idx := strings.Index(name, "."+domain); idx != -1 {
+ return name[:idx]
+ }
+ return name
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go
new file mode 100644
index 000000000..4926b3df9
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go
@@ -0,0 +1,79 @@
+package dnsimple
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ dnsimpleLiveTest bool
+ dnsimpleEmail string
+ dnsimpleAPIKey string
+ dnsimpleDomain string
+)
+
+func init() {
+ dnsimpleEmail = os.Getenv("DNSIMPLE_EMAIL")
+ dnsimpleAPIKey = os.Getenv("DNSIMPLE_API_KEY")
+ dnsimpleDomain = os.Getenv("DNSIMPLE_DOMAIN")
+ if len(dnsimpleEmail) > 0 && len(dnsimpleAPIKey) > 0 && len(dnsimpleDomain) > 0 {
+ dnsimpleLiveTest = true
+ }
+}
+
+func restoreDNSimpleEnv() {
+ os.Setenv("DNSIMPLE_EMAIL", dnsimpleEmail)
+ os.Setenv("DNSIMPLE_API_KEY", dnsimpleAPIKey)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ os.Setenv("DNSIMPLE_EMAIL", "")
+ os.Setenv("DNSIMPLE_API_KEY", "")
+ _, err := NewDNSProviderCredentials("example@example.com", "123")
+ assert.NoError(t, err)
+ restoreDNSimpleEnv()
+}
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("DNSIMPLE_EMAIL", "example@example.com")
+ os.Setenv("DNSIMPLE_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restoreDNSimpleEnv()
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("DNSIMPLE_EMAIL", "")
+ os.Setenv("DNSIMPLE_API_KEY", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "DNSimple credentials missing")
+ restoreDNSimpleEnv()
+}
+
+func TestLiveDNSimplePresent(t *testing.T) {
+ if !dnsimpleLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(dnsimpleEmail, dnsimpleAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.Present(dnsimpleDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveDNSimpleCleanUp(t *testing.T) {
+ if !dnsimpleLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProviderCredentials(dnsimpleEmail, dnsimpleAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(dnsimpleDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go
new file mode 100644
index 000000000..c4363a4eb
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go
@@ -0,0 +1,248 @@
+package dnsmadeeasy
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha1"
+ "crypto/tls"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
+// DNSMadeEasy's DNS API to manage TXT records for a domain.
+type DNSProvider struct {
+ baseURL string
+ apiKey string
+ apiSecret string
+}
+
+// Domain holds the DNSMadeEasy API representation of a Domain
+type Domain struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+}
+
+// Record holds the DNSMadeEasy API representation of a Domain Record
+type Record struct {
+ ID int `json:"id"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Value string `json:"value"`
+ TTL int `json:"ttl"`
+ SourceID int `json:"sourceId"`
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for DNSMadeEasy DNS.
+// Credentials must be passed in the environment variables: DNSMADEEASY_API_KEY
+// and DNSMADEEASY_API_SECRET.
+func NewDNSProvider() (*DNSProvider, error) {
+ dnsmadeeasyAPIKey := os.Getenv("DNSMADEEASY_API_KEY")
+ dnsmadeeasyAPISecret := os.Getenv("DNSMADEEASY_API_SECRET")
+ dnsmadeeasySandbox := os.Getenv("DNSMADEEASY_SANDBOX")
+
+ var baseURL string
+
+ sandbox, _ := strconv.ParseBool(dnsmadeeasySandbox)
+ if sandbox {
+ baseURL = "https://api.sandbox.dnsmadeeasy.com/V2.0"
+ } else {
+ baseURL = "https://api.dnsmadeeasy.com/V2.0"
+ }
+
+ return NewDNSProviderCredentials(baseURL, dnsmadeeasyAPIKey, dnsmadeeasyAPISecret)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for DNSMadeEasy.
+func NewDNSProviderCredentials(baseURL, apiKey, apiSecret string) (*DNSProvider, error) {
+ if baseURL == "" || apiKey == "" || apiSecret == "" {
+ return nil, fmt.Errorf("DNS Made Easy credentials missing")
+ }
+
+ return &DNSProvider{
+ baseURL: baseURL,
+ apiKey: apiKey,
+ apiSecret: apiSecret,
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters
+func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domainName, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ // fetch the domain details
+ domain, err := d.getDomain(authZone)
+ if err != nil {
+ return err
+ }
+
+ // create the TXT record
+ name := strings.Replace(fqdn, "."+authZone, "", 1)
+ record := &Record{Type: "TXT", Name: name, Value: value, TTL: ttl}
+
+ err = d.createRecord(domain, record)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT records matching the specified parameters
+func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domainName, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ // fetch the domain details
+ domain, err := d.getDomain(authZone)
+ if err != nil {
+ return err
+ }
+
+ // find matching records
+ name := strings.Replace(fqdn, "."+authZone, "", 1)
+ records, err := d.getRecords(domain, name, "TXT")
+ if err != nil {
+ return err
+ }
+
+ // delete records
+ for _, record := range *records {
+ err = d.deleteRecord(record)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) getDomain(authZone string) (*Domain, error) {
+ domainName := authZone[0 : len(authZone)-1]
+ resource := fmt.Sprintf("%s%s", "/dns/managed/name?domainname=", domainName)
+
+ resp, err := d.sendRequest("GET", resource, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ domain := &Domain{}
+ err = json.NewDecoder(resp.Body).Decode(&domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return domain, nil
+}
+
+func (d *DNSProvider) getRecords(domain *Domain, recordName, recordType string) (*[]Record, error) {
+ resource := fmt.Sprintf("%s/%d/%s%s%s%s", "/dns/managed", domain.ID, "records?recordName=", recordName, "&type=", recordType)
+
+ resp, err := d.sendRequest("GET", resource, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ type recordsResponse struct {
+ Records *[]Record `json:"data"`
+ }
+
+ records := &recordsResponse{}
+ err = json.NewDecoder(resp.Body).Decode(&records)
+ if err != nil {
+ return nil, err
+ }
+
+ return records.Records, nil
+}
+
+func (d *DNSProvider) createRecord(domain *Domain, record *Record) error {
+ url := fmt.Sprintf("%s/%d/%s", "/dns/managed", domain.ID, "records")
+
+ resp, err := d.sendRequest("POST", url, record)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+func (d *DNSProvider) deleteRecord(record Record) error {
+ resource := fmt.Sprintf("%s/%d/%s/%d", "/dns/managed", record.SourceID, "records", record.ID)
+
+ resp, err := d.sendRequest("DELETE", resource, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*http.Response, error) {
+ url := fmt.Sprintf("%s%s", d.baseURL, resource)
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp := time.Now().UTC().Format(time.RFC1123)
+ signature := computeHMAC(timestamp, d.apiSecret)
+
+ req, err := http.NewRequest(method, url, bytes.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("x-dnsme-apiKey", d.apiKey)
+ req.Header.Set("x-dnsme-requestDate", timestamp)
+ req.Header.Set("x-dnsme-hmac", signature)
+ req.Header.Set("accept", "application/json")
+ req.Header.Set("content-type", "application/json")
+
+ transport := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+ client := &http.Client{
+ Transport: transport,
+ Timeout: time.Duration(10 * time.Second),
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode > 299 {
+ return nil, fmt.Errorf("DNSMadeEasy API request failed with HTTP status code %d", resp.StatusCode)
+ }
+
+ return resp, nil
+}
+
+func computeHMAC(message string, secret string) string {
+ key := []byte(secret)
+ h := hmac.New(sha1.New, key)
+ h.Write([]byte(message))
+ return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go
new file mode 100644
index 000000000..e860ecb69
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go
@@ -0,0 +1,37 @@
+package dnsmadeeasy
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ testLive bool
+ testAPIKey string
+ testAPISecret string
+ testDomain string
+)
+
+func init() {
+ testAPIKey = os.Getenv("DNSMADEEASY_API_KEY")
+ testAPISecret = os.Getenv("DNSMADEEASY_API_SECRET")
+ testDomain = os.Getenv("DNSMADEEASY_DOMAIN")
+ os.Setenv("DNSMADEEASY_SANDBOX", "true")
+ testLive = len(testAPIKey) > 0 && len(testAPISecret) > 0
+}
+
+func TestPresentAndCleanup(t *testing.T) {
+ if !testLive {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+
+ err = provider.Present(testDomain, "", "123d==")
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(testDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go
new file mode 100644
index 000000000..384bc850c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go
@@ -0,0 +1,274 @@
+// Package dyn implements a DNS provider for solving the DNS-01 challenge
+// using Dyn Managed DNS.
+package dyn
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+var dynBaseURL = "https://api.dynect.net/REST"
+
+type dynResponse struct {
+ // One of 'success', 'failure', or 'incomplete'
+ Status string `json:"status"`
+
+ // The structure containing the actual results of the request
+ Data json.RawMessage `json:"data"`
+
+ // The ID of the job that was created in response to a request.
+ JobID int `json:"job_id"`
+
+ // A list of zero or more messages
+ Messages json.RawMessage `json:"msgs"`
+}
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
+// Dyn's Managed DNS API to manage TXT records for a domain.
+type DNSProvider struct {
+ customerName string
+ userName string
+ password string
+ token string
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
+// Credentials must be passed in the environment variables: DYN_CUSTOMER_NAME,
+// DYN_USER_NAME and DYN_PASSWORD.
+func NewDNSProvider() (*DNSProvider, error) {
+ customerName := os.Getenv("DYN_CUSTOMER_NAME")
+ userName := os.Getenv("DYN_USER_NAME")
+ password := os.Getenv("DYN_PASSWORD")
+ return NewDNSProviderCredentials(customerName, userName, password)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Dyn DNS.
+func NewDNSProviderCredentials(customerName, userName, password string) (*DNSProvider, error) {
+ if customerName == "" || userName == "" || password == "" {
+ return nil, fmt.Errorf("DynDNS credentials missing")
+ }
+
+ return &DNSProvider{
+ customerName: customerName,
+ userName: userName,
+ password: password,
+ }, nil
+}
+
+func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) {
+ url := fmt.Sprintf("%s/%s", dynBaseURL, resource)
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest(method, url, bytes.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ if len(d.token) > 0 {
+ req.Header.Set("Auth-Token", d.token)
+ }
+
+ client := &http.Client{Timeout: time.Duration(10 * time.Second)}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d", resp.StatusCode)
+ } else if resp.StatusCode == 307 {
+ // TODO add support for HTTP 307 response and long running jobs
+ return nil, fmt.Errorf("Dyn API request returned HTTP 307. This is currently unsupported")
+ }
+
+ var dynRes dynResponse
+ err = json.NewDecoder(resp.Body).Decode(&dynRes)
+ if err != nil {
+ return nil, err
+ }
+
+ if dynRes.Status == "failure" {
+ // TODO add better error handling
+ return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages)
+ }
+
+ return &dynRes, nil
+}
+
+// Starts a new Dyn API Session. Authenticates using customerName, userName,
+// password and receives a token to be used in for subsequent requests.
+func (d *DNSProvider) login() error {
+ type creds struct {
+ Customer string `json:"customer_name"`
+ User string `json:"user_name"`
+ Pass string `json:"password"`
+ }
+
+ type session struct {
+ Token string `json:"token"`
+ Version string `json:"version"`
+ }
+
+ payload := &creds{Customer: d.customerName, User: d.userName, Pass: d.password}
+ dynRes, err := d.sendRequest("POST", "Session", payload)
+ if err != nil {
+ return err
+ }
+
+ var s session
+ err = json.Unmarshal(dynRes.Data, &s)
+ if err != nil {
+ return err
+ }
+
+ d.token = s.Token
+
+ return nil
+}
+
+// Destroys Dyn Session
+func (d *DNSProvider) logout() error {
+ if len(d.token) == 0 {
+ // nothing to do
+ return nil
+ }
+
+ url := fmt.Sprintf("%s/Session", dynBaseURL)
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Auth-Token", d.token)
+
+ client := &http.Client{Timeout: time.Duration(10 * time.Second)}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("Dyn API request failed to delete session with HTTP status code %d", resp.StatusCode)
+ }
+
+ d.token = ""
+
+ return nil
+}
+
+// Present creates a TXT record using the specified parameters
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ err = d.login()
+ if err != nil {
+ return err
+ }
+
+ data := map[string]interface{}{
+ "rdata": map[string]string{
+ "txtdata": value,
+ },
+ "ttl": strconv.Itoa(ttl),
+ }
+
+ resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
+ _, err = d.sendRequest("POST", resource, data)
+ if err != nil {
+ return err
+ }
+
+ err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client")
+ if err != nil {
+ return err
+ }
+
+ err = d.logout()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) publish(zone, notes string) error {
+ type publish struct {
+ Publish bool `json:"publish"`
+ Notes string `json:"notes"`
+ }
+
+ pub := &publish{Publish: true, Notes: notes}
+ resource := fmt.Sprintf("Zone/%s/", zone)
+ _, err := d.sendRequest("PUT", resource, pub)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ err = d.login()
+ if err != nil {
+ return err
+ }
+
+ resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
+ url := fmt.Sprintf("%s/%s", dynBaseURL, resource)
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Auth-Token", d.token)
+
+ client := &http.Client{Timeout: time.Duration(10 * time.Second)}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("Dyn API request failed to delete TXT record HTTP status code %d", resp.StatusCode)
+ }
+
+ err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client")
+ if err != nil {
+ return err
+ }
+
+ err = d.logout()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go
new file mode 100644
index 000000000..0d28d5d0e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go
@@ -0,0 +1,53 @@
+package dyn
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ dynLiveTest bool
+ dynCustomerName string
+ dynUserName string
+ dynPassword string
+ dynDomain string
+)
+
+func init() {
+ dynCustomerName = os.Getenv("DYN_CUSTOMER_NAME")
+ dynUserName = os.Getenv("DYN_USER_NAME")
+ dynPassword = os.Getenv("DYN_PASSWORD")
+ dynDomain = os.Getenv("DYN_DOMAIN")
+ if len(dynCustomerName) > 0 && len(dynUserName) > 0 && len(dynPassword) > 0 && len(dynDomain) > 0 {
+ dynLiveTest = true
+ }
+}
+
+func TestLiveDynPresent(t *testing.T) {
+ if !dynLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.Present(dynDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveDynCleanUp(t *testing.T) {
+ if !dynLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(dynDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go
new file mode 100644
index 000000000..422b02a21
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go
@@ -0,0 +1,472 @@
+// Package gandi implements a DNS provider for solving the DNS-01
+// challenge using Gandi DNS.
+package gandi
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// Gandi API reference: http://doc.rpc.gandi.net/index.html
+// Gandi API domain examples: http://doc.rpc.gandi.net/domain/faq.html
+
+var (
+ // endpoint is the Gandi XML-RPC endpoint used by Present and
+ // CleanUp. It is overridden during tests.
+ endpoint = "https://rpc.gandi.net/xmlrpc/"
+ // findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
+ // during tests.
+ findZoneByFqdn = acme.FindZoneByFqdn
+)
+
+// inProgressInfo contains information about an in-progress challenge
+type inProgressInfo struct {
+ zoneID int // zoneID of gandi zone to restore in CleanUp
+ newZoneID int // zoneID of temporary gandi zone containing TXT record
+ authZone string // the domain name registered at gandi with trailing "."
+}
+
+// DNSProvider is an implementation of the
+// acme.ChallengeProviderTimeout interface that uses Gandi's XML-RPC
+// API to manage TXT records for a domain.
+type DNSProvider struct {
+ apiKey string
+ inProgressFQDNs map[string]inProgressInfo
+ inProgressAuthZones map[string]struct{}
+ inProgressMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Gandi.
+// Credentials must be passed in the environment variable: GANDI_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiKey := os.Getenv("GANDI_API_KEY")
+ return NewDNSProviderCredentials(apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Gandi.
+func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
+ if apiKey == "" {
+ return nil, fmt.Errorf("No Gandi API Key given")
+ }
+ return &DNSProvider{
+ apiKey: apiKey,
+ inProgressFQDNs: make(map[string]inProgressInfo),
+ inProgressAuthZones: make(map[string]struct{}),
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters. It
+// does this by creating and activating a new temporary Gandi DNS
+// zone. This new zone contains the TXT record.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+ if ttl < 300 {
+ ttl = 300 // 300 is gandi minimum value for ttl
+ }
+ // find authZone and Gandi zone_id for fqdn
+ authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
+ }
+ zoneID, err := d.getZoneID(authZone)
+ if err != nil {
+ return err
+ }
+ // determine name of TXT record
+ if !strings.HasSuffix(
+ strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
+ return fmt.Errorf(
+ "Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
+ }
+ name := fqdn[:len(fqdn)-len("."+authZone)]
+ // acquire lock and check there is not a challenge already in
+ // progress for this value of authZone
+ d.inProgressMu.Lock()
+ defer d.inProgressMu.Unlock()
+ if _, ok := d.inProgressAuthZones[authZone]; ok {
+ return fmt.Errorf(
+ "Gandi DNS: challenge already in progress for authZone %s",
+ authZone)
+ }
+ // perform API actions to create and activate new gandi zone
+ // containing the required TXT record
+ newZoneName := fmt.Sprintf(
+ "%s [ACME Challenge %s]",
+ acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
+ newZoneID, err := d.cloneZone(zoneID, newZoneName)
+ if err != nil {
+ return err
+ }
+ newZoneVersion, err := d.newZoneVersion(newZoneID)
+ if err != nil {
+ return err
+ }
+ err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, ttl)
+ if err != nil {
+ return err
+ }
+ err = d.setZoneVersion(newZoneID, newZoneVersion)
+ if err != nil {
+ return err
+ }
+ err = d.setZone(authZone, newZoneID)
+ if err != nil {
+ return err
+ }
+ // save data necessary for CleanUp
+ d.inProgressFQDNs[fqdn] = inProgressInfo{
+ zoneID: zoneID,
+ newZoneID: newZoneID,
+ authZone: authZone,
+ }
+ d.inProgressAuthZones[authZone] = struct{}{}
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified
+// parameters. It does this by restoring the old Gandi DNS zone and
+// removing the temporary one created by Present.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+ // acquire lock and retrieve zoneID, newZoneID and authZone
+ d.inProgressMu.Lock()
+ defer d.inProgressMu.Unlock()
+ if _, ok := d.inProgressFQDNs[fqdn]; !ok {
+ // if there is no cleanup information then just return
+ return nil
+ }
+ zoneID := d.inProgressFQDNs[fqdn].zoneID
+ newZoneID := d.inProgressFQDNs[fqdn].newZoneID
+ authZone := d.inProgressFQDNs[fqdn].authZone
+ delete(d.inProgressFQDNs, fqdn)
+ delete(d.inProgressAuthZones, authZone)
+ // perform API actions to restore old gandi zone for authZone
+ err := d.setZone(authZone, zoneID)
+ if err != nil {
+ return err
+ }
+ err = d.deleteZone(newZoneID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Timeout returns the values (40*time.Minute, 60*time.Second) which
+// are used by the acme package as timeout and check interval values
+// when checking for DNS record propagation with Gandi.
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 40 * time.Minute, 60 * time.Second
+}
+
+// types for XML-RPC method calls and parameters
+
+type param interface {
+ param()
+}
+type paramString struct {
+ XMLName xml.Name `xml:"param"`
+ Value string `xml:"value>string"`
+}
+type paramInt struct {
+ XMLName xml.Name `xml:"param"`
+ Value int `xml:"value>int"`
+}
+
+type structMember interface {
+ structMember()
+}
+type structMemberString struct {
+ Name string `xml:"name"`
+ Value string `xml:"value>string"`
+}
+type structMemberInt struct {
+ Name string `xml:"name"`
+ Value int `xml:"value>int"`
+}
+type paramStruct struct {
+ XMLName xml.Name `xml:"param"`
+ StructMembers []structMember `xml:"value>struct>member"`
+}
+
+func (p paramString) param() {}
+func (p paramInt) param() {}
+func (m structMemberString) structMember() {}
+func (m structMemberInt) structMember() {}
+func (p paramStruct) param() {}
+
+type methodCall struct {
+ XMLName xml.Name `xml:"methodCall"`
+ MethodName string `xml:"methodName"`
+ Params []param `xml:"params"`
+}
+
+// types for XML-RPC responses
+
+type response interface {
+ faultCode() int
+ faultString() string
+}
+
+type responseFault struct {
+ FaultCode int `xml:"fault>value>struct>member>value>int"`
+ FaultString string `xml:"fault>value>struct>member>value>string"`
+}
+
+func (r responseFault) faultCode() int { return r.FaultCode }
+func (r responseFault) faultString() string { return r.FaultString }
+
+type responseStruct struct {
+ responseFault
+ StructMembers []struct {
+ Name string `xml:"name"`
+ ValueInt int `xml:"value>int"`
+ } `xml:"params>param>value>struct>member"`
+}
+
+type responseInt struct {
+ responseFault
+ Value int `xml:"params>param>value>int"`
+}
+
+type responseBool struct {
+ responseFault
+ Value bool `xml:"params>param>value>boolean"`
+}
+
+// POSTing/Marshalling/Unmarshalling
+
+type rpcError struct {
+ faultCode int
+ faultString string
+}
+
+func (e rpcError) Error() string {
+ return fmt.Sprintf(
+ "Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString)
+}
+
+func httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
+ client := http.Client{Timeout: 60 * time.Second}
+ resp, err := client.Post(url, bodyType, body)
+ if err != nil {
+ return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
+ }
+ defer resp.Body.Close()
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
+ }
+ return b, nil
+}
+
+// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by
+// marshalling the data given in the call argument to XML and sending
+// that via HTTP Post to Gandi. The response is then unmarshalled into
+// the resp argument.
+func rpcCall(call *methodCall, resp response) error {
+ // marshal
+ b, err := xml.MarshalIndent(call, "", " ")
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: Marshal Error: %v", err)
+ }
+ // post
+ b = append([]byte(`<?xml version="1.0"?>`+"\n"), b...)
+ respBody, err := httpPost(endpoint, "text/xml", bytes.NewReader(b))
+ if err != nil {
+ return err
+ }
+ // unmarshal
+ err = xml.Unmarshal(respBody, resp)
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: Unmarshal Error: %v", err)
+ }
+ if resp.faultCode() != 0 {
+ return rpcError{
+ faultCode: resp.faultCode(), faultString: resp.faultString()}
+ }
+ return nil
+}
+
+// functions to perform API actions
+
+func (d *DNSProvider) getZoneID(domain string) (int, error) {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.info",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramString{Value: domain},
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ var zoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "zone_id" {
+ zoneID = member.ValueInt
+ }
+ }
+ if zoneID == 0 {
+ return 0, fmt.Errorf(
+ "Gandi DNS: Could not determine zone_id for %s", domain)
+ }
+ return zoneID, nil
+}
+
+func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.clone",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: 0},
+ paramStruct{
+ StructMembers: []structMember{
+ structMemberString{
+ Name: "name",
+ Value: name,
+ }},
+ },
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ var newZoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "id" {
+ newZoneID = member.ValueInt
+ }
+ }
+ if newZoneID == 0 {
+ return 0, fmt.Errorf("Gandi DNS: Could not determine cloned zone_id")
+ }
+ return newZoneID, nil
+}
+
+func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) {
+ resp := &responseInt{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.version.new",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ if resp.Value == 0 {
+ return 0, fmt.Errorf("Gandi DNS: Could not create new zone version")
+ }
+ return resp.Value, nil
+}
+
+func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value string, ttl int) error {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.record.add",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: version},
+ paramStruct{
+ StructMembers: []structMember{
+ structMemberString{
+ Name: "type",
+ Value: "TXT",
+ }, structMemberString{
+ Name: "name",
+ Value: name,
+ }, structMemberString{
+ Name: "value",
+ Value: value,
+ }, structMemberInt{
+ Name: "ttl",
+ Value: ttl,
+ }},
+ },
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
+ resp := &responseBool{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.version.set",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: version},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ if !resp.Value {
+ return fmt.Errorf("Gandi DNS: could not set zone version")
+ }
+ return nil
+}
+
+func (d *DNSProvider) setZone(domain string, zoneID int) error {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.set",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramString{Value: domain},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ var respZoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "zone_id" {
+ respZoneID = member.ValueInt
+ }
+ }
+ if respZoneID != zoneID {
+ return fmt.Errorf(
+ "Gandi DNS: Could not set new zone_id for %s", domain)
+ }
+ return nil
+}
+
+func (d *DNSProvider) deleteZone(zoneID int) error {
+ resp := &responseBool{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.delete",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ if !resp.Value {
+ return fmt.Errorf("Gandi DNS: could not delete zone_id")
+ }
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go
new file mode 100644
index 000000000..15919e2eb
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go
@@ -0,0 +1,939 @@
+package gandi
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// stagingServer is the Let's Encrypt staging server used by the live test
+const stagingServer = "https://acme-staging.api.letsencrypt.org/directory"
+
+// user implements acme.User and is used by the live test
+type user struct {
+ Email string
+ Registration *acme.RegistrationResource
+ key crypto.PrivateKey
+}
+
+func (u *user) GetEmail() string {
+ return u.Email
+}
+func (u *user) GetRegistration() *acme.RegistrationResource {
+ return u.Registration
+}
+func (u *user) GetPrivateKey() crypto.PrivateKey {
+ return u.key
+}
+
+// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
+// Server, whose responses are predetermined for particular requests.
+func TestDNSProvider(t *testing.T) {
+ fakeAPIKey := "123412341234123412341234"
+ fakeKeyAuth := "XXXX"
+ provider, err := NewDNSProviderCredentials(fakeAPIKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ regexpDate, err := regexp.Compile(`\[ACME Challenge [^\]:]*:[^\]]*\]`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // start fake RPC server
+ fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("Content-Type") != "text/xml" {
+ t.Fatalf("Content-Type: text/xml header not found")
+ }
+ req, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req = regexpDate.ReplaceAllLiteral(
+ req, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`))
+ resp, ok := serverResponses[string(req)]
+ if !ok {
+ t.Fatalf("Server response for request not found")
+ }
+ _, err = io.Copy(w, strings.NewReader(resp))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer fakeServer.Close()
+ // define function to override findZoneByFqdn with
+ fakeFindZoneByFqdn := func(fqdn string, nameserver []string) (string, error) {
+ return "example.com.", nil
+ }
+ // override gandi endpoint and findZoneByFqdn function
+ savedEndpoint, savedFindZoneByFqdn := endpoint, findZoneByFqdn
+ defer func() {
+ endpoint, findZoneByFqdn = savedEndpoint, savedFindZoneByFqdn
+ }()
+ endpoint, findZoneByFqdn = fakeServer.URL+"/", fakeFindZoneByFqdn
+ // run Present
+ err = provider.Present("abc.def.example.com", "", fakeKeyAuth)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // run CleanUp
+ err = provider.CleanUp("abc.def.example.com", "", fakeKeyAuth)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestDNSProviderLive performs a live test to obtain a certificate
+// using the Let's Encrypt staging server. It runs provided that both
+// the environment variables GANDI_API_KEY and GANDI_TEST_DOMAIN are
+// set. Otherwise the test is skipped.
+//
+// To complete this test, go test must be run with the -timeout=40m
+// flag, since the default timeout of 10m is insufficient.
+func TestDNSProviderLive(t *testing.T) {
+ apiKey := os.Getenv("GANDI_API_KEY")
+ domain := os.Getenv("GANDI_TEST_DOMAIN")
+ if apiKey == "" || domain == "" {
+ t.Skip("skipping live test")
+ }
+ // create a user.
+ const rsaKeySize = 2048
+ privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
+ if err != nil {
+ t.Fatal(err)
+ }
+ myUser := user{
+ Email: "test@example.com",
+ key: privateKey,
+ }
+ // create a client using staging server
+ client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048)
+ if err != nil {
+ t.Fatal(err)
+ }
+ provider, err := NewDNSProviderCredentials(apiKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = client.SetChallengeProvider(acme.DNS01, provider)
+ if err != nil {
+ t.Fatal(err)
+ }
+ client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
+ // register and agree tos
+ reg, err := client.Register()
+ if err != nil {
+ t.Fatal(err)
+ }
+ myUser.Registration = reg
+ err = client.AgreeToTOS()
+ if err != nil {
+ t.Fatal(err)
+ }
+ // complete the challenge
+ bundle := false
+ _, failures := client.ObtainCertificate([]string{domain}, bundle, nil)
+ if len(failures) > 0 {
+ t.Fatal(failures)
+ }
+}
+
+// serverResponses is the XML-RPC Request->Response map used by the
+// fake RPC server. It was generated by recording a real RPC session
+// which resulted in the successful issue of a cert, and then
+// anonymizing the RPC data.
+var serverResponses = map[string]string{
+ // Present Request->Response 1 (getZoneID)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.info</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <string>example.com.</string>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:14:23</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_delete</name>
+<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>is_premium</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>date_hold_begin</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_registry_end</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>authinfo_expiration_date</name>
+<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>
+</member>
+<member>
+<name>contacts</name>
+<value><struct>
+<member>
+<name>owner</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>admin</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>bill</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>tech</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>reseller</name>
+<value><nil/></value></member>
+</struct></value>
+</member>
+<member>
+<name>nameservers</name>
+<value><array><data>
+<value><string>a.dns.gandi.net</string></value>
+<value><string>b.dns.gandi.net</string></value>
+<value><string>c.dns.gandi.net</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_restore_end</name>
+<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>2222222</int></value>
+</member>
+<member>
+<name>authinfo</name>
+<value><string>ABCDABCDAB</string></value>
+</member>
+<member>
+<name>status</name>
+<value><array><data>
+<value><string>clientTransferProhibited</string></value>
+<value><string>serverTransferProhibited</string></value>
+</data></array></value>
+</member>
+<member>
+<name>tags</name>
+<value><array><data>
+</data></array></value>
+</member>
+<member>
+<name>date_hold_end</name>
+<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>services</name>
+<value><array><data>
+<value><string>gandidns</string></value>
+<value><string>gandimail</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_pending_delete_end</name>
+<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>zone_id</name>
+<value><int>1234567</int></value>
+</member>
+<member>
+<name>date_renew_begin</name>
+<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>
+</member>
+<member>
+<name>fqdn</name>
+<value><string>example.com</string></value>
+</member>
+<member>
+<name>autorenew</name>
+<value><nil/></value></member>
+<member>
+<name>date_registry_creation</name>
+<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>tld</name>
+<value><string>org</string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 2 (cloneZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.clone</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>1234567</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>0</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>name</name>
+ <value>
+ <string>example.com [ACME Challenge 01 Jan 16 00:00 +0000]</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>name</name>
+<value><string>example.com [ACME Challenge 01 Jan 16 00:00 +0000]</string></value>
+</member>
+<member>
+<name>versions</name>
+<value><array><data>
+<value><int>1</int></value>
+</data></array></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:24:29</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>7654321</int></value>
+</member>
+<member>
+<name>owner</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>version</name>
+<value><int>1</int></value>
+</member>
+<member>
+<name>domains</name>
+<value><int>0</int></value>
+</member>
+<member>
+<name>public</name>
+<value><boolean>0</boolean></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 3 (newZoneVersion)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.version.new</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><int>2</int></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 4 (addTXTRecord)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.record.add</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>2</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>type</name>
+ <value>
+ <string>TXT</string>
+ </value>
+ </member>
+ <member>
+ <name>name</name>
+ <value>
+ <string>_acme-challenge.abc.def</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ</string>
+ </value>
+ </member>
+ <member>
+ <name>ttl</name>
+ <value>
+ <int>300</int>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>name</name>
+<value><string>_acme-challenge.abc.def</string></value>
+</member>
+<member>
+<name>type</name>
+<value><string>TXT</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>3333333333</int></value>
+</member>
+<member>
+<name>value</name>
+<value><string>"ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ"</string></value>
+</member>
+<member>
+<name>ttl</name>
+<value><int>300</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 5 (setZoneVersion)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.version.set</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>2</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><boolean>1</boolean></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 6 (setZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.set</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <string>example.com.</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:14:23</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_delete</name>
+<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>is_premium</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>date_hold_begin</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_registry_end</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>authinfo_expiration_date</name>
+<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>
+</member>
+<member>
+<name>contacts</name>
+<value><struct>
+<member>
+<name>owner</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>admin</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>bill</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>tech</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>reseller</name>
+<value><nil/></value></member>
+</struct></value>
+</member>
+<member>
+<name>nameservers</name>
+<value><array><data>
+<value><string>a.dns.gandi.net</string></value>
+<value><string>b.dns.gandi.net</string></value>
+<value><string>c.dns.gandi.net</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_restore_end</name>
+<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>2222222</int></value>
+</member>
+<member>
+<name>authinfo</name>
+<value><string>ABCDABCDAB</string></value>
+</member>
+<member>
+<name>status</name>
+<value><array><data>
+<value><string>clientTransferProhibited</string></value>
+<value><string>serverTransferProhibited</string></value>
+</data></array></value>
+</member>
+<member>
+<name>tags</name>
+<value><array><data>
+</data></array></value>
+</member>
+<member>
+<name>date_hold_end</name>
+<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>services</name>
+<value><array><data>
+<value><string>gandidns</string></value>
+<value><string>gandimail</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_pending_delete_end</name>
+<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>zone_id</name>
+<value><int>7654321</int></value>
+</member>
+<member>
+<name>date_renew_begin</name>
+<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>
+</member>
+<member>
+<name>fqdn</name>
+<value><string>example.com</string></value>
+</member>
+<member>
+<name>autorenew</name>
+<value><nil/></value></member>
+<member>
+<name>date_registry_creation</name>
+<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>tld</name>
+<value><string>org</string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // CleanUp Request->Response 1 (setZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.set</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <string>example.com.</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>1234567</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:24:38</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_delete</name>
+<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>is_premium</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>date_hold_begin</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_registry_end</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>authinfo_expiration_date</name>
+<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>
+</member>
+<member>
+<name>contacts</name>
+<value><struct>
+<member>
+<name>owner</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>admin</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>bill</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>tech</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>reseller</name>
+<value><nil/></value></member>
+</struct></value>
+</member>
+<member>
+<name>nameservers</name>
+<value><array><data>
+<value><string>a.dns.gandi.net</string></value>
+<value><string>b.dns.gandi.net</string></value>
+<value><string>c.dns.gandi.net</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_restore_end</name>
+<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>2222222</int></value>
+</member>
+<member>
+<name>authinfo</name>
+<value><string>ABCDABCDAB</string></value>
+</member>
+<member>
+<name>status</name>
+<value><array><data>
+<value><string>clientTransferProhibited</string></value>
+<value><string>serverTransferProhibited</string></value>
+</data></array></value>
+</member>
+<member>
+<name>tags</name>
+<value><array><data>
+</data></array></value>
+</member>
+<member>
+<name>date_hold_end</name>
+<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>services</name>
+<value><array><data>
+<value><string>gandidns</string></value>
+<value><string>gandimail</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_pending_delete_end</name>
+<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>zone_id</name>
+<value><int>1234567</int></value>
+</member>
+<member>
+<name>date_renew_begin</name>
+<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>
+</member>
+<member>
+<name>fqdn</name>
+<value><string>example.com</string></value>
+</member>
+<member>
+<name>autorenew</name>
+<value><nil/></value></member>
+<member>
+<name>date_registry_creation</name>
+<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>tld</name>
+<value><string>org</string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // CleanUp Request->Response 2 (deleteZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.delete</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><boolean>1</boolean></value>
+</param>
+</params>
+</methodResponse>
+`,
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go
new file mode 100644
index 000000000..b8d9951c9
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go
@@ -0,0 +1,158 @@
+// Package googlecloud implements a DNS provider for solving the DNS-01
+// challenge using Google Cloud DNS.
+package googlecloud
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+
+ "golang.org/x/net/context"
+ "golang.org/x/oauth2/google"
+
+ "google.golang.org/api/dns/v1"
+)
+
+// DNSProvider is an implementation of the DNSProvider interface.
+type DNSProvider struct {
+ project string
+ client *dns.Service
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Google Cloud
+// DNS. Credentials must be passed in the environment variable: GCE_PROJECT.
+func NewDNSProvider() (*DNSProvider, error) {
+ project := os.Getenv("GCE_PROJECT")
+ return NewDNSProviderCredentials(project)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Google Cloud DNS.
+func NewDNSProviderCredentials(project string) (*DNSProvider, error) {
+ if project == "" {
+ return nil, fmt.Errorf("Google Cloud project name missing")
+ }
+
+ client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to get Google Cloud client: %v", err)
+ }
+ svc, err := dns.New(client)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to create Google Cloud DNS service: %v", err)
+ }
+ return &DNSProvider{
+ project: project,
+ client: svc,
+ }, nil
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge.
+func (c *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+
+ zone, err := c.getHostedZone(domain)
+ if err != nil {
+ return err
+ }
+
+ rec := &dns.ResourceRecordSet{
+ Name: fqdn,
+ Rrdatas: []string{value},
+ Ttl: int64(ttl),
+ Type: "TXT",
+ }
+ change := &dns.Change{
+ Additions: []*dns.ResourceRecordSet{rec},
+ }
+
+ chg, err := c.client.Changes.Create(c.project, zone, change).Do()
+ if err != nil {
+ return err
+ }
+
+ // wait for change to be acknowledged
+ for chg.Status == "pending" {
+ time.Sleep(time.Second)
+
+ chg, err = c.client.Changes.Get(c.project, zone, chg.Id).Do()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ zone, err := c.getHostedZone(domain)
+ if err != nil {
+ return err
+ }
+
+ records, err := c.findTxtRecords(zone, fqdn)
+ if err != nil {
+ return err
+ }
+
+ for _, rec := range records {
+ change := &dns.Change{
+ Deletions: []*dns.ResourceRecordSet{rec},
+ }
+ _, err = c.client.Changes.Create(c.project, zone, change).Do()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Timeout customizes the timeout values used by the ACME package for checking
+// DNS record validity.
+func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 180 * time.Second, 5 * time.Second
+}
+
+// getHostedZone returns the managed-zone
+func (c *DNSProvider) getHostedZone(domain string) (string, error) {
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return "", err
+ }
+
+ zones, err := c.client.ManagedZones.
+ List(c.project).
+ DnsName(authZone).
+ Do()
+ if err != nil {
+ return "", fmt.Errorf("GoogleCloud API call failed: %v", err)
+ }
+
+ if len(zones.ManagedZones) == 0 {
+ return "", fmt.Errorf("No matching GoogleCloud domain found for domain %s", authZone)
+ }
+
+ return zones.ManagedZones[0].Name, nil
+}
+
+func (c *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
+
+ recs, err := c.client.ResourceRecordSets.List(c.project, zone).Do()
+ if err != nil {
+ return nil, err
+ }
+
+ found := []*dns.ResourceRecordSet{}
+ for _, r := range recs.Rrsets {
+ if r.Type == "TXT" && r.Name == fqdn {
+ found = append(found, r)
+ }
+ }
+
+ return found, nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go
new file mode 100644
index 000000000..d73788163
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go
@@ -0,0 +1,85 @@
+package googlecloud
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+ "golang.org/x/oauth2/google"
+ "google.golang.org/api/dns/v1"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ gcloudLiveTest bool
+ gcloudProject string
+ gcloudDomain string
+)
+
+func init() {
+ gcloudProject = os.Getenv("GCE_PROJECT")
+ gcloudDomain = os.Getenv("GCE_DOMAIN")
+ _, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
+ if err == nil && len(gcloudProject) > 0 && len(gcloudDomain) > 0 {
+ gcloudLiveTest = true
+ }
+}
+
+func restoreGCloudEnv() {
+ os.Setenv("GCE_PROJECT", gcloudProject)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test (requires credentials)")
+ }
+ os.Setenv("GCE_PROJECT", "")
+ _, err := NewDNSProviderCredentials("my-project")
+ assert.NoError(t, err)
+ restoreGCloudEnv()
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test (requires credentials)")
+ }
+ os.Setenv("GCE_PROJECT", "my-project")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restoreGCloudEnv()
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("GCE_PROJECT", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "Google Cloud project name missing")
+ restoreGCloudEnv()
+}
+
+func TestLiveGoogleCloudPresent(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(gcloudProject)
+ assert.NoError(t, err)
+
+ err = provider.Present(gcloudDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveGoogleCloudCleanUp(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProviderCredentials(gcloudProject)
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(gcloudDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/linode/linode.go b/vendor/github.com/xenolf/lego/providers/dns/linode/linode.go
new file mode 100644
index 000000000..a91d2b489
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/linode/linode.go
@@ -0,0 +1,131 @@
+// Package linode implements a DNS provider for solving the DNS-01 challenge
+// using Linode DNS.
+package linode
+
+import (
+ "errors"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/timewasted/linode/dns"
+ "github.com/xenolf/lego/acme"
+)
+
+const (
+ dnsMinTTLSecs = 300
+ dnsUpdateFreqMins = 15
+ dnsUpdateFudgeSecs = 120
+)
+
+type hostedZoneInfo struct {
+ domainId int
+ resourceName string
+}
+
+// DNSProvider implements the acme.ChallengeProvider interface.
+type DNSProvider struct {
+ linode *dns.DNS
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Linode.
+// Credentials must be passed in the environment variable: LINODE_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiKey := os.Getenv("LINODE_API_KEY")
+ return NewDNSProviderCredentials(apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Linode.
+func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
+ if len(apiKey) == 0 {
+ return nil, errors.New("Linode credentials missing")
+ }
+
+ return &DNSProvider{
+ linode: dns.New(apiKey),
+ }, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. Adjusting here to cope with spikes in propagation times.
+func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ // Since Linode only updates their zone files every X minutes, we need
+ // to figure out how many minutes we have to wait until we hit the next
+ // interval of X. We then wait another couple of minutes, just to be
+ // safe. Hopefully at some point during all of this, the record will
+ // have propagated throughout Linode's network.
+ minsRemaining := dnsUpdateFreqMins - (time.Now().Minute() % dnsUpdateFreqMins)
+
+ timeout = (time.Duration(minsRemaining) * time.Minute) +
+ (dnsMinTTLSecs * time.Second) +
+ (dnsUpdateFudgeSecs * time.Second)
+ interval = 15 * time.Second
+ return
+}
+
+// Present creates a TXT record using the specified parameters.
+func (p *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zone, err := p.getHostedZoneInfo(fqdn)
+ if err != nil {
+ return err
+ }
+
+ if _, err = p.linode.CreateDomainResourceTXT(zone.domainId, acme.UnFqdn(fqdn), value, 60); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zone, err := p.getHostedZoneInfo(fqdn)
+ if err != nil {
+ return err
+ }
+
+ // Get all TXT records for the specified domain.
+ resources, err := p.linode.GetResourcesByType(zone.domainId, "TXT")
+ if err != nil {
+ return err
+ }
+
+ // Remove the specified resource, if it exists.
+ for _, resource := range resources {
+ if resource.Name == zone.resourceName && resource.Target == value {
+ resp, err := p.linode.DeleteDomainResource(resource.DomainID, resource.ResourceID)
+ if err != nil {
+ return err
+ }
+ if resp.ResourceID != resource.ResourceID {
+ return errors.New("Error deleting resource: resource IDs do not match!")
+ }
+ break
+ }
+ }
+
+ return nil
+}
+
+func (p *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
+ // Lookup the zone that handles the specified FQDN.
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return nil, err
+ }
+ resourceName := strings.TrimSuffix(fqdn, "."+authZone)
+
+ // Query the authority zone.
+ domain, err := p.linode.GetDomain(acme.UnFqdn(authZone))
+ if err != nil {
+ return nil, err
+ }
+
+ return &hostedZoneInfo{
+ domainId: domain.DomainID,
+ resourceName: resourceName,
+ }, nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go b/vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go
new file mode 100644
index 000000000..d9713a275
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go
@@ -0,0 +1,317 @@
+package linode
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/timewasted/linode"
+ "github.com/timewasted/linode/dns"
+)
+
+type (
+ LinodeResponse struct {
+ Action string `json:"ACTION"`
+ Data interface{} `json:"DATA"`
+ Errors []linode.ResponseError `json:"ERRORARRAY"`
+ }
+ MockResponse struct {
+ Response interface{}
+ Errors []linode.ResponseError
+ }
+ MockResponseMap map[string]MockResponse
+)
+
+var (
+ apiKey string
+ isTestLive bool
+)
+
+func init() {
+ apiKey = os.Getenv("LINODE_API_KEY")
+ isTestLive = len(apiKey) != 0
+}
+
+func restoreEnv() {
+ os.Setenv("LINODE_API_KEY", apiKey)
+}
+
+func newMockServer(t *testing.T, responses MockResponseMap) *httptest.Server {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Ensure that we support the requested action.
+ action := r.URL.Query().Get("api_action")
+ resp, ok := responses[action]
+ if !ok {
+ msg := fmt.Sprintf("Unsupported mock action: %s", action)
+ require.FailNow(t, msg)
+ }
+
+ // Build the response that the server will return.
+ linodeResponse := LinodeResponse{
+ Action: action,
+ Data: resp.Response,
+ Errors: resp.Errors,
+ }
+ rawResponse, err := json.Marshal(linodeResponse)
+ if err != nil {
+ msg := fmt.Sprintf("Failed to JSON encode response: %v", err)
+ require.FailNow(t, msg)
+ }
+
+ // Send the response.
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write(rawResponse)
+ }))
+
+ time.Sleep(100 * time.Millisecond)
+ return srv
+}
+
+func TestNewDNSProviderWithEnv(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderWithoutEnv(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "Linode credentials missing")
+}
+
+func TestNewDNSProviderCredentialsWithKey(t *testing.T) {
+ _, err := NewDNSProviderCredentials("testing")
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderCredentialsWithoutKey(t *testing.T) {
+ _, err := NewDNSProviderCredentials("")
+ assert.EqualError(t, err, "Linode credentials missing")
+}
+
+func TestDNSProvider_Present(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.create": MockResponse{
+ Response: dns.ResourceResponse{
+ ResourceID: 1234,
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.Present(domain, "", keyAuth)
+ assert.NoError(t, err)
+}
+
+func TestDNSProvider_PresentNoDomain(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: "foobar.com",
+ DomainID: 1234,
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.Present(domain, "", keyAuth)
+ assert.EqualError(t, err, "dns: requested domain not found")
+}
+
+func TestDNSProvider_PresentCreateFailed(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.create": MockResponse{
+ Response: nil,
+ Errors: []linode.ResponseError{
+ linode.ResponseError{
+ Code: 1234,
+ Message: "Failed to create domain resource",
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.Present(domain, "", keyAuth)
+ assert.EqualError(t, err, "Failed to create domain resource")
+}
+
+func TestDNSProvider_PresentLive(t *testing.T) {
+ if !isTestLive {
+ t.Skip("Skipping live test")
+ }
+}
+
+func TestDNSProvider_CleanUp(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.list": MockResponse{
+ Response: []dns.Resource{
+ dns.Resource{
+ DomainID: 1234,
+ Name: "_acme-challenge",
+ ResourceID: 1234,
+ Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
+ Type: "TXT",
+ },
+ },
+ },
+ "domain.resource.delete": MockResponse{
+ Response: dns.ResourceResponse{
+ ResourceID: 1234,
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.CleanUp(domain, "", keyAuth)
+ assert.NoError(t, err)
+}
+
+func TestDNSProvider_CleanUpNoDomain(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: "foobar.com",
+ DomainID: 1234,
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.CleanUp(domain, "", keyAuth)
+ assert.EqualError(t, err, "dns: requested domain not found")
+}
+
+func TestDNSProvider_CleanUpDeleteFailed(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.list": MockResponse{
+ Response: []dns.Resource{
+ dns.Resource{
+ DomainID: 1234,
+ Name: "_acme-challenge",
+ ResourceID: 1234,
+ Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
+ Type: "TXT",
+ },
+ },
+ },
+ "domain.resource.delete": MockResponse{
+ Response: nil,
+ Errors: []linode.ResponseError{
+ linode.ResponseError{
+ Code: 1234,
+ Message: "Failed to delete domain resource",
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.CleanUp(domain, "", keyAuth)
+ assert.EqualError(t, err, "Failed to delete domain resource")
+}
+
+func TestDNSProvider_CleanUpLive(t *testing.T) {
+ if !isTestLive {
+ t.Skip("Skipping live test")
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go
new file mode 100644
index 000000000..d7eb40935
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go
@@ -0,0 +1,416 @@
+// Package namecheap implements a DNS provider for solving the DNS-01
+// challenge using namecheap DNS.
+package namecheap
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// Notes about namecheap's tool API:
+// 1. Using the API requires registration. Once registered, use your account
+// name and API key to access the API.
+// 2. There is no API to add or modify a single DNS record. Instead you must
+// read the entire list of records, make modifications, and then write the
+// entire updated list of records. (Yuck.)
+// 3. Namecheap's DNS updates can be slow to propagate. I've seen them take
+// as long as an hour.
+// 4. Namecheap requires you to whitelist the IP address from which you call
+// its APIs. It also requires all API calls to include the whitelisted IP
+// address as a form or query string value. This code uses a namecheap
+// service to query the client's IP address.
+
+var (
+ debug = false
+ defaultBaseURL = "https://api.namecheap.com/xml.response"
+ getIPURL = "https://dynamicdns.park-your-domain.com/getip"
+ httpClient = http.Client{Timeout: 60 * time.Second}
+)
+
+// DNSProvider is an implementation of the ChallengeProviderTimeout interface
+// that uses Namecheap's tool API to manage TXT records for a domain.
+type DNSProvider struct {
+ baseURL string
+ apiUser string
+ apiKey string
+ clientIP string
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for namecheap.
+// Credentials must be passed in the environment variables: NAMECHEAP_API_USER
+// and NAMECHEAP_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiUser := os.Getenv("NAMECHEAP_API_USER")
+ apiKey := os.Getenv("NAMECHEAP_API_KEY")
+ return NewDNSProviderCredentials(apiUser, apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for namecheap.
+func NewDNSProviderCredentials(apiUser, apiKey string) (*DNSProvider, error) {
+ if apiUser == "" || apiKey == "" {
+ return nil, fmt.Errorf("Namecheap credentials missing")
+ }
+
+ clientIP, err := getClientIP()
+ if err != nil {
+ return nil, err
+ }
+
+ return &DNSProvider{
+ baseURL: defaultBaseURL,
+ apiUser: apiUser,
+ apiKey: apiKey,
+ clientIP: clientIP,
+ }, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. Namecheap can sometimes take a long time to complete an
+// update, so wait up to 60 minutes for the update to propagate.
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 60 * time.Minute, 15 * time.Second
+}
+
+// host describes a DNS record returned by the Namecheap DNS gethosts API.
+// Namecheap uses the term "host" to refer to all DNS records that include
+// a host field (A, AAAA, CNAME, NS, TXT, URL).
+type host struct {
+ Type string `xml:",attr"`
+ Name string `xml:",attr"`
+ Address string `xml:",attr"`
+ MXPref string `xml:",attr"`
+ TTL string `xml:",attr"`
+}
+
+// apierror describes an error record in a namecheap API response.
+type apierror struct {
+ Number int `xml:",attr"`
+ Description string `xml:",innerxml"`
+}
+
+// getClientIP returns the client's public IP address. It uses namecheap's
+// IP discovery service to perform the lookup.
+func getClientIP() (addr string, err error) {
+ resp, err := httpClient.Get(getIPURL)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ clientIP, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+
+ if debug {
+ fmt.Println("Client IP:", string(clientIP))
+ }
+ return string(clientIP), nil
+}
+
+// A challenge repesents all the data needed to specify a dns-01 challenge
+// to lets-encrypt.
+type challenge struct {
+ domain string
+ key string
+ keyFqdn string
+ keyValue string
+ tld string
+ sld string
+ host string
+}
+
+// newChallenge builds a challenge record from a domain name, a challenge
+// authentication key, and a map of available TLDs.
+func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, error) {
+ domain = acme.UnFqdn(domain)
+ parts := strings.Split(domain, ".")
+
+ // Find the longest matching TLD.
+ longest := -1
+ for i := len(parts); i > 0; i-- {
+ t := strings.Join(parts[i-1:], ".")
+ if _, found := tlds[t]; found {
+ longest = i - 1
+ }
+ }
+ if longest < 1 {
+ return nil, fmt.Errorf("Invalid domain name '%s'", domain)
+ }
+
+ tld := strings.Join(parts[longest:], ".")
+ sld := parts[longest-1]
+
+ var host string
+ if longest >= 1 {
+ host = strings.Join(parts[:longest-1], ".")
+ }
+
+ key, keyValue, _ := acme.DNS01Record(domain, keyAuth)
+
+ return &challenge{
+ domain: domain,
+ key: "_acme-challenge." + host,
+ keyFqdn: key,
+ keyValue: keyValue,
+ tld: tld,
+ sld: sld,
+ host: host,
+ }, nil
+}
+
+// setGlobalParams adds the namecheap global parameters to the provided url
+// Values record.
+func (d *DNSProvider) setGlobalParams(v *url.Values, cmd string) {
+ v.Set("ApiUser", d.apiUser)
+ v.Set("ApiKey", d.apiKey)
+ v.Set("UserName", d.apiUser)
+ v.Set("ClientIp", d.clientIP)
+ v.Set("Command", cmd)
+}
+
+// getTLDs requests the list of available TLDs from namecheap.
+func (d *DNSProvider) getTLDs() (tlds map[string]string, err error) {
+ values := make(url.Values)
+ d.setGlobalParams(&values, "namecheap.domains.getTldList")
+
+ reqURL, _ := url.Parse(d.baseURL)
+ reqURL.RawQuery = values.Encode()
+
+ resp, err := httpClient.Get(reqURL.String())
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ return nil, fmt.Errorf("getHosts HTTP error %d", resp.StatusCode)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ type GetTldsResponse struct {
+ XMLName xml.Name `xml:"ApiResponse"`
+ Errors []apierror `xml:"Errors>Error"`
+ Result []struct {
+ Name string `xml:",attr"`
+ } `xml:"CommandResponse>Tlds>Tld"`
+ }
+
+ var gtr GetTldsResponse
+ if err := xml.Unmarshal(body, &gtr); err != nil {
+ return nil, err
+ }
+ if len(gtr.Errors) > 0 {
+ return nil, fmt.Errorf("Namecheap error: %s [%d]",
+ gtr.Errors[0].Description, gtr.Errors[0].Number)
+ }
+
+ tlds = make(map[string]string)
+ for _, t := range gtr.Result {
+ tlds[t.Name] = t.Name
+ }
+ return tlds, nil
+}
+
+// getHosts reads the full list of DNS host records using the Namecheap API.
+func (d *DNSProvider) getHosts(ch *challenge) (hosts []host, err error) {
+ values := make(url.Values)
+ d.setGlobalParams(&values, "namecheap.domains.dns.getHosts")
+ values.Set("SLD", ch.sld)
+ values.Set("TLD", ch.tld)
+
+ reqURL, _ := url.Parse(d.baseURL)
+ reqURL.RawQuery = values.Encode()
+
+ resp, err := httpClient.Get(reqURL.String())
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ return nil, fmt.Errorf("getHosts HTTP error %d", resp.StatusCode)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ type GetHostsResponse struct {
+ XMLName xml.Name `xml:"ApiResponse"`
+ Status string `xml:"Status,attr"`
+ Errors []apierror `xml:"Errors>Error"`
+ Hosts []host `xml:"CommandResponse>DomainDNSGetHostsResult>host"`
+ }
+
+ var ghr GetHostsResponse
+ if err = xml.Unmarshal(body, &ghr); err != nil {
+ return nil, err
+ }
+ if len(ghr.Errors) > 0 {
+ return nil, fmt.Errorf("Namecheap error: %s [%d]",
+ ghr.Errors[0].Description, ghr.Errors[0].Number)
+ }
+
+ return ghr.Hosts, nil
+}
+
+// setHosts writes the full list of DNS host records using the Namecheap API.
+func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error {
+ values := make(url.Values)
+ d.setGlobalParams(&values, "namecheap.domains.dns.setHosts")
+ values.Set("SLD", ch.sld)
+ values.Set("TLD", ch.tld)
+
+ for i, h := range hosts {
+ ind := fmt.Sprintf("%d", i+1)
+ values.Add("HostName"+ind, h.Name)
+ values.Add("RecordType"+ind, h.Type)
+ values.Add("Address"+ind, h.Address)
+ values.Add("MXPref"+ind, h.MXPref)
+ values.Add("TTL"+ind, h.TTL)
+ }
+
+ resp, err := httpClient.PostForm(d.baseURL, values)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ return fmt.Errorf("setHosts HTTP error %d", resp.StatusCode)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ type SetHostsResponse struct {
+ XMLName xml.Name `xml:"ApiResponse"`
+ Status string `xml:"Status,attr"`
+ Errors []apierror `xml:"Errors>Error"`
+ Result struct {
+ IsSuccess string `xml:",attr"`
+ } `xml:"CommandResponse>DomainDNSSetHostsResult"`
+ }
+
+ var shr SetHostsResponse
+ if err := xml.Unmarshal(body, &shr); err != nil {
+ return err
+ }
+ if len(shr.Errors) > 0 {
+ return fmt.Errorf("Namecheap error: %s [%d]",
+ shr.Errors[0].Description, shr.Errors[0].Number)
+ }
+ if shr.Result.IsSuccess != "true" {
+ return fmt.Errorf("Namecheap setHosts failed.")
+ }
+
+ return nil
+}
+
+// addChallengeRecord adds a DNS challenge TXT record to a list of namecheap
+// host records.
+func (d *DNSProvider) addChallengeRecord(ch *challenge, hosts *[]host) {
+ host := host{
+ Name: ch.key,
+ Type: "TXT",
+ Address: ch.keyValue,
+ MXPref: "10",
+ TTL: "120",
+ }
+
+ // If there's already a TXT record with the same name, replace it.
+ for i, h := range *hosts {
+ if h.Name == ch.key && h.Type == "TXT" {
+ (*hosts)[i] = host
+ return
+ }
+ }
+
+ // No record was replaced, so add a new one.
+ *hosts = append(*hosts, host)
+}
+
+// removeChallengeRecord removes a DNS challenge TXT record from a list of
+// namecheap host records. Return true if a record was removed.
+func (d *DNSProvider) removeChallengeRecord(ch *challenge, hosts *[]host) bool {
+ // Find the challenge TXT record and remove it if found.
+ for i, h := range *hosts {
+ if h.Name == ch.key && h.Type == "TXT" {
+ *hosts = append((*hosts)[:i], (*hosts)[i+1:]...)
+ return true
+ }
+ }
+
+ return false
+}
+
+// Present installs a TXT record for the DNS challenge.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ tlds, err := d.getTLDs()
+ if err != nil {
+ return err
+ }
+
+ ch, err := newChallenge(domain, keyAuth, tlds)
+ if err != nil {
+ return err
+ }
+
+ hosts, err := d.getHosts(ch)
+ if err != nil {
+ return err
+ }
+
+ d.addChallengeRecord(ch, &hosts)
+
+ if debug {
+ for _, h := range hosts {
+ fmt.Printf(
+ "%-5.5s %-30.30s %-6s %-70.70s\n",
+ h.Type, h.Name, h.TTL, h.Address)
+ }
+ }
+
+ return d.setHosts(ch, hosts)
+}
+
+// CleanUp removes a TXT record used for a previous DNS challenge.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ tlds, err := d.getTLDs()
+ if err != nil {
+ return err
+ }
+
+ ch, err := newChallenge(domain, keyAuth, tlds)
+ if err != nil {
+ return err
+ }
+
+ hosts, err := d.getHosts(ch)
+ if err != nil {
+ return err
+ }
+
+ if removed := d.removeChallengeRecord(ch, &hosts); !removed {
+ return nil
+ }
+
+ return d.setHosts(ch, hosts)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go
new file mode 100644
index 000000000..0631d4a3e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go
@@ -0,0 +1,402 @@
+package namecheap
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+)
+
+var (
+ fakeUser = "foo"
+ fakeKey = "bar"
+ fakeClientIP = "10.0.0.1"
+
+ tlds = map[string]string{
+ "com.au": "com.au",
+ "com": "com",
+ "co.uk": "co.uk",
+ "uk": "uk",
+ "edu": "edu",
+ "co.com": "co.com",
+ "za.com": "za.com",
+ }
+)
+
+func assertEq(t *testing.T, variable, got, want string) {
+ if got != want {
+ t.Errorf("Expected %s to be '%s' but got '%s'", variable, want, got)
+ }
+}
+
+func assertHdr(tc *testcase, t *testing.T, values *url.Values) {
+ ch, _ := newChallenge(tc.domain, "", tlds)
+
+ assertEq(t, "ApiUser", values.Get("ApiUser"), fakeUser)
+ assertEq(t, "ApiKey", values.Get("ApiKey"), fakeKey)
+ assertEq(t, "UserName", values.Get("UserName"), fakeUser)
+ assertEq(t, "ClientIp", values.Get("ClientIp"), fakeClientIP)
+ assertEq(t, "SLD", values.Get("SLD"), ch.sld)
+ assertEq(t, "TLD", values.Get("TLD"), ch.tld)
+}
+
+func mockServer(tc *testcase, t *testing.T, w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+
+ case "GET":
+ values := r.URL.Query()
+ cmd := values.Get("Command")
+ switch cmd {
+ case "namecheap.domains.dns.getHosts":
+ assertHdr(tc, t, &values)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, tc.getHostsResponse)
+ case "namecheap.domains.getTldList":
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, responseGetTlds)
+ default:
+ t.Errorf("Unexpected GET command: %s", cmd)
+ }
+
+ case "POST":
+ r.ParseForm()
+ values := r.Form
+ cmd := values.Get("Command")
+ switch cmd {
+ case "namecheap.domains.dns.setHosts":
+ assertHdr(tc, t, &values)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, tc.setHostsResponse)
+ default:
+ t.Errorf("Unexpected POST command: %s", cmd)
+ }
+
+ default:
+ t.Errorf("Unexpected http method: %s", r.Method)
+
+ }
+}
+
+func testGetHosts(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := &DNSProvider{
+ baseURL: mock.URL,
+ apiUser: fakeUser,
+ apiKey: fakeKey,
+ clientIP: fakeClientIP,
+ }
+
+ ch, _ := newChallenge(tc.domain, "", tlds)
+ hosts, err := prov.getHosts(ch)
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap getHosts case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err)
+ }
+ }
+
+next1:
+ for _, h := range hosts {
+ for _, th := range tc.hosts {
+ if h == th {
+ continue next1
+ }
+ }
+ t.Errorf("getHosts case %s unexpected record [%s:%s:%s]",
+ tc.name, h.Type, h.Name, h.Address)
+ }
+
+next2:
+ for _, th := range tc.hosts {
+ for _, h := range hosts {
+ if h == th {
+ continue next2
+ }
+ }
+ t.Errorf("getHosts case %s missing record [%s:%s:%s]",
+ tc.name, th.Type, th.Name, th.Address)
+ }
+}
+
+func mockDNSProvider(url string) *DNSProvider {
+ return &DNSProvider{
+ baseURL: url,
+ apiUser: fakeUser,
+ apiKey: fakeKey,
+ clientIP: fakeClientIP,
+ }
+}
+
+func testSetHosts(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := mockDNSProvider(mock.URL)
+ ch, _ := newChallenge(tc.domain, "", tlds)
+ hosts, err := prov.getHosts(ch)
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap getHosts case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err)
+ }
+ }
+ if err != nil {
+ return
+ }
+
+ err = prov.setHosts(ch, hosts)
+ if err != nil {
+ t.Errorf("Namecheap setHosts case %s failed", tc.name)
+ }
+}
+
+func testPresent(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := mockDNSProvider(mock.URL)
+ err := prov.Present(tc.domain, "", "dummyKey")
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap Present case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap Present case %s failed\n%v", tc.name, err)
+ }
+ }
+}
+
+func testCleanUp(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := mockDNSProvider(mock.URL)
+ err := prov.CleanUp(tc.domain, "", "dummyKey")
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap CleanUp case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap CleanUp case %s failed\n%v", tc.name, err)
+ }
+ }
+}
+
+func TestNamecheap(t *testing.T) {
+ for _, tc := range testcases {
+ testGetHosts(&tc, t)
+ testSetHosts(&tc, t)
+ testPresent(&tc, t)
+ testCleanUp(&tc, t)
+ }
+}
+
+func TestNamecheapDomainSplit(t *testing.T) {
+ tests := []struct {
+ domain string
+ valid bool
+ tld string
+ sld string
+ host string
+ }{
+ {"a.b.c.test.co.uk", true, "co.uk", "test", "a.b.c"},
+ {"test.co.uk", true, "co.uk", "test", ""},
+ {"test.com", true, "com", "test", ""},
+ {"test.co.com", true, "co.com", "test", ""},
+ {"www.test.com.au", true, "com.au", "test", "www"},
+ {"www.za.com", true, "za.com", "www", ""},
+ {"", false, "", "", ""},
+ {"a", false, "", "", ""},
+ {"com", false, "", "", ""},
+ {"co.com", false, "", "", ""},
+ {"co.uk", false, "", "", ""},
+ {"test.au", false, "", "", ""},
+ {"za.com", false, "", "", ""},
+ {"www.za", false, "", "", ""},
+ {"www.test.au", false, "", "", ""},
+ {"www.test.unk", false, "", "", ""},
+ }
+
+ for _, test := range tests {
+ valid := true
+ ch, err := newChallenge(test.domain, "", tlds)
+ if err != nil {
+ valid = false
+ }
+
+ if test.valid && !valid {
+ t.Errorf("Expected '%s' to split", test.domain)
+ } else if !test.valid && valid {
+ t.Errorf("Expected '%s' to produce error", test.domain)
+ }
+
+ if test.valid && valid {
+ assertEq(t, "domain", ch.domain, test.domain)
+ assertEq(t, "tld", ch.tld, test.tld)
+ assertEq(t, "sld", ch.sld, test.sld)
+ assertEq(t, "host", ch.host, test.host)
+ }
+ }
+}
+
+type testcase struct {
+ name string
+ domain string
+ hosts []host
+ errString string
+ getHostsResponse string
+ setHostsResponse string
+}
+
+var testcases = []testcase{
+ {
+ "Test:Success:1",
+ "test.example.com",
+ []host{
+ {"A", "home", "10.0.0.1", "10", "1799"},
+ {"A", "www", "10.0.0.2", "10", "1200"},
+ {"AAAA", "a", "::0", "10", "1799"},
+ {"CNAME", "*", "example.com.", "10", "1799"},
+ {"MXE", "example.com", "10.0.0.5", "10", "1800"},
+ {"URL", "xyz", "https://google.com", "10", "1799"},
+ },
+ "",
+ responseGetHostsSuccess1,
+ responseSetHostsSuccess1,
+ },
+ {
+ "Test:Success:2",
+ "example.com",
+ []host{
+ {"A", "@", "10.0.0.2", "10", "1200"},
+ {"A", "www", "10.0.0.3", "10", "60"},
+ },
+ "",
+ responseGetHostsSuccess2,
+ responseSetHostsSuccess2,
+ },
+ {
+ "Test:Error:BadApiKey:1",
+ "test.example.com",
+ nil,
+ "Namecheap error: API Key is invalid or API access has not been enabled [1011102]",
+ responseGetHostsErrorBadAPIKey1,
+ "",
+ },
+}
+
+var responseGetHostsSuccess1 = `<?xml version="1.0" encoding="utf-8"?>
+<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
+ <Errors />
+ <Warnings />
+ <RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>
+ <CommandResponse Type="namecheap.domains.dns.getHosts">
+ <DomainDNSGetHostsResult Domain="example.com" EmailType="MXE" IsUsingOurDNS="true">
+ <host HostId="217076" Name="www" Type="A" Address="10.0.0.2" MXPref="10" TTL="1200" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
+ <host HostId="217069" Name="home" Type="A" Address="10.0.0.1" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
+ <host HostId="217071" Name="a" Type="AAAA" Address="::0" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
+ <host HostId="217075" Name="*" Type="CNAME" Address="example.com." MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
+ <host HostId="217073" Name="example.com" Type="MXE" Address="10.0.0.5" MXPref="10" TTL="1800" AssociatedAppTitle="MXE" FriendlyName="MXE1" IsActive="true" IsDDNSEnabled="false" />
+ <host HostId="217077" Name="xyz" Type="URL" Address="https://google.com" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
+ </DomainDNSGetHostsResult>
+ </CommandResponse>
+ <Server>PHX01SBAPI01</Server>
+ <GMTTimeDifference>--5:00</GMTTimeDifference>
+ <ExecutionTime>3.338</ExecutionTime>
+</ApiResponse>`
+
+var responseSetHostsSuccess1 = `<?xml version="1.0" encoding="utf-8"?>
+<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
+ <Errors />
+ <Warnings />
+ <RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>
+ <CommandResponse Type="namecheap.domains.dns.setHosts">
+ <DomainDNSSetHostsResult Domain="example.com" IsSuccess="true">
+ <Warnings />
+ </DomainDNSSetHostsResult>
+ </CommandResponse>
+ <Server>PHX01SBAPI01</Server>
+ <GMTTimeDifference>--5:00</GMTTimeDifference>
+ <ExecutionTime>2.347</ExecutionTime>
+</ApiResponse>`
+
+var responseGetHostsSuccess2 = `<?xml version="1.0" encoding="utf-8"?>
+<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
+ <Errors />
+ <Warnings />
+ <RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>
+ <CommandResponse Type="namecheap.domains.dns.getHosts">
+ <DomainDNSGetHostsResult Domain="example.com" EmailType="MXE" IsUsingOurDNS="true">
+ <host HostId="217076" Name="@" Type="A" Address="10.0.0.2" MXPref="10" TTL="1200" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
+ <host HostId="217069" Name="www" Type="A" Address="10.0.0.3" MXPref="10" TTL="60" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
+ </DomainDNSGetHostsResult>
+ </CommandResponse>
+ <Server>PHX01SBAPI01</Server>
+ <GMTTimeDifference>--5:00</GMTTimeDifference>
+ <ExecutionTime>3.338</ExecutionTime>
+</ApiResponse>`
+
+var responseSetHostsSuccess2 = `<?xml version="1.0" encoding="utf-8"?>
+<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
+ <Errors />
+ <Warnings />
+ <RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>
+ <CommandResponse Type="namecheap.domains.dns.setHosts">
+ <DomainDNSSetHostsResult Domain="example.com" IsSuccess="true">
+ <Warnings />
+ </DomainDNSSetHostsResult>
+ </CommandResponse>
+ <Server>PHX01SBAPI01</Server>
+ <GMTTimeDifference>--5:00</GMTTimeDifference>
+ <ExecutionTime>2.347</ExecutionTime>
+</ApiResponse>`
+
+var responseGetHostsErrorBadAPIKey1 = `<?xml version="1.0" encoding="utf-8"?>
+<ApiResponse Status="ERROR" xmlns="http://api.namecheap.com/xml.response">
+ <Errors>
+ <Error Number="1011102">API Key is invalid or API access has not been enabled</Error>
+ </Errors>
+ <Warnings />
+ <RequestedCommand />
+ <Server>PHX01SBAPI01</Server>
+ <GMTTimeDifference>--5:00</GMTTimeDifference>
+ <ExecutionTime>0</ExecutionTime>
+</ApiResponse>`
+
+var responseGetTlds = `<?xml version="1.0" encoding="utf-8"?>
+<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
+ <Errors />
+ <Warnings />
+ <RequestedCommand>namecheap.domains.getTldList</RequestedCommand>
+ <CommandResponse Type="namecheap.domains.getTldList">
+ <Tlds>
+ <Tld Name="com" NonRealTime="false" MinRegisterYears="1" MaxRegisterYears="10" MinRenewYears="1" MaxRenewYears="10" RenewalMinDays="0" RenewalMaxDays="4000" ReactivateMaxDays="27" MinTransferYears="1" MaxTransferYears="1" IsApiRegisterable="true" IsApiRenewable="true" IsApiTransferable="true" IsEppRequired="true" IsDisableModContact="false" IsDisableWGAllot="false" IsIncludeInExtendedSearchOnly="false" SequenceNumber="10" Type="GTLD" SubType="" IsSupportsIDN="true" Category="A" SupportsRegistrarLock="true" AddGracePeriodDays="5" WhoisVerification="false" ProviderApiDelete="true" TldState="" SearchGroup="" Registry="">Most recognized top level domain<Categories><TldCategory Name="popular" SequenceNumber="10" /></Categories></Tld>
+ </Tlds>
+ </CommandResponse>
+ <Server>PHX01SBAPI01</Server>
+ <GMTTimeDifference>--5:00</GMTTimeDifference>
+ <ExecutionTime>0.004</ExecutionTime>
+</ApiResponse>`
diff --git a/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go
new file mode 100644
index 000000000..290a8d7df
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go
@@ -0,0 +1,159 @@
+// Package OVH implements a DNS provider for solving the DNS-01
+// challenge using OVH DNS.
+package ovh
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/ovh/go-ovh/ovh"
+ "github.com/xenolf/lego/acme"
+)
+
+// OVH API reference: https://eu.api.ovh.com/
+// Create a Token: https://eu.api.ovh.com/createToken/
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+// that uses OVH's REST API to manage TXT records for a domain.
+type DNSProvider struct {
+ client *ovh.Client
+ recordIDs map[string]int
+ recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for OVH
+// Credentials must be passed in the environment variable:
+// OVH_ENDPOINT : it must be ovh-eu or ovh-ca
+// OVH_APPLICATION_KEY
+// OVH_APPLICATION_SECRET
+// OVH_CONSUMER_KEY
+func NewDNSProvider() (*DNSProvider, error) {
+ apiEndpoint := os.Getenv("OVH_ENDPOINT")
+ applicationKey := os.Getenv("OVH_APPLICATION_KEY")
+ applicationSecret := os.Getenv("OVH_APPLICATION_SECRET")
+ consumerKey := os.Getenv("OVH_CONSUMER_KEY")
+ return NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, consumerKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for OVH.
+func NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, consumerKey string) (*DNSProvider, error) {
+ if apiEndpoint == "" || applicationKey == "" || applicationSecret == "" || consumerKey == "" {
+ return nil, fmt.Errorf("OVH credentials missing")
+ }
+
+ ovhClient, _ := ovh.NewClient(
+ apiEndpoint,
+ applicationKey,
+ applicationSecret,
+ consumerKey,
+ )
+
+ return &DNSProvider{
+ client: ovhClient,
+ recordIDs: make(map[string]int),
+ }, nil
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+
+ // txtRecordRequest represents the request body to DO's API to make a TXT record
+ type txtRecordRequest struct {
+ FieldType string `json:"fieldType"`
+ SubDomain string `json:"subDomain"`
+ Target string `json:"target"`
+ TTL int `json:"ttl"`
+ }
+
+ // txtRecordResponse represents a response from DO's API after making a TXT record
+ type txtRecordResponse struct {
+ ID int `json:"id"`
+ FieldType string `json:"fieldType"`
+ SubDomain string `json:"subDomain"`
+ Target string `json:"target"`
+ TTL int `json:"ttl"`
+ Zone string `json:"zone"`
+ }
+
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+
+ // Parse domain name
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+ subDomain := d.extractRecordName(fqdn, authZone)
+
+ reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone)
+ reqData := txtRecordRequest{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: ttl}
+ var respData txtRecordResponse
+
+ // Create TXT record
+ err = d.client.Post(reqURL, reqData, &respData)
+ if err != nil {
+ fmt.Printf("Error when call OVH api to add record : %q \n", err)
+ return err
+ }
+
+ // Apply the change
+ reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone)
+ err = d.client.Post(reqURL, nil, nil)
+ if err != nil {
+ fmt.Printf("Error when call OVH api to refresh zone : %q \n", err)
+ return err
+ }
+
+ d.recordIDsMu.Lock()
+ d.recordIDs[fqdn] = respData.ID
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ // get the record's unique ID from when we created it
+ d.recordIDsMu.Lock()
+ recordID, ok := d.recordIDs[fqdn]
+ d.recordIDsMu.Unlock()
+ if !ok {
+ return fmt.Errorf("unknown record ID for '%s'", fqdn)
+ }
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+
+ reqURL := fmt.Sprintf("/domain/zone/%s/record/%d", authZone, recordID)
+
+ err = d.client.Delete(reqURL, nil)
+ if err != nil {
+ fmt.Printf("Error when call OVH api to delete challenge record : %q \n", err)
+ return err
+ }
+
+ // Delete record ID from map
+ d.recordIDsMu.Lock()
+ delete(d.recordIDs, fqdn)
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
+ name := acme.UnFqdn(fqdn)
+ if idx := strings.Index(name, "."+domain); idx != -1 {
+ return name[:idx]
+ }
+ return name
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go
new file mode 100644
index 000000000..47da60e57
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go
@@ -0,0 +1,103 @@
+package ovh
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ liveTest bool
+ apiEndpoint string
+ applicationKey string
+ applicationSecret string
+ consumerKey string
+ domain string
+)
+
+func init() {
+ apiEndpoint = os.Getenv("OVH_ENDPOINT")
+ applicationKey = os.Getenv("OVH_APPLICATION_KEY")
+ applicationSecret = os.Getenv("OVH_APPLICATION_SECRET")
+ consumerKey = os.Getenv("OVH_CONSUMER_KEY")
+ liveTest = len(apiEndpoint) > 0 && len(applicationKey) > 0 && len(applicationSecret) > 0 && len(consumerKey) > 0
+}
+
+func restoreEnv() {
+ os.Setenv("OVH_ENDPOINT", apiEndpoint)
+ os.Setenv("OVH_APPLICATION_KEY", applicationKey)
+ os.Setenv("OVH_APPLICATION_SECRET", applicationSecret)
+ os.Setenv("OVH_CONSUMER_KEY", consumerKey)
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("OVH_ENDPOINT", "")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err = NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err = NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "")
+ defer restoreEnv()
+ _, err = NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+}
+
+func TestLivePresent(t *testing.T) {
+ if !liveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.Present(domain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveCleanUp(t *testing.T) {
+ if !liveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(domain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/pdns/README.md b/vendor/github.com/xenolf/lego/providers/dns/pdns/README.md
new file mode 100644
index 000000000..23abb7669
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/pdns/README.md
@@ -0,0 +1,7 @@
+## PowerDNS provider
+
+Tested and confirmed to work with PowerDNS authoratative server 3.4.8 and 4.0.1. Refer to [PowerDNS documentation](https://doc.powerdns.com/md/httpapi/README/) instructions on how to enable the built-in API interface.
+
+PowerDNS Notes:
+- PowerDNS API does not currently support SSL, therefore you should take care to ensure that traffic between lego and the PowerDNS API is over a trusted network, VPN etc.
+- In order to have the SOA serial automatically increment each time the `_acme-challenge` record is added/modified via the API, set `SOA-API-EDIT` to `INCEPTION-INCREMENT` for the zone in the `domainmetadata` table
diff --git a/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go
new file mode 100644
index 000000000..a4fd22b0c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go
@@ -0,0 +1,343 @@
+// Package pdns implements a DNS provider for solving the DNS-01
+// challenge using PowerDNS nameserver.
+package pdns
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+type DNSProvider struct {
+ apiKey string
+ host *url.URL
+ apiVersion int
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for pdns.
+// Credentials must be passed in the environment variable:
+// PDNS_API_URL and PDNS_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ key := os.Getenv("PDNS_API_KEY")
+ hostUrl, err := url.Parse(os.Getenv("PDNS_API_URL"))
+ if err != nil {
+ return nil, err
+ }
+
+ return NewDNSProviderCredentials(hostUrl, key)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for pdns.
+func NewDNSProviderCredentials(host *url.URL, key string) (*DNSProvider, error) {
+ if key == "" {
+ return nil, fmt.Errorf("PDNS API key missing")
+ }
+
+ if host == nil || host.Host == "" {
+ return nil, fmt.Errorf("PDNS API URL missing")
+ }
+
+ provider := &DNSProvider{
+ host: host,
+ apiKey: key,
+ }
+ provider.getAPIVersion()
+
+ return provider, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. Adjusting here to cope with spikes in propagation times.
+func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 120 * time.Second, 2 * time.Second
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge
+func (c *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zone, err := c.getHostedZone(fqdn)
+ if err != nil {
+ return err
+ }
+
+ name := fqdn
+
+ // pre-v1 API wants non-fqdn
+ if c.apiVersion == 0 {
+ name = acme.UnFqdn(fqdn)
+ }
+
+ rec := pdnsRecord{
+ Content: "\"" + value + "\"",
+ Disabled: false,
+
+ // pre-v1 API
+ Type: "TXT",
+ Name: name,
+ TTL: 120,
+ }
+
+ rrsets := rrSets{
+ RRSets: []rrSet{
+ rrSet{
+ Name: name,
+ ChangeType: "REPLACE",
+ Type: "TXT",
+ Kind: "Master",
+ TTL: 120,
+ Records: []pdnsRecord{rec},
+ },
+ },
+ }
+
+ body, err := json.Marshal(rrsets)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body))
+ if err != nil {
+ fmt.Println("here")
+ return err
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ zone, err := c.getHostedZone(fqdn)
+ if err != nil {
+ return err
+ }
+
+ set, err := c.findTxtRecord(fqdn)
+ if err != nil {
+ return err
+ }
+
+ rrsets := rrSets{
+ RRSets: []rrSet{
+ rrSet{
+ Name: set.Name,
+ Type: set.Type,
+ ChangeType: "DELETE",
+ },
+ },
+ }
+ body, err := json.Marshal(rrsets)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
+ var zone hostedZone
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return nil, err
+ }
+
+ url := "/servers/localhost/zones"
+ result, err := c.makeRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ zones := []hostedZone{}
+ err = json.Unmarshal(result, &zones)
+ if err != nil {
+ return nil, err
+ }
+
+ url = ""
+ for _, zone := range zones {
+ if acme.UnFqdn(zone.Name) == acme.UnFqdn(authZone) {
+ url = zone.URL
+ }
+ }
+
+ result, err = c.makeRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal(result, &zone)
+ if err != nil {
+ return nil, err
+ }
+
+ // convert pre-v1 API result
+ if len(zone.Records) > 0 {
+ zone.RRSets = []rrSet{}
+ for _, record := range zone.Records {
+ set := rrSet{
+ Name: record.Name,
+ Type: record.Type,
+ Records: []pdnsRecord{record},
+ }
+ zone.RRSets = append(zone.RRSets, set)
+ }
+ }
+
+ return &zone, nil
+}
+
+func (c *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) {
+ zone, err := c.getHostedZone(fqdn)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = c.makeRequest("GET", zone.URL, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, set := range zone.RRSets {
+ if (set.Name == acme.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" {
+ return &set, nil
+ }
+ }
+
+ return nil, fmt.Errorf("No existing record found for %s", fqdn)
+}
+
+func (c *DNSProvider) getAPIVersion() {
+ type APIVersion struct {
+ URL string `json:"url"`
+ Version int `json:"version"`
+ }
+
+ result, err := c.makeRequest("GET", "/api", nil)
+ if err != nil {
+ return
+ }
+
+ var versions []APIVersion
+ err = json.Unmarshal(result, &versions)
+ if err != nil {
+ return
+ }
+
+ latestVersion := 0
+ for _, v := range versions {
+ if v.Version > latestVersion {
+ latestVersion = v.Version
+ }
+ }
+ c.apiVersion = latestVersion
+}
+
+func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
+ type APIError struct {
+ Error string `json:"error"`
+ }
+ var path = ""
+ if c.host.Path != "/" {
+ path = c.host.Path
+ }
+ if c.apiVersion > 0 {
+ if !strings.HasPrefix(uri, "api/v") {
+ uri = "/api/v" + strconv.Itoa(c.apiVersion) + uri
+ } else {
+ uri = "/" + uri
+ }
+ }
+ url := c.host.Scheme + "://" + c.host.Host + path + uri
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("X-API-Key", c.apiKey)
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("Error talking to PDNS API -> %v", err)
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 422 && (resp.StatusCode < 200 || resp.StatusCode >= 300) {
+ return nil, fmt.Errorf("Unexpected HTTP status code %d when fetching '%s'", resp.StatusCode, url)
+ }
+
+ var msg json.RawMessage
+ err = json.NewDecoder(resp.Body).Decode(&msg)
+ switch {
+ case err == io.EOF:
+ // empty body
+ return nil, nil
+ case err != nil:
+ // other error
+ return nil, err
+ }
+
+ // check for PowerDNS error message
+ if len(msg) > 0 && msg[0] == '{' {
+ var apiError APIError
+ err = json.Unmarshal(msg, &apiError)
+ if err != nil {
+ return nil, err
+ }
+ if apiError.Error != "" {
+ return nil, fmt.Errorf("Error talking to PDNS API -> %v", apiError.Error)
+ }
+ }
+ return msg, nil
+}
+
+type pdnsRecord struct {
+ Content string `json:"content"`
+ Disabled bool `json:"disabled"`
+
+ // pre-v1 API
+ Name string `json:"name"`
+ Type string `json:"type"`
+ TTL int `json:"ttl,omitempty"`
+}
+
+type hostedZone struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ URL string `json:"url"`
+ RRSets []rrSet `json:"rrsets"`
+
+ // pre-v1 API
+ Records []pdnsRecord `json:"records"`
+}
+
+type rrSet struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Kind string `json:"kind"`
+ ChangeType string `json:"changetype"`
+ Records []pdnsRecord `json:"records"`
+ TTL int `json:"ttl,omitempty"`
+}
+
+type rrSets struct {
+ RRSets []rrSet `json:"rrsets"`
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go
new file mode 100644
index 000000000..70e7670ed
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go
@@ -0,0 +1,80 @@
+package pdns
+
+import (
+ "net/url"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ pdnsLiveTest bool
+ pdnsURL *url.URL
+ pdnsURLStr string
+ pdnsAPIKey string
+ pdnsDomain string
+)
+
+func init() {
+ pdnsURLStr = os.Getenv("PDNS_API_URL")
+ pdnsURL, _ = url.Parse(pdnsURLStr)
+ pdnsAPIKey = os.Getenv("PDNS_API_KEY")
+ pdnsDomain = os.Getenv("PDNS_DOMAIN")
+ if len(pdnsURLStr) > 0 && len(pdnsAPIKey) > 0 && len(pdnsDomain) > 0 {
+ pdnsLiveTest = true
+ }
+}
+
+func restorePdnsEnv() {
+ os.Setenv("PDNS_API_URL", pdnsURLStr)
+ os.Setenv("PDNS_API_KEY", pdnsAPIKey)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ os.Setenv("PDNS_API_URL", "")
+ os.Setenv("PDNS_API_KEY", "")
+ tmpURL, _ := url.Parse("http://localhost:8081")
+ _, err := NewDNSProviderCredentials(tmpURL, "123")
+ assert.NoError(t, err)
+ restorePdnsEnv()
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("PDNS_API_URL", "http://localhost:8081")
+ os.Setenv("PDNS_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restorePdnsEnv()
+}
+
+func TestNewDNSProviderMissingHostErr(t *testing.T) {
+ os.Setenv("PDNS_API_URL", "")
+ os.Setenv("PDNS_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "PDNS API URL missing")
+ restorePdnsEnv()
+}
+
+func TestNewDNSProviderMissingKeyErr(t *testing.T) {
+ os.Setenv("PDNS_API_URL", pdnsURLStr)
+ os.Setenv("PDNS_API_KEY", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "PDNS API key missing")
+ restorePdnsEnv()
+}
+
+func TestPdnsPresentAndCleanup(t *testing.T) {
+ if !pdnsLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(pdnsURL, pdnsAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.Present(pdnsDomain, "", "123d==")
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(pdnsDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go
new file mode 100644
index 000000000..43a95f18c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go
@@ -0,0 +1,129 @@
+// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge
+// using the rfc2136 dynamic update.
+package rfc2136
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/miekg/dns"
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface that
+// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
+type DNSProvider struct {
+ nameserver string
+ tsigAlgorithm string
+ tsigKey string
+ tsigSecret string
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for rfc2136
+// dynamic update. Credentials must be passed in the environment variables:
+// RFC2136_NAMESERVER, RFC2136_TSIG_ALGORITHM, RFC2136_TSIG_KEY and
+// RFC2136_TSIG_SECRET. To disable TSIG authentication, leave the TSIG
+// variables unset. RFC2136_NAMESERVER must be a network address in the form
+// "host" or "host:port".
+func NewDNSProvider() (*DNSProvider, error) {
+ nameserver := os.Getenv("RFC2136_NAMESERVER")
+ tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM")
+ tsigKey := os.Getenv("RFC2136_TSIG_KEY")
+ tsigSecret := os.Getenv("RFC2136_TSIG_SECRET")
+ return NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for rfc2136 dynamic update. To disable TSIG
+// authentication, leave the TSIG parameters as empty strings.
+// nameserver must be a network address in the form "host" or "host:port".
+func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProvider, error) {
+ if nameserver == "" {
+ return nil, fmt.Errorf("RFC2136 nameserver missing")
+ }
+
+ // Append the default DNS port if none is specified.
+ if _, _, err := net.SplitHostPort(nameserver); err != nil {
+ if strings.Contains(err.Error(), "missing port") {
+ nameserver = net.JoinHostPort(nameserver, "53")
+ } else {
+ return nil, err
+ }
+ }
+ d := &DNSProvider{
+ nameserver: nameserver,
+ }
+ if tsigAlgorithm == "" {
+ tsigAlgorithm = dns.HmacMD5
+ }
+ d.tsigAlgorithm = tsigAlgorithm
+ if len(tsigKey) > 0 && len(tsigSecret) > 0 {
+ d.tsigKey = tsigKey
+ d.tsigSecret = tsigSecret
+ }
+
+ return d, nil
+}
+
+// Present creates a TXT record using the specified parameters
+func (r *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+ return r.changeRecord("INSERT", fqdn, value, ttl)
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+ return r.changeRecord("REMOVE", fqdn, value, ttl)
+}
+
+func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
+ // Find the zone for the given fqdn
+ zone, err := acme.FindZoneByFqdn(fqdn, []string{r.nameserver})
+ if err != nil {
+ return err
+ }
+
+ // Create RR
+ rr := new(dns.TXT)
+ rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)}
+ rr.Txt = []string{value}
+ rrs := []dns.RR{rr}
+
+ // Create dynamic update packet
+ m := new(dns.Msg)
+ m.SetUpdate(zone)
+ switch action {
+ case "INSERT":
+ // Always remove old challenge left over from who knows what.
+ m.RemoveRRset(rrs)
+ m.Insert(rrs)
+ case "REMOVE":
+ m.Remove(rrs)
+ default:
+ return fmt.Errorf("Unexpected action: %s", action)
+ }
+
+ // Setup client
+ c := new(dns.Client)
+ c.SingleInflight = true
+ // TSIG authentication / msg signing
+ if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 {
+ m.SetTsig(dns.Fqdn(r.tsigKey), r.tsigAlgorithm, 300, time.Now().Unix())
+ c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret}
+ }
+
+ // Send the query
+ reply, _, err := c.Exchange(m, r.nameserver)
+ if err != nil {
+ return fmt.Errorf("DNS update failed: %v", err)
+ }
+ if reply != nil && reply.Rcode != dns.RcodeSuccess {
+ return fmt.Errorf("DNS update failed. Server replied: %s", dns.RcodeToString[reply.Rcode])
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go
new file mode 100644
index 000000000..a2515e995
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go
@@ -0,0 +1,244 @@
+package rfc2136
+
+import (
+ "bytes"
+ "fmt"
+ "net"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/miekg/dns"
+ "github.com/xenolf/lego/acme"
+)
+
+var (
+ rfc2136TestDomain = "123456789.www.example.com"
+ rfc2136TestKeyAuth = "123d=="
+ rfc2136TestValue = "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"
+ rfc2136TestFqdn = "_acme-challenge.123456789.www.example.com."
+ rfc2136TestZone = "example.com."
+ rfc2136TestTTL = 120
+ rfc2136TestTsigKey = "example.com."
+ rfc2136TestTsigSecret = "IwBTJx9wrDp4Y1RyC3H0gA=="
+)
+
+var reqChan = make(chan *dns.Msg, 10)
+
+func TestRFC2136CanaryLocalTestServer(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc("example.com.", serverHandlerHello)
+ defer dns.HandleRemove("example.com.")
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ c := new(dns.Client)
+ m := new(dns.Msg)
+ m.SetQuestion("example.com.", dns.TypeTXT)
+ r, _, err := c.Exchange(m, addrstr)
+ if err != nil || len(r.Extra) == 0 {
+ t.Fatalf("Failed to communicate with test server: %v", err)
+ }
+ txt := r.Extra[0].(*dns.TXT).Txt[0]
+ if txt != "Hello world" {
+ t.Error("Expected test server to return 'Hello world' but got: ", txt)
+ }
+}
+
+func TestRFC2136ServerSuccess(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", "", "")
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+ if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
+ t.Errorf("Expected Present() to return no error but the error was -> %v", err)
+ }
+}
+
+func TestRFC2136ServerError(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", "", "")
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+ if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err == nil {
+ t.Errorf("Expected Present() to return an error but it did not.")
+ } else if !strings.Contains(err.Error(), "NOTZONE") {
+ t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string but it did not.")
+ }
+}
+
+func TestRFC2136TsigClient(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", true)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", rfc2136TestTsigKey, rfc2136TestTsigSecret)
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+ if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
+ t.Errorf("Expected Present() to return no error but the error was -> %v", err)
+ }
+}
+
+func TestRFC2136ValidUpdatePacket(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", rfc2136TestFqdn, rfc2136TestTTL, rfc2136TestValue))
+ rrs := []dns.RR{txtRR}
+ m := new(dns.Msg)
+ m.SetUpdate(rfc2136TestZone)
+ m.RemoveRRset(rrs)
+ m.Insert(rrs)
+ expectstr := m.String()
+ expect, err := m.Pack()
+ if err != nil {
+ t.Fatalf("Error packing expect msg: %v", err)
+ }
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", "", "")
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+
+ if err := provider.Present(rfc2136TestDomain, "", "1234d=="); err != nil {
+ t.Errorf("Expected Present() to return no error but the error was -> %v", err)
+ }
+
+ rcvMsg := <-reqChan
+ rcvMsg.Id = m.Id
+ actual, err := rcvMsg.Pack()
+ if err != nil {
+ t.Fatalf("Error packing actual msg: %v", err)
+ }
+
+ if !bytes.Equal(actual, expect) {
+ tmp := new(dns.Msg)
+ if err := tmp.Unpack(actual); err != nil {
+ t.Fatalf("Error unpacking actual msg: %v", err)
+ }
+ t.Errorf("Expected msg:\n%s", expectstr)
+ t.Errorf("Actual msg:\n%v", tmp)
+ }
+}
+
+func runLocalDNSTestServer(listenAddr string, tsig bool) (*dns.Server, string, error) {
+ pc, err := net.ListenPacket("udp", listenAddr)
+ if err != nil {
+ return nil, "", err
+ }
+ server := &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
+ if tsig {
+ server.TsigSecret = map[string]string{rfc2136TestTsigKey: rfc2136TestTsigSecret}
+ }
+
+ waitLock := sync.Mutex{}
+ waitLock.Lock()
+ server.NotifyStartedFunc = waitLock.Unlock
+
+ go func() {
+ server.ActivateAndServe()
+ pc.Close()
+ }()
+
+ waitLock.Lock()
+ return server, pc.LocalAddr().String(), nil
+}
+
+func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ m.Extra = make([]dns.RR, 1)
+ m.Extra[0] = &dns.TXT{
+ Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0},
+ Txt: []string{"Hello world"},
+ }
+ w.WriteMsg(m)
+}
+
+func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET {
+ // Return SOA to appease findZoneByFqdn()
+ soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone))
+ m.Answer = []dns.RR{soaRR}
+ }
+
+ if t := req.IsTsig(); t != nil {
+ if w.TsigStatus() == nil {
+ // Validated
+ m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix())
+ }
+ }
+
+ w.WriteMsg(m)
+}
+
+func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetRcode(req, dns.RcodeNotZone)
+ w.WriteMsg(m)
+}
+
+func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET {
+ // Return SOA to appease findZoneByFqdn()
+ soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone))
+ m.Answer = []dns.RR{soaRR}
+ }
+
+ if t := req.IsTsig(); t != nil {
+ if w.TsigStatus() == nil {
+ // Validated
+ m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix())
+ }
+ }
+
+ w.WriteMsg(m)
+ if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET {
+ // Only talk back when it is not the SOA RR.
+ reqChan <- req
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go
new file mode 100644
index 000000000..a5cc9c878
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go
@@ -0,0 +1,39 @@
+package route53
+
+var ChangeResourceRecordSetsResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
+<ChangeInfo>
+ <Id>/change/123456</Id>
+ <Status>PENDING</Status>
+ <SubmittedAt>2016-02-10T01:36:41.958Z</SubmittedAt>
+</ChangeInfo>
+</ChangeResourceRecordSetsResponse>`
+
+var ListHostedZonesByNameResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<ListHostedZonesByNameResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
+ <HostedZones>
+ <HostedZone>
+ <Id>/hostedzone/ABCDEFG</Id>
+ <Name>example.com.</Name>
+ <CallerReference>D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A</CallerReference>
+ <Config>
+ <Comment>Test comment</Comment>
+ <PrivateZone>false</PrivateZone>
+ </Config>
+ <ResourceRecordSetCount>10</ResourceRecordSetCount>
+ </HostedZone>
+ </HostedZones>
+ <IsTruncated>true</IsTruncated>
+ <NextDNSName>example2.com</NextDNSName>
+ <NextHostedZoneId>ZLT12321321124</NextHostedZoneId>
+ <MaxItems>1</MaxItems>
+</ListHostedZonesByNameResponse>`
+
+var GetChangeResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<GetChangeResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
+ <ChangeInfo>
+ <Id>123456</Id>
+ <Status>INSYNC</Status>
+ <SubmittedAt>2016-02-10T01:36:41.958Z</SubmittedAt>
+ </ChangeInfo>
+</GetChangeResponse>`
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go b/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go
new file mode 100644
index 000000000..f3e53a8e5
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go
@@ -0,0 +1,171 @@
+// Package route53 implements a DNS provider for solving the DNS-01 challenge
+// using AWS Route 53 DNS.
+package route53
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/client"
+ "github.com/aws/aws-sdk-go/aws/request"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/route53"
+ "github.com/xenolf/lego/acme"
+)
+
+const (
+ maxRetries = 5
+ route53TTL = 10
+)
+
+// DNSProvider implements the acme.ChallengeProvider interface
+type DNSProvider struct {
+ client *route53.Route53
+}
+
+// customRetryer implements the client.Retryer interface by composing the
+// DefaultRetryer. It controls the logic for retrying recoverable request
+// errors (e.g. when rate limits are exceeded).
+type customRetryer struct {
+ client.DefaultRetryer
+}
+
+// RetryRules overwrites the DefaultRetryer's method.
+// It uses a basic exponential backoff algorithm that returns an initial
+// delay of ~400ms with an upper limit of ~30 seconds which should prevent
+// causing a high number of consecutive throttling errors.
+// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.
+func (d customRetryer) RetryRules(r *request.Request) time.Duration {
+ retryCount := r.RetryCount
+ if retryCount > 7 {
+ retryCount = 7
+ }
+
+ delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
+ return time.Duration(delay) * time.Millisecond
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for the AWS
+// Route 53 service.
+//
+// AWS Credentials are automatically detected in the following locations
+// and prioritized in the following order:
+// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
+// AWS_REGION, [AWS_SESSION_TOKEN]
+// 2. Shared credentials file (defaults to ~/.aws/credentials)
+// 3. Amazon EC2 IAM role
+//
+// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
+func NewDNSProvider() (*DNSProvider, error) {
+ r := customRetryer{}
+ r.NumMaxRetries = maxRetries
+ config := request.WithRetryer(aws.NewConfig(), r)
+ client := route53.New(session.New(config))
+
+ return &DNSProvider{client: client}, nil
+}
+
+// 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)
+}
+
+// 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)
+}
+
+func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
+ hostedZoneID, err := getHostedZoneID(fqdn, r.client)
+ if err != nil {
+ return fmt.Errorf("Failed to determine Route 53 hosted zone ID: %v", err)
+ }
+
+ recordSet := newTXTRecordSet(fqdn, value, ttl)
+ reqParams := &route53.ChangeResourceRecordSetsInput{
+ HostedZoneId: aws.String(hostedZoneID),
+ ChangeBatch: &route53.ChangeBatch{
+ Comment: aws.String("Managed by Lego"),
+ Changes: []*route53.Change{
+ {
+ Action: aws.String(action),
+ ResourceRecordSet: recordSet,
+ },
+ },
+ },
+ }
+
+ resp, err := r.client.ChangeResourceRecordSets(reqParams)
+ if err != nil {
+ return fmt.Errorf("Failed to change Route 53 record set: %v", err)
+ }
+
+ statusID := resp.ChangeInfo.Id
+
+ return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) {
+ reqParams := &route53.GetChangeInput{
+ Id: statusID,
+ }
+ resp, err := r.client.GetChange(reqParams)
+ if err != nil {
+ return false, fmt.Errorf("Failed to query Route 53 change status: %v", err)
+ }
+ if *resp.ChangeInfo.Status == route53.ChangeStatusInsync {
+ return true, nil
+ }
+ return false, nil
+ })
+}
+
+func getHostedZoneID(fqdn string, client *route53.Route53) (string, error) {
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return "", err
+ }
+
+ // .DNSName should not have a trailing dot
+ reqParams := &route53.ListHostedZonesByNameInput{
+ DNSName: aws.String(acme.UnFqdn(authZone)),
+ }
+ resp, err := client.ListHostedZonesByName(reqParams)
+ if err != nil {
+ return "", err
+ }
+
+ var hostedZoneID string
+ for _, hostedZone := range resp.HostedZones {
+ // .Name has a trailing dot
+ if !*hostedZone.Config.PrivateZone && *hostedZone.Name == authZone {
+ hostedZoneID = *hostedZone.Id
+ break
+ }
+ }
+
+ if len(hostedZoneID) == 0 {
+ return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn)
+ }
+
+ if strings.HasPrefix(hostedZoneID, "/hostedzone/") {
+ hostedZoneID = strings.TrimPrefix(hostedZoneID, "/hostedzone/")
+ }
+
+ return hostedZoneID, nil
+}
+
+func newTXTRecordSet(fqdn, value string, ttl int) *route53.ResourceRecordSet {
+ return &route53.ResourceRecordSet{
+ Name: aws.String(fqdn),
+ Type: aws.String("TXT"),
+ TTL: aws.Int64(int64(ttl)),
+ ResourceRecords: []*route53.ResourceRecord{
+ {Value: aws.String(value)},
+ },
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go
new file mode 100644
index 000000000..64678906a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go
@@ -0,0 +1,70 @@
+package route53
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/route53"
+)
+
+func TestRoute53TTL(t *testing.T) {
+
+ m, err := testGetAndPreCheck()
+ if err != nil {
+ t.Skip(err.Error())
+ }
+
+ provider, err := NewDNSProvider()
+ if err != nil {
+ t.Fatalf("Fatal: %s", err.Error())
+ }
+
+ err = provider.Present(m["route53Domain"], "foo", "bar")
+ if err != nil {
+ t.Fatalf("Fatal: %s", err.Error())
+ }
+ // we need a separate R53 client here as the one in the DNS provider is
+ // unexported.
+ fqdn := "_acme-challenge." + m["route53Domain"] + "."
+ svc := route53.New(session.New())
+ zoneID, err := getHostedZoneID(fqdn, svc)
+ if err != nil {
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ t.Fatalf("Fatal: %s", err.Error())
+ }
+ params := &route53.ListResourceRecordSetsInput{
+ HostedZoneId: aws.String(zoneID),
+ }
+ resp, err := svc.ListResourceRecordSets(params)
+ if err != nil {
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ t.Fatalf("Fatal: %s", err.Error())
+ }
+
+ for _, v := range resp.ResourceRecordSets {
+ if *v.Name == fqdn && *v.Type == "TXT" && *v.TTL == 10 {
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ return
+ }
+ }
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ t.Fatalf("Could not find a TXT record for _acme-challenge.%s with a TTL of 10", m["route53Domain"])
+}
+
+func testGetAndPreCheck() (map[string]string, error) {
+ m := map[string]string{
+ "route53Key": os.Getenv("AWS_ACCESS_KEY_ID"),
+ "route53Secret": os.Getenv("AWS_SECRET_ACCESS_KEY"),
+ "route53Region": os.Getenv("AWS_REGION"),
+ "route53Domain": os.Getenv("R53_DOMAIN"),
+ }
+ for _, v := range m {
+ if v == "" {
+ return nil, fmt.Errorf("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, and R53_DOMAIN are needed to run this test")
+ }
+ }
+ return m, nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go
new file mode 100644
index 000000000..ab8739a58
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go
@@ -0,0 +1,87 @@
+package route53
+
+import (
+ "net/http/httptest"
+ "os"
+ "testing"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/route53"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ route53Secret string
+ route53Key string
+ route53Region string
+)
+
+func init() {
+ route53Key = os.Getenv("AWS_ACCESS_KEY_ID")
+ route53Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
+ route53Region = os.Getenv("AWS_REGION")
+}
+
+func restoreRoute53Env() {
+ os.Setenv("AWS_ACCESS_KEY_ID", route53Key)
+ os.Setenv("AWS_SECRET_ACCESS_KEY", route53Secret)
+ os.Setenv("AWS_REGION", route53Region)
+}
+
+func makeRoute53Provider(ts *httptest.Server) *DNSProvider {
+ config := &aws.Config{
+ Credentials: credentials.NewStaticCredentials("abc", "123", " "),
+ Endpoint: aws.String(ts.URL),
+ Region: aws.String("mock-region"),
+ MaxRetries: aws.Int(1),
+ }
+
+ client := route53.New(session.New(config))
+ return &DNSProvider{client: client}
+}
+
+func TestCredentialsFromEnv(t *testing.T) {
+ os.Setenv("AWS_ACCESS_KEY_ID", "123")
+ os.Setenv("AWS_SECRET_ACCESS_KEY", "123")
+ os.Setenv("AWS_REGION", "us-east-1")
+
+ config := &aws.Config{
+ CredentialsChainVerboseErrors: aws.Bool(true),
+ }
+
+ sess := session.New(config)
+ _, err := sess.Config.Credentials.Get()
+ assert.NoError(t, err, "Expected credentials to be set from environment")
+
+ restoreRoute53Env()
+}
+
+func TestRegionFromEnv(t *testing.T) {
+ os.Setenv("AWS_REGION", "us-east-1")
+
+ sess := session.New(aws.NewConfig())
+ assert.Equal(t, "us-east-1", *sess.Config.Region, "Expected Region to be set from environment")
+
+ restoreRoute53Env()
+}
+
+func TestRoute53Present(t *testing.T) {
+ mockResponses := MockResponseMap{
+ "/2013-04-01/hostedzonesbyname": MockResponse{StatusCode: 200, Body: ListHostedZonesByNameResponse},
+ "/2013-04-01/hostedzone/ABCDEFG/rrset/": MockResponse{StatusCode: 200, Body: ChangeResourceRecordSetsResponse},
+ "/2013-04-01/change/123456": MockResponse{StatusCode: 200, Body: GetChangeResponse},
+ }
+
+ ts := newMockServer(t, mockResponses)
+ defer ts.Close()
+
+ provider := makeRoute53Provider(ts)
+
+ domain := "example.com"
+ keyAuth := "123456d=="
+
+ err := provider.Present(domain, "", keyAuth)
+ assert.NoError(t, err, "Expected Present to return no error")
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go
new file mode 100644
index 000000000..e448a6858
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go
@@ -0,0 +1,38 @@
+package route53
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+// MockResponse represents a predefined response used by a mock server
+type MockResponse struct {
+ StatusCode int
+ Body string
+}
+
+// MockResponseMap maps request paths to responses
+type MockResponseMap map[string]MockResponse
+
+func newMockServer(t *testing.T, responses MockResponseMap) *httptest.Server {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+ resp, ok := responses[path]
+ if !ok {
+ msg := fmt.Sprintf("Requested path not found in response map: %s", path)
+ require.FailNow(t, msg)
+ }
+
+ w.Header().Set("Content-Type", "application/xml")
+ w.WriteHeader(resp.StatusCode)
+ w.Write([]byte(resp.Body))
+ }))
+
+ time.Sleep(100 * time.Millisecond)
+ return ts
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go
new file mode 100644
index 000000000..53804e270
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go
@@ -0,0 +1,127 @@
+// Package vultr implements a DNS provider for solving the DNS-01 challenge using
+// the vultr DNS.
+// See https://www.vultr.com/api/#dns
+package vultr
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ vultr "github.com/JamesClonk/vultr/lib"
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface.
+type DNSProvider struct {
+ client *vultr.Client
+}
+
+// NewDNSProvider returns a DNSProvider instance with a configured Vultr client.
+// Authentication uses the VULTR_API_KEY environment variable.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiKey := os.Getenv("VULTR_API_KEY")
+ return NewDNSProviderCredentials(apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a DNSProvider
+// instance configured for Vultr.
+func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
+ if apiKey == "" {
+ return nil, fmt.Errorf("Vultr credentials missing")
+ }
+
+ c := &DNSProvider{
+ client: vultr.NewClient(apiKey, nil),
+ }
+
+ return c, nil
+}
+
+// Present creates a TXT record to fulfil the DNS-01 challenge.
+func (c *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+
+ zoneDomain, err := c.getHostedZone(domain)
+ if err != nil {
+ return err
+ }
+
+ name := c.extractRecordName(fqdn, zoneDomain)
+
+ err = c.client.CreateDnsRecord(zoneDomain, name, "TXT", `"`+value+`"`, 0, ttl)
+ if err != nil {
+ return fmt.Errorf("Vultr API call failed: %v", err)
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+
+ zoneDomain, records, err := c.findTxtRecords(domain, fqdn)
+ if err != nil {
+ return err
+ }
+
+ for _, rec := range records {
+ err := c.client.DeleteDnsRecord(zoneDomain, rec.RecordID)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *DNSProvider) getHostedZone(domain string) (string, error) {
+ domains, err := c.client.GetDnsDomains()
+ if err != nil {
+ return "", fmt.Errorf("Vultr API call failed: %v", err)
+ }
+
+ var hostedDomain vultr.DnsDomain
+ for _, d := range domains {
+ if strings.HasSuffix(domain, d.Domain) {
+ if len(d.Domain) > len(hostedDomain.Domain) {
+ hostedDomain = d
+ }
+ }
+ }
+ if hostedDomain.Domain == "" {
+ return "", fmt.Errorf("No matching Vultr domain found for domain %s", domain)
+ }
+
+ return hostedDomain.Domain, nil
+}
+
+func (c *DNSProvider) findTxtRecords(domain, fqdn string) (string, []vultr.DnsRecord, error) {
+ zoneDomain, err := c.getHostedZone(domain)
+ if err != nil {
+ return "", nil, err
+ }
+
+ var records []vultr.DnsRecord
+ result, err := c.client.GetDnsRecords(zoneDomain)
+ if err != nil {
+ return "", records, fmt.Errorf("Vultr API call has failed: %v", err)
+ }
+
+ recordName := c.extractRecordName(fqdn, zoneDomain)
+ for _, record := range result {
+ if record.Type == "TXT" && record.Name == recordName {
+ records = append(records, record)
+ }
+ }
+
+ return zoneDomain, records, nil
+}
+
+func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
+ name := acme.UnFqdn(fqdn)
+ if idx := strings.Index(name, "."+domain); idx != -1 {
+ return name[:idx]
+ }
+ return name
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go
new file mode 100644
index 000000000..7c8cdaf1e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go
@@ -0,0 +1,65 @@
+package vultr
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ liveTest bool
+ apiKey string
+ domain string
+)
+
+func init() {
+ apiKey = os.Getenv("VULTR_API_KEY")
+ domain = os.Getenv("VULTR_TEST_DOMAIN")
+ liveTest = len(apiKey) > 0 && len(domain) > 0
+}
+
+func restoreEnv() {
+ os.Setenv("VULTR_API_KEY", apiKey)
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("VULTR_API_KEY", "123")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("VULTR_API_KEY", "")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "Vultr credentials missing")
+}
+
+func TestLivePresent(t *testing.T) {
+ if !liveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.Present(domain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveCleanUp(t *testing.T) {
+ if !liveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(domain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/http/webroot/webroot.go b/vendor/github.com/xenolf/lego/providers/http/webroot/webroot.go
new file mode 100644
index 000000000..4bf211f39
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/http/webroot/webroot.go
@@ -0,0 +1,58 @@
+// Package webroot implements a HTTP provider for solving the HTTP-01 challenge using web server's root path.
+package webroot
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// HTTPProvider implements ChallengeProvider for `http-01` challenge
+type HTTPProvider struct {
+ path string
+}
+
+// NewHTTPProvider returns a HTTPProvider instance with a configured webroot path
+func NewHTTPProvider(path string) (*HTTPProvider, error) {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return nil, fmt.Errorf("Webroot path does not exist")
+ }
+
+ c := &HTTPProvider{
+ path: path,
+ }
+
+ return c, nil
+}
+
+// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path
+func (w *HTTPProvider) Present(domain, token, keyAuth string) error {
+ var err error
+
+ challengeFilePath := path.Join(w.path, acme.HTTP01ChallengePath(token))
+ err = os.MkdirAll(path.Dir(challengeFilePath), 0755)
+ if err != nil {
+ return fmt.Errorf("Could not create required directories in webroot for HTTP challenge -> %v", err)
+ }
+
+ err = ioutil.WriteFile(challengeFilePath, []byte(keyAuth), 0644)
+ if err != nil {
+ return fmt.Errorf("Could not write file in webroot for HTTP challenge -> %v", err)
+ }
+
+ return nil
+}
+
+// CleanUp removes the file created for the challenge
+func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
+ var err error
+ err = os.Remove(path.Join(w.path, acme.HTTP01ChallengePath(token)))
+ if err != nil {
+ return fmt.Errorf("Could not remove file in webroot after HTTP challenge -> %v", err)
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/http/webroot/webroot_test.go b/vendor/github.com/xenolf/lego/providers/http/webroot/webroot_test.go
new file mode 100644
index 000000000..99c930ed3
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/http/webroot/webroot_test.go
@@ -0,0 +1,46 @@
+package webroot
+
+import (
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+func TestHTTPProvider(t *testing.T) {
+ webroot := "webroot"
+ domain := "domain"
+ token := "token"
+ keyAuth := "keyAuth"
+ challengeFilePath := webroot + "/.well-known/acme-challenge/" + token
+
+ os.MkdirAll(webroot+"/.well-known/acme-challenge", 0777)
+ defer os.RemoveAll(webroot)
+
+ provider, err := NewHTTPProvider(webroot)
+ if err != nil {
+ t.Errorf("Webroot provider error: got %v, want nil", err)
+ }
+
+ err = provider.Present(domain, token, keyAuth)
+ if err != nil {
+ t.Errorf("Webroot provider present() error: got %v, want nil", err)
+ }
+
+ if _, err := os.Stat(challengeFilePath); os.IsNotExist(err) {
+ t.Error("Challenge file was not created in webroot")
+ }
+
+ data, err := ioutil.ReadFile(challengeFilePath)
+ if err != nil {
+ t.Errorf("Webroot provider ReadFile() error: got %v, want nil", err)
+ }
+ dataStr := string(data)
+ if dataStr != keyAuth {
+ t.Errorf("Challenge file content: got %q, want %q", dataStr, keyAuth)
+ }
+
+ err = provider.CleanUp(domain, token, keyAuth)
+ if err != nil {
+ t.Errorf("Webroot provider CleanUp() error: got %v, want nil", err)
+ }
+}