Vendor main dependencies.

This commit is contained in:
Timo Reimann 2017-02-07 22:33:23 +01:00
parent 49a09ab7dd
commit dd5e3fba01
2738 changed files with 1045689 additions and 0 deletions

21
vendor/github.com/xenolf/lego/LICENSE generated vendored Normal file
View file

@ -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.

109
vendor/github.com/xenolf/lego/account.go generated vendored Normal file
View file

@ -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,
)
}

16
vendor/github.com/xenolf/lego/acme/challenges.go generated vendored Normal file
View file

@ -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")
)

762
vendor/github.com/xenolf/lego/acme/client.go generated vendored Normal file
View file

@ -0,0 +1,762 @@
// 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
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 p that can solve the given challenge type.
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.
//
// NOTE: This REPLACES any custom HTTP provider previously set by calling
// c.SetChallengeProvider with the default HTTP challenge provider.
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.
//
// NOTE: This REPLACES any custom TLS-SNI provider previously set by calling
// c.SetChallengeProvider with the default TLS-SNI challenge provider.
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, mustStaple bool) (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, mustStaple)
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, mustStaple 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()))
// We always need to request a new certificate to renew.
// 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, mustStaple)
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, mustStaple bool) (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, mustStaple)
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))
// 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 {
issuerCert = pemEncode(derCertificateBytes(issuerCert))
// If bundle is true, we want to return a certificate bundle.
// To do this, we append the issuer cert to the issued cert.
if bundle {
issuedCert = append(issuedCert, issuerCert...)
}
}
cerRes.Certificate = issuedCert
cerRes.IssuerCertificate = issuerCert
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
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
logf("[INFO] acme: Requesting issuer cert from %s", url)
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
}
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
}
}
}

347
vendor/github.com/xenolf/lego/acme/crypto.go generated vendored Normal file
View file

@ -0,0 +1,347 @@
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"
"encoding/asn1"
"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
)
// Constants for OCSP must staple
var (
tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
)
// 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, mustStaple bool) ([]byte, error) {
template := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: domain,
},
}
if len(san) > 0 {
template.DNSNames = san
}
if mustStaple {
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
Id: tlsFeatureExtensionOID,
Value: ocspMustStapleFeature,
})
}
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)
}

305
vendor/github.com/xenolf/lego/acme/dns_challenge.go generated vendored Normal file
View file

@ -0,0 +1,305 @@
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{}
)
const defaultResolvConf = "/etc/resolv.conf"
var defaultNameservers = []string{
"google-public-dns-a.google.com:53",
"google-public-dns-b.google.com:53",
}
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
var DNSTimeout = 10 * time.Second
// getNameservers attempts to get systems nameservers before falling back to the defaults
func getNameservers(path string, defaults []string) []string {
config, err := dns.ClientConfigFromFile(path)
if err != nil || len(config.Servers) == 0 {
return defaults
}
systemNameservers := []string{}
for _, server := range config.Servers {
// ensure all servers have a port number
if _, _, err := net.SplitHostPort(server); err != nil {
systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53"))
} else {
systemNameservers = append(systemNameservers, server)
}
}
return systemNameservers
}
// 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 using %+v", domain, RecursiveNameservers)
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
}

View file

@ -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
}

86
vendor/github.com/xenolf/lego/acme/error.go generated vendored Normal file
View file

@ -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}
}

117
vendor/github.com/xenolf/lego/acme/http.go generated vendored Normal file
View file

@ -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)
}

41
vendor/github.com/xenolf/lego/acme/http_challenge.go generated vendored Normal file
View file

@ -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})
}

View file

@ -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("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", 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
}

115
vendor/github.com/xenolf/lego/acme/jws.go generated vendored Normal file
View file

@ -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.Lock()
defer j.Unlock()
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 {
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) {
j.Lock()
defer j.Unlock()
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")
}
nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1]
return nonce, nil
}

110
vendor/github.com/xenolf/lego/acme/messages.go generated vendored Normal file
View file

@ -0,0 +1,110 @@
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 registrationMessage struct {
Resource string `json:"resource"`
Contact []string `json:"contact"`
Delete bool `json:"delete,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"`
}
// 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, Certificate and IssuerCertificate are all
// 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:"-"`
IssuerCertificate []byte `json:"-"`
CSR []byte `json:"-"`
}

1
vendor/github.com/xenolf/lego/acme/pop_challenge.go generated vendored Normal file
View file

@ -0,0 +1 @@
package acme

28
vendor/github.com/xenolf/lego/acme/provider.go generated vendored Normal file
View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

29
vendor/github.com/xenolf/lego/acme/utils.go generated vendored Normal file
View file

@ -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)
}
}

231
vendor/github.com/xenolf/lego/cli.go generated vendored Normal file
View file

@ -0,0 +1,231 @@
// 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.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
},
},
{
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.",
},
cli.BoolFlag{
Name: "must-staple",
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
},
},
},
{
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.StringSliceFlag{
Name: "memcached-host",
Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.",
},
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, "\tazure:\tAZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP")
fmt.Fprintln(w, "\tauroradns:\tAURORA_USER_ID, AURORA_KEY, AURORA_ENDPOINT")
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, "\texoscale:\tEXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT")
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, "\trackspace:\tRACKSPACE_USER, RACKSPACE_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")
fmt.Fprintln(w, "\tdnspod:\tDNSPOD_API_KEY")
w.Flush()
fmt.Println(`
For a more detailed explanation of a DNS provider's credential variables,
please consult their online documentation.`)
return nil
}

418
vendor/github.com/xenolf/lego/cli_handlers.go generated vendored Normal file
View file

@ -0,0 +1,418 @@
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"
"github.com/xenolf/lego/providers/http/memcached"
"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("memcached-host") {
provider, err := memcached.NewMemcachedProvider(c.GlobalStringSlice("memcached-host"))
if err != nil {
logger().Fatal(err)
}
client.SetChallengeProvider(acme.HTTP01, provider)
// --memcached-host=foo:11211 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") {
provider, err := dns.NewDNSChallengeProviderByName(c.GlobalString("dns"))
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")
issuerOut := path.Join(conf.CertPath(), certRes.Domain+".issuer.crt")
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.IssuerCertificate != nil {
err = ioutil.WriteFile(issuerOut, certRes.IssuerCertificate, 0600)
if err != nil {
logger().Fatalf("Unable to save IssuerCertificate 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, c.Bool("must-staple"))
} 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"), c.Bool("must-staple"))
if err != nil {
logger().Fatalf("%s", err.Error())
}
saveCertRes(newCert, conf)
return nil
}

76
vendor/github.com/xenolf/lego/configuration.go generated vendored Normal file
View file

@ -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")
}

56
vendor/github.com/xenolf/lego/crypto.go generated vendored Normal file
View file

@ -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.")
}

View file

@ -0,0 +1,141 @@
package auroradns
import (
"fmt"
"github.com/edeckers/auroradnsclient"
"github.com/edeckers/auroradnsclient/records"
"github.com/edeckers/auroradnsclient/zones"
"github.com/xenolf/lego/acme"
"os"
"sync"
)
// DNSProvider describes a provider for AuroraDNS
type DNSProvider struct {
recordIDs map[string]string
recordIDsMu sync.Mutex
client *auroradnsclient.AuroraDNSClient
}
// NewDNSProvider returns a DNSProvider instance configured for AuroraDNS.
// Credentials must be passed in the environment variables: AURORA_USER_ID
// and AURORA_KEY.
func NewDNSProvider() (*DNSProvider, error) {
userID := os.Getenv("AURORA_USER_ID")
key := os.Getenv("AURORA_KEY")
endpoint := os.Getenv("AURORA_ENDPOINT")
if endpoint == "" {
endpoint = "https://api.auroradns.eu"
}
return NewDNSProviderCredentials(endpoint, userID, key)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for AuroraDNS.
func NewDNSProviderCredentials(baseURL string, userID string, key string) (*DNSProvider, error) {
client, err := auroradnsclient.NewAuroraDNSClient(baseURL, userID, key)
if err != nil {
return nil, err
}
return &DNSProvider{
client: client,
recordIDs: make(map[string]string),
}, nil
}
func (provider *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRecord, error) {
zs, err := provider.client.GetZones()
if err != nil {
return zones.ZoneRecord{}, err
}
for _, element := range zs {
if element.Name == name {
return element, nil
}
}
return zones.ZoneRecord{}, fmt.Errorf("Could not find Zone record")
}
// Present creates a record with a secret
func (provider *DNSProvider) Present(domain, token, keyAuth string) error {
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)
}
// 1. Aurora will happily create the TXT record when it is provided a fqdn,
// but it will only appear in the control panel and will not be
// propagated to DNS servers. Extract and use subdomain instead.
// 2. A trailing dot in the fqdn will cause Aurora to add a trailing dot to
// the subdomain, resulting in _acme-challenge..<domain> rather
// than _acme-challenge.<domain>
subdomain := fqdn[0 : len(fqdn)-len(authZone)-1]
authZone = acme.UnFqdn(authZone)
zoneRecord, err := provider.getZoneInformationByName(authZone)
reqData :=
records.CreateRecordRequest{
RecordType: "TXT",
Name: subdomain,
Content: value,
TTL: 300,
}
respData, err := provider.client.CreateRecord(zoneRecord.ID, reqData)
if err != nil {
return fmt.Errorf("Could not create record: '%s'.", err)
}
provider.recordIDsMu.Lock()
provider.recordIDs[fqdn] = respData.ID
provider.recordIDsMu.Unlock()
return nil
}
// CleanUp removes a given record that was generated by Present
func (provider *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
provider.recordIDsMu.Lock()
recordID, ok := provider.recordIDs[fqdn]
provider.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("Unknown recordID 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)
zoneRecord, err := provider.getZoneInformationByName(authZone)
if err != nil {
return err
}
_, err = provider.client.RemoveRecord(zoneRecord.ID, recordID)
if err != nil {
return err
}
provider.recordIDsMu.Lock()
delete(provider.recordIDs, fqdn)
provider.recordIDsMu.Unlock()
return nil
}

View file

@ -0,0 +1,142 @@
// Package azure implements a DNS provider for solving the DNS-01
// challenge using azure DNS.
// Azure doesn't like trailing dots on domain names, most of the acme code does.
package azure
import (
"fmt"
"os"
"time"
"github.com/Azure/azure-sdk-for-go/arm/dns"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/xenolf/lego/acme"
"strings"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
clientId string
clientSecret string
subscriptionId string
tenantId string
resourceGroup string
}
// NewDNSProvider returns a DNSProvider instance configured for azure.
// Credentials must be passed in the environment variables: AZURE_CLIENT_ID,
// AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID
func NewDNSProvider() (*DNSProvider, error) {
clientId := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
subscriptionId := os.Getenv("AZURE_SUBSCRIPTION_ID")
tenantId := os.Getenv("AZURE_TENANT_ID")
resourceGroup := os.Getenv("AZURE_RESOURCE_GROUP")
return NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for azure.
func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) {
if clientId == "" || clientSecret == "" || subscriptionId == "" || tenantId == "" || resourceGroup == "" {
return nil, fmt.Errorf("Azure configuration missing")
}
return &DNSProvider{
clientId: clientId,
clientSecret: clientSecret,
subscriptionId: subscriptionId,
tenantId: tenantId,
resourceGroup: resourceGroup,
}, 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.getHostedZoneID(fqdn)
if err != nil {
return err
}
rsc := dns.NewRecordSetsClient(c.subscriptionId)
rsc.Authorizer, err = c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
rec := dns.RecordSet{
Name: &relative,
RecordSetProperties: &dns.RecordSetProperties{
TTL: to.Int64Ptr(60),
TXTRecords: &[]dns.TxtRecord{dns.TxtRecord{Value: &[]string{value}}},
},
}
_, err = rsc.CreateOrUpdate(c.resourceGroup, zone, relative, dns.TXT, rec, "", "")
if err != nil {
return err
}
return nil
}
// Returns the relative record to the domain
func toRelativeRecord(domain, zone string) string {
return acme.UnFqdn(strings.TrimSuffix(domain, zone))
}
// 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.getHostedZoneID(fqdn)
if err != nil {
return err
}
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
rsc := dns.NewRecordSetsClient(c.subscriptionId)
rsc.Authorizer, err = c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
_, err = rsc.Delete(c.resourceGroup, zone, relative, dns.TXT, "")
if err != nil {
return err
}
return nil
}
// Checks that azure has a zone for this domain name.
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
// Now we want to to Azure and get the zone.
dc := dns.NewZonesClient(c.subscriptionId)
dc.Authorizer, err = c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
zone, err := dc.Get(c.resourceGroup, acme.UnFqdn(authZone))
if err != nil {
return "", err
}
// zone.Name shouldn't have a trailing dot(.)
return to.String(zone.Name), nil
}
// NewServicePrincipalTokenFromCredentials creates a new ServicePrincipalToken using values of the
// passed credentials map.
func (c *DNSProvider) newServicePrincipalTokenFromCredentials(scope string) (*azure.ServicePrincipalToken, error) {
oauthConfig, err := azure.PublicCloud.OAuthConfigForTenant(c.tenantId)
if err != nil {
panic(err)
}
return azure.NewServicePrincipalToken(*oauthConfig, c.clientId, c.clientSecret, scope)
}

View file

@ -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"`
}

View file

@ -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"

View file

@ -0,0 +1,80 @@
// Factory for DNS providers
package dns
import (
"fmt"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/providers/dns/auroradns"
"github.com/xenolf/lego/providers/dns/azure"
"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/dnspod"
"github.com/xenolf/lego/providers/dns/dyn"
"github.com/xenolf/lego/providers/dns/exoscale"
"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/ns1"
"github.com/xenolf/lego/providers/dns/ovh"
"github.com/xenolf/lego/providers/dns/pdns"
"github.com/xenolf/lego/providers/dns/rackspace"
"github.com/xenolf/lego/providers/dns/rfc2136"
"github.com/xenolf/lego/providers/dns/route53"
"github.com/xenolf/lego/providers/dns/vultr"
)
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
var err error
var provider acme.ChallengeProvider
switch name {
case "azure":
provider, err = azure.NewDNSProvider()
case "auroradns":
provider, err = auroradns.NewDNSProvider()
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 "dnspod":
provider, err = dnspod.NewDNSProvider()
case "dyn":
provider, err = dyn.NewDNSProvider()
case "exoscale":
provider, err = exoscale.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 "rackspace":
provider, err = rackspace.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()
case "ns1":
provider, err = ns1.NewDNSProvider()
default:
err = fmt.Errorf("Unrecognised DNS provider: %s", name)
}
return provider, err
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -0,0 +1,146 @@
// Package dnspod implements a DNS provider for solving the DNS-01 challenge
// using dnspod DNS.
package dnspod
import (
"fmt"
"os"
"strings"
"github.com/decker502/dnspod-go"
"github.com/xenolf/lego/acme"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *dnspod.Client
}
// NewDNSProvider returns a DNSProvider instance configured for dnspod.
// Credentials must be passed in the environment variables: DNSPOD_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
key := os.Getenv("DNSPOD_API_KEY")
return NewDNSProviderCredentials(key)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for dnspod.
func NewDNSProviderCredentials(key string) (*DNSProvider, error) {
if key == "" {
return nil, fmt.Errorf("dnspod credentials missing")
}
params := dnspod.CommonParams{LoginToken: key, Format: "json"}
return &DNSProvider{
client: dnspod.NewClient(params),
}, 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("dnspod 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
}
zoneID, _, err := c.getHostedZone(domain)
if err != nil {
return err
}
for _, rec := range records {
_, err := c.client.Domains.DeleteRecord(zoneID, 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("dnspod API call failed: %v", err)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return "", "", err
}
var hostedZone dnspod.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 dnspod for domain %s", authZone, domain)
}
return fmt.Sprintf("%v", hostedZone.ID), hostedZone.Name, nil
}
func (c *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnspod.Record {
name := c.extractRecordName(fqdn, zone)
return &dnspod.Record{
Type: "TXT",
Name: name,
Value: value,
Line: "默认",
TTL: "600",
}
}
func (c *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnspod.Record, error) {
zoneID, zoneName, err := c.getHostedZone(domain)
if err != nil {
return nil, err
}
var records []dnspod.Record
result, _, err := c.client.Domains.ListRecords(zoneID, "")
if err != nil {
return records, fmt.Errorf("dnspod 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) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}

274
vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go generated vendored Normal file
View file

@ -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
}

View file

@ -0,0 +1,132 @@
// Package exoscale implements a DNS provider for solving the DNS-01 challenge
// using exoscale DNS.
package exoscale
import (
"errors"
"fmt"
"os"
"github.com/pyr/egoscale/src/egoscale"
"github.com/xenolf/lego/acme"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *egoscale.Client
}
// Credentials must be passed in the environment variables:
// EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT.
func NewDNSProvider() (*DNSProvider, error) {
key := os.Getenv("EXOSCALE_API_KEY")
secret := os.Getenv("EXOSCALE_API_SECRET")
endpoint := os.Getenv("EXOSCALE_ENDPOINT")
return NewDNSProviderClient(key, secret, endpoint)
}
// Uses the supplied parameters to return a DNSProvider instance
// configured for Exoscale.
func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) {
if key == "" || secret == "" {
return nil, fmt.Errorf("Exoscale credentials missing")
}
if endpoint == "" {
endpoint = "https://api.exoscale.ch/dns"
}
return &DNSProvider{
client: egoscale.NewClient(endpoint, key, secret),
}, 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, recordName, err := c.FindZoneAndRecordName(fqdn, domain)
if err != nil {
return err
}
recordId, err := c.FindExistingRecordId(zone, recordName)
if err != nil {
return err
}
record := egoscale.DNSRecord{
Name: recordName,
Ttl: ttl,
Content: value,
RecordType: "TXT",
}
if recordId == 0 {
_, err := c.client.CreateRecord(zone, record)
if err != nil {
return errors.New("Error while creating DNS record: " + err.Error())
}
} else {
record.Id = recordId
_, err := c.client.UpdateRecord(zone, record)
if err != nil {
return errors.New("Error while updating DNS record: " + err.Error())
}
}
return nil
}
// CleanUp removes the record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
zone, recordName, err := c.FindZoneAndRecordName(fqdn, domain)
if err != nil {
return err
}
recordId, err := c.FindExistingRecordId(zone, recordName)
if err != nil {
return err
}
if recordId != 0 {
record := egoscale.DNSRecord{
Id: recordId,
}
err = c.client.DeleteRecord(zone, record)
if err != nil {
return errors.New("Error while deleting DNS record: " + err.Error())
}
}
return nil
}
// Query Exoscale to find an existing record for this name.
// Returns nil if no record could be found
func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, error) {
responses, err := c.client.GetRecords(zone)
if err != nil {
return -1, errors.New("Error while retrievening DNS records: " + err.Error())
}
for _, response := range responses {
if response.Record.Name == recordName {
return response.Record.Id, nil
}
}
return 0, nil
}
// Extract DNS zone and DNS entry name
func (c *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) {
zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return "", "", err
}
zone = acme.UnFqdn(zone)
name := acme.UnFqdn(fqdn)
name = name[:len(name)-len("."+zone)]
return zone, name, nil
}

View file

@ -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
}

View file

@ -0,0 +1,168 @@
// 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},
}
// Look for existing records.
list, err := c.client.ResourceRecordSets.List(c.project, zone).Name(fqdn).Type("TXT").Do()
if err != nil {
return err
}
if len(list.Rrsets) > 0 {
// Attempt to delete the existing records when adding our new one.
change.Deletions = list.Rrsets
}
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
}

View file

@ -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
}

View file

@ -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)
}

97
vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go generated vendored Normal file
View file

@ -0,0 +1,97 @@
// Package ns1 implements a DNS provider for solving the DNS-01 challenge
// using NS1 DNS.
package ns1
import (
"fmt"
"net/http"
"os"
"time"
"github.com/xenolf/lego/acme"
"gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *rest.Client
}
// NewDNSProvider returns a DNSProvider instance configured for NS1.
// Credentials must be passed in the environment variables: NS1_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
key := os.Getenv("NS1_API_KEY")
if key == "" {
return nil, fmt.Errorf("NS1 credentials missing")
}
return NewDNSProviderCredentials(key)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for NS1.
func NewDNSProviderCredentials(key string) (*DNSProvider, error) {
if key == "" {
return nil, fmt.Errorf("NS1 credentials missing")
}
httpClient := &http.Client{Timeout: time.Second * 10}
client := rest.NewClient(httpClient, rest.SetAPIKey(key))
return &DNSProvider{client}, 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
}
record := c.newTxtRecord(zone, fqdn, value, ttl)
_, err = c.client.Records.Create(record)
if err != nil && err != rest.ErrRecordExists {
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
}
name := acme.UnFqdn(fqdn)
_, err = c.client.Records.Delete(zone.Zone, name, "TXT")
return err
}
func (c *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) {
zone, _, err := c.client.Zones.Get(domain)
if err != nil {
return nil, err
}
return zone, nil
}
func (c *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record {
name := acme.UnFqdn(fqdn)
return &dns.Record{
Type: "TXT",
Zone: zone.Zone,
Domain: name,
TTL: ttl,
Answers: []*dns.Answer{
{Rdata: []string{value}},
},
}
}

159
vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go generated vendored Normal file
View file

@ -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
}

View file

@ -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"`
}

View file

@ -0,0 +1,284 @@
// Package rackspace implements a DNS provider for solving the DNS-01
// challenge using rackspace DNS.
package rackspace
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/xenolf/lego/acme"
)
// rackspaceAPIURL represents the Identity API endpoint to call
var rackspaceAPIURL = "https://identity.api.rackspacecloud.com/v2.0/tokens"
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// used to store the reusable token and DNS API endpoint
type DNSProvider struct {
token string
cloudDNSEndpoint string
}
// NewDNSProvider returns a DNSProvider instance configured for Rackspace.
// Credentials must be passed in the environment variables: RACKSPACE_USER
// and RACKSPACE_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
user := os.Getenv("RACKSPACE_USER")
key := os.Getenv("RACKSPACE_API_KEY")
return NewDNSProviderCredentials(user, key)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Rackspace. It authenticates against
// the API, also grabbing the DNS Endpoint.
func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) {
if user == "" || key == "" {
return nil, fmt.Errorf("Rackspace credentials missing")
}
type APIKeyCredentials struct {
Username string `json:"username"`
APIKey string `json:"apiKey"`
}
type Auth struct {
APIKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials"`
}
type RackspaceAuthData struct {
Auth `json:"auth"`
}
type RackspaceIdentity struct {
Access struct {
ServiceCatalog []struct {
Endpoints []struct {
PublicURL string `json:"publicURL"`
TenantID string `json:"tenantId"`
} `json:"endpoints"`
Name string `json:"name"`
} `json:"serviceCatalog"`
Token struct {
ID string `json:"id"`
} `json:"token"`
} `json:"access"`
}
authData := RackspaceAuthData{
Auth: Auth{
APIKeyCredentials: APIKeyCredentials{
Username: user,
APIKey: key,
},
},
}
body, err := json.Marshal(authData)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", rackspaceAPIURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("Error querying Rackspace Identity API: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Rackspace Authentication failed. Response code: %d", resp.StatusCode)
}
var rackspaceIdentity RackspaceIdentity
err = json.NewDecoder(resp.Body).Decode(&rackspaceIdentity)
if err != nil {
return nil, err
}
// Iterate through the Service Catalog to get the DNS Endpoint
var dnsEndpoint string
for _, service := range rackspaceIdentity.Access.ServiceCatalog {
if service.Name == "cloudDNS" {
dnsEndpoint = service.Endpoints[0].PublicURL
break
}
}
if dnsEndpoint == "" {
return nil, fmt.Errorf("Failed to populate DNS endpoint, check Rackspace API for changes.")
}
return &DNSProvider{
token: rackspaceIdentity.Access.Token.ID,
cloudDNSEndpoint: dnsEndpoint,
}, nil
}
// 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 := RackspaceRecords{
RackspaceRecord: []RackspaceRecord{{
Name: acme.UnFqdn(fqdn),
Type: "TXT",
Data: value,
TTL: 300,
}},
}
body, err := json.Marshal(rec)
if err != nil {
return err
}
_, err = c.makeRequest("POST", fmt.Sprintf("/domains/%d/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)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
}
record, err := c.findTxtRecord(fqdn, zoneID)
if err != nil {
return err
}
_, err = c.makeRequest("DELETE", fmt.Sprintf("/domains/%d/records?id=%s", zoneID, record.ID), nil)
if err != nil {
return err
}
return nil
}
// getHostedZoneID performs a lookup to get the DNS zone which needs
// modifying for a given FQDN
func (c *DNSProvider) getHostedZoneID(fqdn string) (int, error) {
// HostedZones represents the response when querying Rackspace DNS zones
type ZoneSearchResponse struct {
TotalEntries int `json:"totalEntries"`
HostedZones []struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"domains"`
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return 0, err
}
result, err := c.makeRequest("GET", fmt.Sprintf("/domains?name=%s", acme.UnFqdn(authZone)), nil)
if err != nil {
return 0, err
}
var zoneSearchResponse ZoneSearchResponse
err = json.Unmarshal(result, &zoneSearchResponse)
if err != nil {
return 0, err
}
// If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur)
if zoneSearchResponse.TotalEntries != 1 {
return 0, fmt.Errorf("Found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn)
}
return zoneSearchResponse.HostedZones[0].ID, nil
}
// findTxtRecord searches a DNS zone for a TXT record with a specific name
func (c *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*RackspaceRecord, error) {
result, err := c.makeRequest("GET", fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil)
if err != nil {
return nil, err
}
var records RackspaceRecords
err = json.Unmarshal(result, &records)
if err != nil {
return nil, err
}
recordsLength := len(records.RackspaceRecord)
switch recordsLength {
case 1:
break
case 0:
return nil, fmt.Errorf("No TXT record found for %s", fqdn)
default:
return nil, fmt.Errorf("More than 1 TXT record found for %s", fqdn)
}
return &records.RackspaceRecord[0], nil
}
// makeRequest is a wrapper function used for making DNS API requests
func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
url := c.cloudDNSEndpoint + uri
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("X-Auth-Token", c.token)
req.Header.Set("Content-Type", "application/json")
client := http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("Error querying DNS API: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
return nil, fmt.Errorf("Request failed for %s %s. Response code: %d", method, url, resp.StatusCode)
}
var r json.RawMessage
err = json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
return nil, fmt.Errorf("JSON decode failed for %s %s. Response code: %d", method, url, resp.StatusCode)
}
return r, nil
}
// RackspaceRecords is the list of records sent/recieved from the DNS API
type RackspaceRecords struct {
RackspaceRecord []RackspaceRecord `json:"records"`
}
// RackspaceRecord represents a Rackspace DNS record
type RackspaceRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Data string `json:"data"`
TTL int `json:"ttl,omitempty"`
ID string `json:"id,omitempty"`
}

View file

@ -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
}

View file

@ -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)},
},
}
}

View file

@ -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
}