Fix: Add TTL and custom Timeout in DigitalOcean DNS provider

This commit is contained in:
Ludovic Fernandez 2018-04-06 17:04:03 +02:00 committed by Traefiker Bot
parent 66485e81b4
commit 0ef1b7b683
120 changed files with 23764 additions and 9782 deletions

View file

@ -1,16 +0,0 @@
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")
)

View file

@ -1,825 +0,0 @@
// 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"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
var (
// Logger is an optional custom logger.
Logger *log.Logger
)
const (
// maxBodySize is the maximum size of body that we will read.
maxBodySize = 1024 * 1024
// overallRequestLimit is the overall number of request per second limited on the
// “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the
// limitation is 20 requests per second, but using 20 as value doesn't work but 18 do
overallRequestLimit = 18
)
// 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 {
for _, auth := range challenges {
c.disableAuthz(auth)
}
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 {
for _, auth := range challenges {
c.disableAuthz(auth)
}
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 {
c.disableAuthz(authz)
failures[authz.Domain] = err
}
}
} else {
c.disableAuthz(authz)
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)
delay := time.Second / overallRequestLimit
for _, domain := range domains {
time.Sleep(delay)
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)
errc <- domainError{Domain: domain, Error: errors.New("Server did not provide next link to proceed")}
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)
}
}
logAuthz(challenges)
close(resc)
close(errc)
return challenges, failures
}
func logAuthz(authz []authorizationResource) {
for _, auth := range authz {
logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL)
}
}
// cleanAuthz loops through the passed in slice and disables any auths which are not "valid"
func (c *Client) disableAuthz(auth authorizationResource) error {
var disabledAuth authorization
_, err := postJSON(c.jws, auth.AuthURL, deactivateAuthMessage{Resource: "authz", Status: "deactivated"}, &disabledAuth)
return err
}
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
}
certRes := CertificateResource{
Domain: commonName.Domain,
CertURL: resp.Header.Get("Location"),
PrivateKey: privateKeyPem,
}
maxChecks := 1000
for i := 0; i < maxChecks; i++ {
done, err := c.checkCertResponse(resp, &certRes, bundle)
resp.Body.Close()
if err != nil {
return CertificateResource{}, err
}
if done {
break
}
if i == maxChecks-1 {
return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i)
}
resp, err = httpGet(certRes.CertURL)
if err != nil {
return CertificateResource{}, err
}
}
return certRes, nil
}
// checkCertResponse checks resp to see if a certificate is contained in the
// response, and if so, loads it into certRes and returns true. If the cert
// is not yet ready, it returns false. This function honors the waiting period
// required by the Retry-After header of the response, if specified. This
// function may read from resp.Body but does NOT close it. The certRes input
// should already have the Domain (common name) field populated. If bundle is
// true, the certificate will be bundled with the issuer's cert.
func (c *Client) checkCertResponse(resp *http.Response, certRes *CertificateResource, bundle bool) (bool, error) {
switch resp.StatusCode {
case 201, 202:
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
if err != nil {
return false, 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 {
certRes.CertStableURL = resp.Header.Get("Content-Location")
certRes.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", certRes.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...)
}
}
certRes.Certificate = issuedCert
certRes.IssuerCertificate = issuerCert
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
return true, 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 false, err
}
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", certRes.Domain, retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
return false, nil
default:
return false, handleHTTPError(resp)
}
}
// 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, maxBodySize))
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
}
}
}

View file

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

View file

@ -1,309 +0,0 @@
package acme
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/miekg/dns"
)
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 succeeds, 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:]
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 {
// CNAME records cannot/should not exist at the root of a zone.
// So we skip a domain when a CNAME is found.
if dnsMsgContainsCNAME(in) {
continue
}
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")
}
// dnsMsgContainsCNAME checks for a CNAME answer in msg
func dnsMsgContainsCNAME(msg *dns.Msg) bool {
for _, ans := range msg.Answer {
if _, ok := ans.(*dns.CNAME); ok {
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

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

View file

@ -1,94 +0,0 @@
package acme
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
const (
tosAgreementError = "Must agree to subscriber agreement before any further actions"
invalidNonceError = "JWS has invalid anti-replay nonce"
)
// 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
}
// NonceError represents the error which is returned if the
// nonce sent by the client was not accepted by the server.
type NonceError 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
contentType := resp.Header.Get("Content-Type")
if contentType == "application/json" || contentType == "application/problem+json" {
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
if err != nil {
return err
}
} else {
detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
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}
}
if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) {
return NonceError{errorDetail}
}
return errorDetail
}
func handleChallengeError(chlng challenge) error {
return challengeError{chlng.Error, chlng.ValidationRecords}
}

View file

@ -1,160 +0,0 @@
package acme
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"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{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * 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, fmt.Errorf("failed to head %q: %v", url, err)
}
req.Header.Set("User-Agent", userAgent())
resp, err = HTTPClient.Do(req)
if err != nil {
return resp, fmt.Errorf("failed to do head %q: %v", url, 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, fmt.Errorf("failed to post %q: %v", url, 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, fmt.Errorf("failed to get %q: %v", url, 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 json %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 {
err := handleHTTPError(resp)
switch err.(type) {
case NonceError:
// Retry once if the nonce was invalidated
retryResp, err := j.post(uri, jsonBytes)
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
}
defer retryResp.Body.Close()
if retryResp.StatusCode >= http.StatusBadRequest {
return retryResp.Header, handleHTTPError(retryResp)
}
if respBody == nil {
return retryResp.Header, nil
}
return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody)
default:
return resp.Header, err
}
}
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)
}

View file

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

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

View file

@ -1,131 +0,0 @@
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 nonceManager
}
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.
// It does NOT close the response body, so the caller must
// do that if no error was returned.
func (j *jws) post(url string, content []byte) (*http.Response, error) {
signedContent, err := j.signContent(content)
if err != nil {
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
}
resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
if err != nil {
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
}
nonce, nonceErr := getNonceFromResponse(resp)
if nonceErr == nil {
j.nonces.Push(nonce)
}
return resp, nil
}
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, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
}
signer.SetNonceSource(j)
signed, err := signer.Sign(content)
if err != nil {
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
}
return signed, nil
}
func (j *jws) Nonce() (string, error) {
if nonce, ok := j.nonces.Pop(); ok {
return nonce, nil
}
return getNonce(j.directoryURL)
}
type nonceManager struct {
nonces []string
sync.Mutex
}
func (n *nonceManager) Pop() (string, bool) {
n.Lock()
defer n.Unlock()
if len(n.nonces) == 0 {
return "", false
}
nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true
}
func (n *nonceManager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
func getNonce(url string) (string, error) {
resp, err := httpHead(url)
if err != nil {
return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error())
}
return getNonceFromResponse(resp)
}
func getNonceFromResponse(resp *http.Response) (string, error) {
nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" {
return "", fmt.Errorf("Server did not respond with a proper nonce header.")
}
return nonce, nil
}

View file

@ -1,115 +0,0 @@
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"`
}
type deactivateAuthMessage struct {
Resource string `json:"resource,omitempty"`
Status string `jsom:"status"`
}
// 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:"-"`
}

View file

@ -1 +0,0 @@
package acme

View file

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

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

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

View file

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

View file

@ -1,13 +1,13 @@
package acme
package acmev2
// 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
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acmev2.md#http
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
HTTP01 = Challenge("http-01")
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acmev2.md#dns
// Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01")
)

View file

@ -1,5 +1,5 @@
// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers.
package acme
// package acmev2 implements the ACME protocol for Let's Encrypt and other conforming providers.
package acmev2
import (
"crypto"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"bytes"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"crypto/sha256"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"bufio"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"encoding/json"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"encoding/json"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"fmt"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"fmt"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"bytes"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"time"

View file

@ -1 +1 @@
package acme
package acmev2

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import "time"

View file

@ -1,4 +1,4 @@
package acme
package acmev2
import (
"fmt"

View file

@ -2,12 +2,13 @@ package auroradns
import (
"fmt"
"os"
"sync"
"github.com/edeckers/auroradnsclient"
"github.com/edeckers/auroradnsclient/records"
"github.com/edeckers/auroradnsclient/zones"
"github.com/xenolf/lego/acme"
"os"
"sync"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider describes a provider for AuroraDNS
@ -64,9 +65,9 @@ func (provider *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRe
// Present creates a record with a secret
func (provider *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
}
@ -80,7 +81,7 @@ func (provider *DNSProvider) Present(domain, token, keyAuth string) error {
subdomain := fqdn[0 : len(fqdn)-len(authZone)-1]
authZone = acme.UnFqdn(authZone)
authZone = acmev2.UnFqdn(authZone)
zoneRecord, err := provider.getZoneInformationByName(authZone)
@ -106,7 +107,7 @@ func (provider *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes a given record that was generated by Present
func (provider *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
provider.recordIDsMu.Lock()
recordID, ok := provider.recordIDs[fqdn]
@ -116,12 +117,12 @@ func (provider *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("Unknown recordID for '%s'", fqdn)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
authZone = acmev2.UnFqdn(authZone)
zoneRecord, err := provider.getZoneInformationByName(authZone)
if err != nil {

View file

@ -16,10 +16,10 @@ import (
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
type DNSProvider struct {
clientId string
clientSecret string
@ -64,7 +64,7 @@ func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
// 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)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
@ -74,7 +74,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
relative := toRelativeRecord(fqdn, acmev2.ToFqdn(zone))
rec := dns.RecordSet{
Name: &relative,
RecordSetProperties: &dns.RecordSetProperties{
@ -93,19 +93,19 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// Returns the relative record to the domain
func toRelativeRecord(domain, zone string) string {
return acme.UnFqdn(strings.TrimSuffix(domain, zone))
return acmev2.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)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
}
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
relative := toRelativeRecord(fqdn, acmev2.ToFqdn(zone))
rsc := dns.NewRecordSetsClient(c.subscriptionId)
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
@ -119,7 +119,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// 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)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return "", err
}
@ -130,7 +130,7 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
dc := dns.NewZonesClient(c.subscriptionId)
dc.Authorizer = autorest.NewBearerAuthorizer(spt)
zone, err := dc.Get(c.resourceGroup, acme.UnFqdn(authZone))
zone, err := dc.Get(c.resourceGroup, acmev2.UnFqdn(authZone))
if err != nil {
return "", err

View file

@ -0,0 +1,419 @@
// Package bluecat implements a DNS provider for solving the DNS-01 challenge
// using a self-hosted Bluecat Address Manager.
package bluecat
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"io/ioutil"
"github.com/xenolf/lego/acmev2"
)
const bluecatUrlTemplate = "%s/Services/REST/v1"
const configType = "Configuration"
const viewType = "View"
const txtType = "TXTRecord"
const zoneType = "Zone"
type entityResponse struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Properties string `json:"properties"`
}
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface that uses
// Bluecat's Address Manager REST API to manage TXT records for a domain.
type DNSProvider struct {
baseUrl string
userName string
password string
configName string
dnsView string
token string
httpClient *http.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS.
// Credentials must be passed in the environment variables: BLUECAT_SERVER_URL,
// BLUECAT_USER_NAME and BLUECAT_PASSWORD. BLUECAT_SERVER_URL should have the
// scheme, hostname, and port (if required) of the authoritative Bluecat BAM
// server. The REST endpoint will be appended. In addition, the Configuration name
// and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and
// BLUECAT_DNS_VIEW
func NewDNSProvider() (*DNSProvider, error) {
server := os.Getenv("BLUECAT_SERVER_URL")
userName := os.Getenv("BLUECAT_USER_NAME")
password := os.Getenv("BLUECAT_PASSWORD")
configName := os.Getenv("BLUECAT_CONFIG_NAME")
dnsView := os.Getenv("BLUECAT_DNS_VIEW")
httpClient := http.Client{Timeout: time.Duration(30 * time.Second)}
return NewDNSProviderCredentials(server, userName, password, configName, dnsView, httpClient)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Bluecat DNS.
func NewDNSProviderCredentials(server, userName, password, configName, dnsView string, httpClient http.Client) (*DNSProvider, error) {
if server == "" || userName == "" || password == "" || configName == "" || dnsView == "" {
return nil, fmt.Errorf("Bluecat credentials missing")
}
return &DNSProvider{
baseUrl: fmt.Sprintf(bluecatUrlTemplate, server),
userName: userName,
password: password,
configName: configName,
dnsView: dnsView,
httpClient: http.DefaultClient,
}, nil
}
// Send a REST request, using query parameters specified. The Authorization
// header will be set if we have an active auth token
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) {
url := fmt.Sprintf("%s/%s", d.baseUrl, 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("Authorization", d.token)
}
// Add all query parameters
q := req.URL.Query()
for argName, argVal := range queryArgs {
q.Add(argName, argVal)
}
req.URL.RawQuery = q.Encode()
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
errBytes, _ := ioutil.ReadAll(resp.Body)
errResp := string(errBytes)
return nil, fmt.Errorf("Bluecat API request failed with HTTP status code %d\n Full message: %s",
resp.StatusCode, errResp)
}
return resp, nil
}
// Starts a new Bluecat API Session. Authenticates using customerName, userName,
// password and receives a token to be used in for subsequent requests.
func (d *DNSProvider) login() error {
queryArgs := map[string]string{
"username": d.userName,
"password": d.password,
}
resp, err := d.sendRequest("GET", "login", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
authBytes, _ := ioutil.ReadAll(resp.Body)
authResp := string(authBytes)
if strings.Contains(authResp, "Authentication Error") {
msg := strings.Trim(authResp, "\"")
return fmt.Errorf("Bluecat API request failed: %s", msg)
}
// Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username"
re := regexp.MustCompile("BAMAuthToken: [^ ]+")
token := re.FindString(authResp)
d.token = token
return nil
}
// Destroys Bluecat Session
func (d *DNSProvider) logout() error {
if len(d.token) == 0 {
// nothing to do
return nil
}
resp, err := d.sendRequest("GET", "logout", nil, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("Bluecat API request failed to delete session with HTTP status code %d", resp.StatusCode)
} else {
authBytes, _ := ioutil.ReadAll(resp.Body)
authResp := string(authBytes)
if !strings.Contains(authResp, "successfully") {
msg := strings.Trim(authResp, "\"")
return fmt.Errorf("Bluecat API request failed to delete session: %s", msg)
}
}
d.token = ""
return nil
}
// Lookup the entity ID of the configuration named in our properties
func (d *DNSProvider) lookupConfId() (uint, error) {
queryArgs := map[string]string{
"parentId": strconv.Itoa(0),
"name": d.configName,
"type": configType,
}
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
if err != nil {
return 0, err
}
defer resp.Body.Close()
var conf entityResponse
err = json.NewDecoder(resp.Body).Decode(&conf)
if err != nil {
return 0, err
}
return conf.Id, nil
}
// Find the DNS view with the given name within
func (d *DNSProvider) lookupViewId(viewName string) (uint, error) {
confId, err := d.lookupConfId()
if err != nil {
return 0, err
}
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(confId), 10),
"name": d.dnsView,
"type": viewType,
}
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
if err != nil {
return 0, err
}
defer resp.Body.Close()
var view entityResponse
err = json.NewDecoder(resp.Body).Decode(&view)
if err != nil {
return 0, err
}
return view.Id, nil
}
// Return the entityId of the parent zone by recursing from the root view
// Also return the simple name of the host
func (d *DNSProvider) lookupParentZoneId(viewId uint, fqdn string) (uint, string, error) {
parentViewId := viewId
name := ""
if fqdn != "" {
zones := strings.Split(strings.Trim(fqdn, "."), ".")
last := len(zones) - 1
name = zones[0]
for i := last; i > -1; i-- {
zoneId, err := d.getZone(parentViewId, zones[i])
if err != nil || zoneId == 0 {
return parentViewId, name, err
}
if i > 0 {
name = strings.Join(zones[0:i], ".")
}
parentViewId = zoneId
}
}
return parentViewId, name, nil
}
// Get the DNS zone with the specified name under the parentId
func (d *DNSProvider) getZone(parentId uint, name string) (uint, error) {
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(parentId), 10),
"name": name,
"type": zoneType,
}
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
// Return an empty zone if the named zone doesn't exist
if resp != nil && resp.StatusCode == 404 {
return 0, fmt.Errorf("Bluecat API could not find zone named %s", name)
}
if err != nil {
return 0, err
}
defer resp.Body.Close()
var zone entityResponse
err = json.NewDecoder(resp.Body).Decode(&zone)
if err != nil {
return 0, err
}
return zone.Id, nil
}
// Present creates a TXT record using the specified parameters
// This will *not* create a subzone to contain the TXT record,
// so make sure the FQDN specified is within an extant zone.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewId, err := d.lookupViewId(d.dnsView)
if err != nil {
return err
}
parentZoneId, name, err := d.lookupParentZoneId(viewId, fqdn)
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(parentZoneId), 10),
}
body := bluecatEntity{
Name: name,
Type: "TXTRecord",
Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value),
}
resp, err := d.sendRequest("POST", "addEntity", body, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
addTxtBytes, _ := ioutil.ReadAll(resp.Body)
addTxtResp := string(addTxtBytes)
// addEntity responds only with body text containing the ID of the created record
_, err = strconv.ParseUint(addTxtResp, 10, 64)
if err != nil {
return fmt.Errorf("Bluecat API addEntity request failed: %s", addTxtResp)
}
err = d.deploy(uint(parentZoneId))
if err != nil {
return err
}
err = d.logout()
if err != nil {
return err
}
return nil
}
// Deploy the DNS config for the specified entity to the authoritative servers
func (d *DNSProvider) deploy(entityId uint) error {
queryArgs := map[string]string{
"entityId": strconv.FormatUint(uint64(entityId), 10),
}
resp, err := d.sendRequest("POST", "quickDeploy", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewId, err := d.lookupViewId(d.dnsView)
if err != nil {
return err
}
parentId, name, err := d.lookupParentZoneId(viewId, fqdn)
if err != nil {
return err
}
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(parentId), 10),
"name": name,
"type": txtType,
}
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
var txtRec entityResponse
err = json.NewDecoder(resp.Body).Decode(&txtRec)
if err != nil {
return err
}
queryArgs = map[string]string{
"objectId": strconv.FormatUint(uint64(txtRec.Id), 10),
}
resp, err = d.sendRequest("DELETE", "delete", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
err = d.deploy(parentId)
if err != nil {
return err
}
err = d.logout()
if err != nil {
return err
}
return nil
}
//JSON body for Bluecat entity requests and responses
type bluecatEntity struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Properties string `json:"properties"`
}

View file

@ -11,14 +11,14 @@ import (
"os"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// 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
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
type DNSProvider struct {
authEmail string
authKey string
@ -54,7 +54,7 @@ func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
// 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)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
@ -62,7 +62,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
rec := cloudFlareRecord{
Type: "TXT",
Name: acme.UnFqdn(fqdn),
Name: acmev2.UnFqdn(fqdn),
Content: value,
TTL: 120,
}
@ -82,7 +82,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
record, err := c.findTxtRecord(fqdn)
if err != nil {
@ -104,12 +104,12 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
Name string `json:"name"`
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return "", err
}
result, err := c.makeRequest("GET", "/zones?name="+acme.UnFqdn(authZone), nil)
result, err := c.makeRequest("GET", "/zones?name="+acmev2.UnFqdn(authZone), nil)
if err != nil {
return "", err
}
@ -135,7 +135,7 @@ func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
result, err := c.makeRequest(
"GET",
fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)),
fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acmev2.UnFqdn(fqdn)),
nil,
)
if err != nil {
@ -149,7 +149,7 @@ func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
}
for _, rec := range records {
if rec.Name == acme.UnFqdn(fqdn) {
if rec.Name == acmev2.UnFqdn(fqdn) {
return &rec, nil
}
}

View file

@ -13,12 +13,12 @@ import (
"strconv"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
const cloudXNSBaseURL = "https://www.cloudxns.net/api2/"
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
type DNSProvider struct {
apiKey string
secretKey string
@ -48,7 +48,7 @@ func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
@ -59,7 +59,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
@ -79,7 +79,7 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
Domain string `json:"domain"`
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return "", err
}
@ -117,7 +117,7 @@ func (c *DNSProvider) findTxtRecord(zoneID, fqdn string) (string, error) {
}
for _, record := range records {
if record.Host == acme.UnFqdn(fqdn) && record.Type == "TXT" {
if record.Host == acmev2.UnFqdn(fqdn) && record.Type == "TXT" {
return record.RecordID, nil
}
}
@ -133,7 +133,7 @@ func (c *DNSProvider) addTxtRecord(zoneID, fqdn, value string, ttl int) error {
payload := cloudXNSRecord{
ID: id,
Host: acme.UnFqdn(fqdn),
Host: acmev2.UnFqdn(fqdn),
Value: value,
Type: "TXT",
LineID: 1,
@ -183,7 +183,7 @@ func (c *DNSProvider) makeRequest(method, uri string, body []byte) (json.RawMess
req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body)))
req.Header.Set("API-FORMAT", "json")
resp, err := acme.HTTPClient.Do(req)
resp, err := acmev2.HTTPClient.Do(req)
if err != nil {
return nil, err
}

View file

@ -11,10 +11,10 @@ import (
"sync"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
// that uses DigitalOcean's REST API to manage TXT records for a domain.
type DNSProvider struct {
apiAuthToken string
@ -49,6 +49,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
RecordType string `json:"type"`
Name string `json:"name"`
Data string `json:"data"`
TTL int `json:"ttl"`
}
// txtRecordResponse represents a response from DO's API after making a TXT record
@ -61,17 +62,17 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
} `json:"domain_record"`
}
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
authZone = acmev2.UnFqdn(authZone)
reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone)
reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value}
reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value, TTL: 60}
body, err := json.Marshal(reqData)
if err != nil {
return err
@ -112,7 +113,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
// get the record's unique ID from when we created it
d.recordIDsMu.Lock()
@ -122,12 +123,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("unknown record ID for '%s'", fqdn)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
authZone = acmev2.UnFqdn(authZone)
reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", digitalOceanBaseURL, authZone, recordID)
req, err := http.NewRequest("DELETE", reqURL, nil)
@ -164,3 +165,9 @@ type digitalOceanAPIError struct {
}
var digitalOceanBaseURL = "https://api.digitalocean.com"
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 90 * time.Second, 5 * time.Second
}

View file

@ -4,23 +4,30 @@ package dns
import (
"fmt"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
"github.com/xenolf/lego/providers/dns/auroradns"
"github.com/xenolf/lego/providers/dns/azure"
"github.com/xenolf/lego/providers/dns/bluecat"
"github.com/xenolf/lego/providers/dns/cloudflare"
"github.com/xenolf/lego/providers/dns/cloudxns"
"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/duckdns"
"github.com/xenolf/lego/providers/dns/dyn"
"github.com/xenolf/lego/providers/dns/exec"
"github.com/xenolf/lego/providers/dns/exoscale"
"github.com/xenolf/lego/providers/dns/fastdns"
"github.com/xenolf/lego/providers/dns/gandi"
"github.com/xenolf/lego/providers/dns/gandiv5"
"github.com/xenolf/lego/providers/dns/googlecloud"
"github.com/xenolf/lego/providers/dns/glesys"
"github.com/xenolf/lego/providers/dns/godaddy"
"github.com/xenolf/lego/providers/dns/googlecloud"
"github.com/xenolf/lego/providers/dns/lightsail"
"github.com/xenolf/lego/providers/dns/linode"
"github.com/xenolf/lego/providers/dns/namecheap"
"github.com/xenolf/lego/providers/dns/namedotcom"
"github.com/xenolf/lego/providers/dns/ns1"
"github.com/xenolf/lego/providers/dns/otc"
"github.com/xenolf/lego/providers/dns/ovh"
@ -31,14 +38,16 @@ import (
"github.com/xenolf/lego/providers/dns/vultr"
)
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
func NewDNSChallengeProviderByName(name string) (acmev2.ChallengeProvider, error) {
var err error
var provider acme.ChallengeProvider
var provider acmev2.ChallengeProvider
switch name {
case "azure":
provider, err = azure.NewDNSProvider()
case "auroradns":
provider, err = auroradns.NewDNSProvider()
case "bluecat":
provider, err = bluecat.NewDNSProvider()
case "cloudflare":
provider, err = cloudflare.NewDNSProvider()
case "cloudxns":
@ -51,24 +60,34 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
provider, err = dnsmadeeasy.NewDNSProvider()
case "dnspod":
provider, err = dnspod.NewDNSProvider()
case "duckdns":
provider, err = duckdns.NewDNSProvider()
case "dyn":
provider, err = dyn.NewDNSProvider()
case "fastdns":
provider, err = fastdns.NewDNSProvider()
case "exoscale":
provider, err = exoscale.NewDNSProvider()
case "gandi":
provider, err = gandi.NewDNSProvider()
case "gandiv5":
provider, err = gandiv5.NewDNSProvider()
case "glesys":
provider, err = glesys.NewDNSProvider()
case "gcloud":
provider, err = googlecloud.NewDNSProvider()
case "godaddy":
provider, err = godaddy.NewDNSProvider()
case "lightsail":
provider, err = lightsail.NewDNSProvider()
case "linode":
provider, err = linode.NewDNSProvider()
case "manual":
provider, err = acme.NewDNSProviderManual()
provider, err = acmev2.NewDNSProviderManual()
case "namecheap":
provider, err = namecheap.NewDNSProvider()
case "namedotcom":
provider, err = namedotcom.NewDNSProvider()
case "rackspace":
provider, err = rackspace.NewDNSProvider()
case "route53":
@ -85,6 +104,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
provider, err = ns1.NewDNSProvider()
case "otc":
provider, err = otc.NewDNSProvider()
case "exec":
provider, err = exec.NewDNSProvider()
default:
err = fmt.Errorf("Unrecognised DNS provider: %s", name)
}

View file

@ -9,10 +9,10 @@ import (
"strings"
"github.com/dnsimple/dnsimple-go/dnsimple"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface.
type DNSProvider struct {
client *dnsimple.Client
}
@ -47,7 +47,7 @@ func NewDNSProviderCredentials(accessToken, baseUrl string) (*DNSProvider, error
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zoneName, err := c.getHostedZone(domain)
@ -71,7 +71,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
records, err := c.findTxtRecords(domain, fqdn)
if err != nil {
@ -94,7 +94,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
func (c *DNSProvider) getHostedZone(domain string) (string, error) {
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return "", err
}
@ -104,7 +104,7 @@ func (c *DNSProvider) getHostedZone(domain string) (string, error) {
return "", err
}
zoneName := acme.UnFqdn(authZone)
zoneName := acmev2.UnFqdn(authZone)
zones, err := c.client.Zones.ListZones(accountID, &dnsimple.ZoneListOptions{NameLike: zoneName})
if err != nil {
@ -159,7 +159,7 @@ func (c *DNSProvider) newTxtRecord(zoneName, fqdn, value string, ttl int) *dnsim
}
func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
name := acmev2.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}

View file

@ -14,10 +14,10 @@ import (
"strings"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface that uses
// DNSMadeEasy's DNS API to manage TXT records for a domain.
type DNSProvider struct {
baseURL string
@ -77,9 +77,9 @@ func NewDNSProviderCredentials(baseURL, apiKey, apiSecret string) (*DNSProvider,
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domainName, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return err
}
@ -104,9 +104,9 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
// CleanUp removes the TXT records matching the specified parameters
func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domainName, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domainName, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return err
}

View file

@ -8,10 +8,10 @@ import (
"strings"
"github.com/decker502/dnspod-go"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface.
type DNSProvider struct {
client *dnspod.Client
}
@ -38,7 +38,7 @@ func NewDNSProviderCredentials(key string) (*DNSProvider, error) {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zoneID, zoneName, err := c.getHostedZone(domain)
if err != nil {
return err
@ -55,7 +55,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
records, err := c.findTxtRecords(domain, fqdn)
if err != nil {
@ -82,14 +82,14 @@ func (c *DNSProvider) getHostedZone(domain string) (string, string, error) {
return "", "", fmt.Errorf("dnspod API call failed: %v", err)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return "", "", err
}
var hostedZone dnspod.Domain
for _, zone := range zones {
if zone.Name == acme.UnFqdn(authZone) {
if zone.Name == acmev2.UnFqdn(authZone) {
hostedZone = zone
}
}
@ -138,7 +138,7 @@ func (c *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnspod.Record, erro
}
func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
name := acmev2.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}

View file

@ -0,0 +1,82 @@
// Adds lego support for http://duckdns.org .
//
// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.
package duckdns
import (
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider adds and removes the record for the DNS challenge
type DNSProvider struct {
// The duckdns api token
token string
}
// NewDNSProvider returns a new DNS provider using
// environment variable DUCKDNS_TOKEN for adding and removing the DNS record.
func NewDNSProvider() (*DNSProvider, error) {
duckdnsToken := os.Getenv("DUCKDNS_TOKEN")
return NewDNSProviderCredentials(duckdnsToken)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for http://duckdns.org .
func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) {
if duckdnsToken == "" {
return nil, errors.New("environment variable DUCKDNS_TOKEN not set")
}
return &DNSProvider{token: duckdnsToken}, nil
}
// makeDuckdnsURL creates a url to clear the set or unset the TXT record.
// txt == "" will clear the TXT record.
func makeDuckdnsURL(domain, token, txt string) string {
requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token)
if txt == "" {
return requestBase + "&clear=true"
}
return requestBase + "&txt=" + txt
}
func issueDuckdnsRequest(url string) error {
response, err := acmev2.HTTPClient.Get(url)
if err != nil {
return err
}
defer response.Body.Close()
bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
body := string(bodyBytes)
if body != "OK" {
return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url)
}
return nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// In duckdns you only have one TXT record shared with
// the domain and all sub domains.
//
// To update the TXT record we just need to make one simple get request.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord, _ := acmev2.DNS01Record(domain, keyAuth)
url := makeDuckdnsURL(domain, d.token, txtRecord)
return issueDuckdnsRequest(url)
}
// CleanUp clears duckdns TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
url := makeDuckdnsURL(domain, d.token, "")
return issueDuckdnsRequest(url)
}

View file

@ -11,7 +11,7 @@ import (
"strconv"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
var dynBaseURL = "https://api.dynect.net/REST"
@ -30,7 +30,7 @@ type dynResponse struct {
Messages json.RawMessage `json:"msgs"`
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface that uses
// Dyn's Managed DNS API to manage TXT records for a domain.
type DNSProvider struct {
customerName string
@ -87,11 +87,8 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{})
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
if resp.StatusCode >= 500 {
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
@ -100,6 +97,13 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{})
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages)
} 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")
}
if dynRes.Status == "failure" {
// TODO add better error handling
return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages)
@ -172,9 +176,9 @@ func (d *DNSProvider) logout() error {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return err
}
@ -228,9 +232,9 @@ func (d *DNSProvider) publish(zone, notes string) error {
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return err
}

View file

@ -0,0 +1,72 @@
// Package exec implements a manual DNS provider which runs a program for
// adding/removing the DNS record.
//
// The file name of the external program is specified in the environment
// variable EXEC_PATH. When it is run by lego, three command-line parameters
// are passed to it: The action ("present" or "cleanup"), the fully-qualified domain
// name, the value for the record and the TTL.
//
// For example, requesting a certificate for the domain 'foo.example.com' can
// be achieved by calling lego as follows:
//
// EXEC_PATH=./update-dns.sh \
// lego --dns exec \
// --domains foo.example.com \
// --email invalid@example.com run
//
// It will then call the program './update-dns.sh' with like this:
//
// ./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120"
//
// The program then needs to make sure the record is inserted. When it returns
// an error via a non-zero exit code, lego aborts.
//
// When the record is to be removed again, the program is called with the first
// command-line parameter set to "cleanup" instead of "present".
package exec
import (
"errors"
"os"
"os/exec"
"strconv"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider adds and removes the record for the DNS challenge by calling a
// program with command-line parameters.
type DNSProvider struct {
program string
}
// NewDNSProvider returns a new DNS provider which runs the program in the
// environment variable EXEC_PATH for adding and removing the DNS record.
func NewDNSProvider() (*DNSProvider, error) {
s := os.Getenv("EXEC_PATH")
if s == "" {
return nil, errors.New("environment variable EXEC_PATH not set")
}
return &DNSProvider{program: s}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
cmd := exec.Command(d.program, "present", fqdn, value, strconv.Itoa(ttl))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
cmd := exec.Command(d.program, "cleanup", fqdn, value, strconv.Itoa(ttl))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

View file

@ -8,10 +8,10 @@ import (
"os"
"github.com/exoscale/egoscale"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface.
type DNSProvider struct {
client *egoscale.Client
}
@ -42,7 +42,7 @@ func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zone, recordName, err := c.FindZoneAndRecordName(fqdn, domain)
if err != nil {
return err
@ -78,7 +78,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zone, recordName, err := c.FindZoneAndRecordName(fqdn, domain)
if err != nil {
return err
@ -116,12 +116,12 @@ func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, erro
// 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)
zone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return "", "", err
}
zone = acme.UnFqdn(zone)
name := acme.UnFqdn(fqdn)
zone = acmev2.UnFqdn(zone)
name := acmev2.UnFqdn(fqdn)
name = name[:len(name)-len("."+zone)]
return zone, name, nil

View file

@ -0,0 +1,139 @@
package fastdns
import (
"fmt"
"os"
"reflect"
configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v1"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface.
type DNSProvider struct {
config edgegrid.Config
}
// NewDNSProvider uses the supplied environment variables to return a DNSProvider instance:
// AKAMAI_HOST, AKAMAI_CLIENT_TOKEN, AKAMAI_CLIENT_SECRET, AKAMAI_ACCESS_TOKEN
func NewDNSProvider() (*DNSProvider, error) {
host := os.Getenv("AKAMAI_HOST")
clientToken := os.Getenv("AKAMAI_CLIENT_TOKEN")
clientSecret := os.Getenv("AKAMAI_CLIENT_SECRET")
accessToken := os.Getenv("AKAMAI_ACCESS_TOKEN")
return NewDNSProviderClient(host, clientToken, clientSecret, accessToken)
}
// NewDNSProviderClient uses the supplied parameters to return a DNSProvider instance
// configured for FastDNS.
func NewDNSProviderClient(host, clientToken, clientSecret, accessToken string) (*DNSProvider, error) {
if clientToken == "" || clientSecret == "" || accessToken == "" || host == "" {
return nil, fmt.Errorf("Akamai FastDNS credentials missing")
}
config := edgegrid.Config{
Host: host,
ClientToken: clientToken,
ClientSecret: clientSecret,
AccessToken: accessToken,
MaxBody: 131072,
}
return &DNSProvider{
config: config,
}, nil
}
// Present creates a TXT record to fullfil the dns-01 challenge.
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zoneName, recordName, err := c.findZoneAndRecordName(fqdn, domain)
if err != nil {
return err
}
configdns.Init(c.config)
zone, err := configdns.GetZone(zoneName)
if err != nil {
return err
}
record := configdns.NewTxtRecord()
record.SetField("name", recordName)
record.SetField("ttl", ttl)
record.SetField("target", value)
record.SetField("active", true)
existingRecord := c.findExistingRecord(zone, recordName)
if existingRecord != nil {
if reflect.DeepEqual(existingRecord.ToMap(), record.ToMap()) {
return nil
}
zone.RemoveRecord(existingRecord)
return c.createRecord(zone, record)
}
return c.createRecord(zone, record)
}
// CleanUp removes the record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zoneName, recordName, err := c.findZoneAndRecordName(fqdn, domain)
if err != nil {
return err
}
configdns.Init(c.config)
zone, err := configdns.GetZone(zoneName)
if err != nil {
return err
}
existingRecord := c.findExistingRecord(zone, recordName)
if existingRecord != nil {
err := zone.RemoveRecord(existingRecord)
if err != nil {
return err
}
return zone.Save()
}
return nil
}
func (c *DNSProvider) findZoneAndRecordName(fqdn, domain string) (string, string, error) {
zone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return "", "", err
}
zone = acmev2.UnFqdn(zone)
name := acmev2.UnFqdn(fqdn)
name = name[:len(name)-len("."+zone)]
return zone, name, nil
}
func (c *DNSProvider) findExistingRecord(zone *configdns.Zone, recordName string) *configdns.TxtRecord {
for _, r := range zone.Zone.Txt {
if r.Name == recordName {
return r
}
}
return nil
}
func (c *DNSProvider) createRecord(zone *configdns.Zone, record *configdns.TxtRecord) error {
err := zone.AddRecord(record)
if err != nil {
return err
}
return zone.Save()
}

View file

@ -14,7 +14,7 @@ import (
"sync"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// Gandi API reference: http://doc.rpc.gandi.net/index.html
@ -26,7 +26,7 @@ var (
endpoint = "https://rpc.gandi.net/xmlrpc/"
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
// during tests.
findZoneByFqdn = acme.FindZoneByFqdn
findZoneByFqdn = acmev2.FindZoneByFqdn
)
// inProgressInfo contains information about an in-progress challenge
@ -37,7 +37,7 @@ type inProgressInfo struct {
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses Gandi's XML-RPC
// acmev2.ChallengeProviderTimeout interface that uses Gandi's XML-RPC
// API to manage TXT records for a domain.
type DNSProvider struct {
apiKey string
@ -70,12 +70,12 @@ func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
// 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)
fqdn, value, ttl := acmev2.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)
authZone, err := findZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
}
@ -103,7 +103,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// containing the required TXT record
newZoneName := fmt.Sprintf(
"%s [ACME Challenge %s]",
acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
acmev2.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
newZoneID, err := d.cloneZone(zoneID, newZoneName)
if err != nil {
return err
@ -138,7 +138,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// 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)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
// acquire lock and retrieve zoneID, newZoneID and authZone
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()

View file

@ -12,7 +12,7 @@ import (
"sync"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// Gandi API reference: http://doc.livedns.gandi.net/
@ -23,7 +23,7 @@ var (
endpoint = "https://dns.api.gandi.net/api/v5"
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
// during tests.
findZoneByFqdn = acme.FindZoneByFqdn
findZoneByFqdn = acmev2.FindZoneByFqdn
)
// inProgressInfo contains information about an in-progress challenge
@ -33,7 +33,7 @@ type inProgressInfo struct {
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses Gandi's LiveDNS
// acmev2.ChallengeProviderTimeout interface that uses Gandi's LiveDNS
// API to manage TXT records for a domain.
type DNSProvider struct {
apiKey string
@ -62,12 +62,12 @@ func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
if ttl < 300 {
ttl = 300 // 300 is gandi minimum value for ttl
}
// find authZone
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := findZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
}
@ -83,7 +83,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()
// add TXT record into authZone
err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, ttl)
err = d.addTXTRecord(acmev2.UnFqdn(authZone), name, value, ttl)
if err != nil {
return err
}
@ -97,7 +97,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
// acquire lock and retrieve authZone
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()
@ -109,7 +109,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
authZone := d.inProgressFQDNs[fqdn].authZone
delete(d.inProgressFQDNs, fqdn)
// delete TXT record from authZone
err := d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName)
err := d.deleteTXTRecord(acmev2.UnFqdn(authZone), fieldName)
if err != nil {
return err
}

View file

@ -0,0 +1,211 @@
// Package glesys implements a DNS provider for solving the DNS-01
// challenge using GleSYS api.
package glesys
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/xenolf/lego/acmev2"
)
// GleSYS API reference: https://github.com/GleSYS/API/wiki/API-Documentation
// domainAPI is the GleSYS API endpoint used by Present and CleanUp.
const domainAPI = "https://api.glesys.com/domain"
var (
// Logger is used to log API communication results;
// if nil, the default log.Logger is used.
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...)
}
}
// DNSProvider is an implementation of the
// acmev2.ChallengeProviderTimeout interface that uses GleSYS
// API to manage TXT records for a domain.
type DNSProvider struct {
apiUser string
apiKey string
activeRecords map[string]int
inProgressMu sync.Mutex
}
// NewDNSProvider returns a DNSProvider instance configured for GleSYS.
// Credentials must be passed in the environment variables: GLESYS_API_USER
// and GLESYS_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
apiUser := os.Getenv("GLESYS_API_USER")
apiKey := os.Getenv("GLESYS_API_KEY")
return NewDNSProviderCredentials(apiUser, apiKey)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for GleSYS.
func NewDNSProviderCredentials(apiUser string, apiKey string) (*DNSProvider, error) {
if apiUser == "" || apiKey == "" {
return nil, fmt.Errorf("GleSYS DNS: Incomplete credentials provided")
}
return &DNSProvider{
apiUser: apiUser,
apiKey: apiKey,
activeRecords: make(map[string]int),
}, nil
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
if ttl < 60 {
ttl = 60 // 60 is GleSYS minimum value for ttl
}
// find authZone
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("GleSYS DNS: findZoneByFqdn failure: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf(
"GleSYS 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()
// add TXT record into authZone
recordId, err := d.addTXTRecord(domain, acmev2.UnFqdn(authZone), name, value, ttl)
if err != nil {
return err
}
// save data necessary for CleanUp
d.activeRecords[fqdn] = recordId
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
// acquire lock and retrieve authZone
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()
if _, ok := d.activeRecords[fqdn]; !ok {
// if there is no cleanup information then just return
return nil
}
recordId := d.activeRecords[fqdn]
delete(d.activeRecords, fqdn)
// delete TXT record from authZone
err := d.deleteTXTRecord(domain, recordId)
if err != nil {
return err
}
return nil
}
// Timeout returns the values (20*time.Minute, 20*time.Second) which
// are used by the acme package as timeout and check interval values
// when checking for DNS record propagation with GleSYS.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 20 * time.Minute, 20 * time.Second
}
// types for JSON method calls, parameters, and responses
type addRecordRequest struct {
Domainname string `json:"domainname"`
Host string `json:"host"`
Type string `json:"type"`
Data string `json:"data"`
Ttl int `json:"ttl,omitempty"`
}
type deleteRecordRequest struct {
Recordid int `json:"recordid"`
}
type responseStruct struct {
Response struct {
Status struct {
Code int `json:"code"`
} `json:"status"`
Record deleteRecordRequest `json:"record"`
} `json:"response"`
}
// POSTing/Marshalling/Unmarshalling
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
url := fmt.Sprintf("%s/%s", domainAPI, 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")
req.SetBasicAuth(d.apiUser, d.apiKey)
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("GleSYS DNS: request failed with HTTP status code %d", resp.StatusCode)
}
var response responseStruct
err = json.NewDecoder(resp.Body).Decode(&response)
return &response, err
}
// functions to perform API actions
func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) {
response, err := d.sendRequest("POST", "addrecord", addRecordRequest{
Domainname: domain,
Host: name,
Type: "TXT",
Data: value,
Ttl: ttl,
})
if response != nil && response.Response.Status.Code == 200 {
logf("[INFO][%s] GleSYS DNS: Successfully created recordid %d", fqdn, response.Response.Record.Recordid)
return response.Response.Record.Recordid, nil
}
return 0, err
}
func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error {
response, err := d.sendRequest("POST", "deleterecord", deleteRecordRequest{
Recordid: recordid,
})
if response != nil && response.Response.Status.Code == 200 {
logf("[INFO][%s] GleSYS DNS: Successfully deleted recordid %d", fqdn, recordid)
}
return err
}

View file

@ -10,15 +10,16 @@ import (
"bytes"
"encoding/json"
"github.com/xenolf/lego/acme"
"io/ioutil"
"strings"
"github.com/xenolf/lego/acmev2"
)
// GoDaddyAPIURL represents the API endpoint to call.
const apiURL = "https://api.godaddy.com"
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
type DNSProvider struct {
apiKey string
apiSecret string
@ -50,7 +51,7 @@ func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
}
func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
name := acmev2.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
@ -59,7 +60,7 @@ func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
domainZone, err := c.getZone(fqdn)
if err != nil {
return err
@ -105,7 +106,7 @@ func (c *DNSProvider) updateRecords(records []DNSRecord, domainZone string, reco
// CleanUp sets null value in the TXT DNS record as GoDaddy has no proper DELETE record method
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
domainZone, err := c.getZone(fqdn)
if err != nil {
return err
@ -124,12 +125,12 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
func (c *DNSProvider) getZone(fqdn string) (string, error) {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return "", err
}
return acme.UnFqdn(authZone), nil
return acmev2.UnFqdn(authZone), nil
}
func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Response, error) {

View file

@ -8,7 +8,7 @@ import (
"os"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
"golang.org/x/net/context"
"golang.org/x/oauth2"
@ -88,7 +88,7 @@ func NewDNSProviderServiceAccount(project string, saFile string) (*DNSProvider,
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(domain)
if err != nil {
@ -135,7 +135,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(domain)
if err != nil {
@ -167,7 +167,7 @@ func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
// getHostedZone returns the managed-zone
func (c *DNSProvider) getHostedZone(domain string) (string, error) {
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return "", err
}

View file

@ -0,0 +1,107 @@
// Package lightsail implements a DNS provider for solving the DNS-01 challenge
// using AWS Lightsail DNS.
package lightsail
import (
"math/rand"
"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/lightsail"
"github.com/xenolf/lego/acmev2"
)
const (
maxRetries = 5
)
// DNSProvider implements the acmev2.ChallengeProvider interface
type DNSProvider struct {
client *lightsail.Lightsail
}
// 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
// Lightsail 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_SESSION_TOKEN], [DNS_ZONE]
// 2. Shared credentials file (defaults to ~/.aws/credentials)
// 3. Amazon EC2 IAM role
//
// public hosted zone via the FQDN.
//
// 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 := lightsail.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, _ := acmev2.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
err := r.newTxtRecord(domain, fqdn, value)
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
params := &lightsail.DeleteDomainEntryInput{
DomainName: aws.String(domain),
DomainEntry: &lightsail.DomainEntry{
Name: aws.String(fqdn),
Type: aws.String("TXT"),
Target: aws.String(value),
},
}
_, err := r.client.DeleteDomainEntry(params)
return err
}
func (r *DNSProvider) newTxtRecord(domain string, fqdn string, value string) error {
params := &lightsail.CreateDomainEntryInput{
DomainName: aws.String(domain),
DomainEntry: &lightsail.DomainEntry{
Name: aws.String(fqdn),
Target: aws.String(value),
Type: aws.String("TXT"),
},
}
_, err := r.client.CreateDomainEntry(params)
return err
}

View file

@ -9,7 +9,7 @@ import (
"time"
"github.com/timewasted/linode/dns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
const (
@ -23,7 +23,7 @@ type hostedZoneInfo struct {
resourceName string
}
// DNSProvider implements the acme.ChallengeProvider interface.
// DNSProvider implements the acmev2.ChallengeProvider interface.
type DNSProvider struct {
linode *dns.DNS
}
@ -66,13 +66,13 @@ func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
// Present creates a TXT record using the specified parameters.
func (p *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acmev2.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 {
if _, err = p.linode.CreateDomainResourceTXT(zone.domainId, acmev2.UnFqdn(fqdn), value, 60); err != nil {
return err
}
@ -81,7 +81,7 @@ func (p *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
zone, err := p.getHostedZoneInfo(fqdn)
if err != nil {
return err
@ -112,14 +112,14 @@ func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
func (p *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
// Lookup the zone that handles the specified FQDN.
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return nil, err
}
resourceName := strings.TrimSuffix(fqdn, "."+authZone)
// Query the authority zone.
domain, err := p.linode.GetDomain(acme.UnFqdn(authZone))
domain, err := p.linode.GetDomain(acmev2.UnFqdn(authZone))
if err != nil {
return nil, err
}

View file

@ -12,7 +12,7 @@ import (
"strings"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// Notes about namecheap's tool API:
@ -132,7 +132,7 @@ type challenge struct {
// 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)
domain = acmev2.UnFqdn(domain)
parts := strings.Split(domain, ".")
// Find the longest matching TLD.
@ -155,7 +155,7 @@ func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, e
host = strings.Join(parts[:longest-1], ".")
}
key, keyValue, _ := acme.DNS01Record(domain, keyAuth)
key, keyValue, _ := acmev2.DNS01Record(domain, keyAuth)
return &challenge{
domain: domain,

View file

@ -0,0 +1,124 @@
// Package namedotcom implements a DNS provider for solving the DNS-01 challenge
// using Name.com's DNS service.
package namedotcom
import (
"fmt"
"os"
"strings"
"github.com/namedotcom/go/namecom"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface.
type DNSProvider struct {
client *namecom.NameCom
}
// NewDNSProvider returns a DNSProvider instance configured for namedotcom.
// Credentials must be passed in the environment variables: NAMECOM_USERNAME and NAMECOM_API_TOKEN
func NewDNSProvider() (*DNSProvider, error) {
username := os.Getenv("NAMECOM_USERNAME")
apiToken := os.Getenv("NAMECOM_API_TOKEN")
server := os.Getenv("NAMECOM_SERVER")
return NewDNSProviderCredentials(username, apiToken, server)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for namedotcom.
func NewDNSProviderCredentials(username, apiToken, server string) (*DNSProvider, error) {
if username == "" {
return nil, fmt.Errorf("Name.com Username is required")
}
if apiToken == "" {
return nil, fmt.Errorf("Name.com API token is required")
}
client := namecom.New(username, apiToken)
if server != "" {
client.Server = server
}
return &DNSProvider{client: 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 := acmev2.DNS01Record(domain, keyAuth)
request := &namecom.Record{
DomainName: domain,
Host: c.extractRecordName(fqdn, domain),
Type: "TXT",
TTL: uint32(ttl),
Answer: value,
}
_, err := c.client.CreateRecord(request)
if err != nil {
return fmt.Errorf("namedotcom 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, _, _ := acmev2.DNS01Record(domain, keyAuth)
records, err := c.getRecords(domain)
if err != nil {
return err
}
for _, rec := range records {
if rec.Fqdn == fqdn && rec.Type == "TXT" {
request := &namecom.DeleteRecordRequest{
DomainName: domain,
ID: rec.ID,
}
_, err := c.client.DeleteRecord(request)
if err != nil {
return err
}
}
}
return nil
}
func (c *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) {
var (
err error
records []*namecom.Record
response *namecom.ListRecordsResponse
)
request := &namecom.ListRecordsRequest{
DomainName: domain,
Page: 1,
}
for request.Page > 0 {
response, err = c.client.ListRecords(request)
if err != nil {
return nil, err
}
records = append(records, response.Records...)
request.Page = response.NextPage
}
return records, nil
}
func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acmev2.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}

View file

@ -8,12 +8,12 @@ import (
"os"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
"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.
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface.
type DNSProvider struct {
client *rest.Client
}
@ -43,7 +43,7 @@ func NewDNSProviderCredentials(key string) (*DNSProvider, error) {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(domain)
if err != nil {
@ -61,14 +61,14 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(domain)
if err != nil {
return err
}
name := acme.UnFqdn(fqdn)
name := acmev2.UnFqdn(fqdn)
_, err = c.client.Records.Delete(zone.Zone, name, "TXT")
return err
}
@ -83,7 +83,7 @@ func (c *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) {
}
func (c *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record {
name := acme.UnFqdn(fqdn)
name := acmev2.UnFqdn(fqdn)
return &dns.Record{
Type: "TXT",

View file

@ -12,10 +12,10 @@ import (
"os"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface that uses
// OTC's Managed DNS API to manage TXT records for a domain.
type DNSProvider struct {
identityEndpoint string
@ -313,13 +313,13 @@ func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
if ttl < 300 {
ttl = 300 // 300 is otc minimum value for ttl
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return err
}
@ -362,9 +362,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return err
}

View file

@ -9,13 +9,13 @@ import (
"sync"
"github.com/ovh/go-ovh/ovh"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// 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
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
// that uses OVH's REST API to manage TXT records for a domain.
type DNSProvider struct {
client *ovh.Client
@ -78,15 +78,15 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
Zone string `json:"zone"`
}
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
// Parse domain name
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
authZone = acmev2.UnFqdn(authZone)
subDomain := d.extractRecordName(fqdn, authZone)
reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone)
@ -117,7 +117,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
// get the record's unique ID from when we created it
d.recordIDsMu.Lock()
@ -127,12 +127,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("unknown record ID for '%s'", fqdn)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(acmev2.ToFqdn(domain), acmev2.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
authZone = acmev2.UnFqdn(authZone)
reqURL := fmt.Sprintf("/domain/zone/%s/record/%d", authZone, recordID)
@ -151,7 +151,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
name := acmev2.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}

View file

@ -14,10 +14,10 @@ import (
"strings"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
type DNSProvider struct {
apiKey string
host *url.URL
@ -65,7 +65,7 @@ func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
// 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)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(fqdn)
if err != nil {
return err
@ -75,7 +75,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// pre-v1 API wants non-fqdn
if c.apiVersion == 0 {
name = acme.UnFqdn(fqdn)
name = acmev2.UnFqdn(fqdn)
}
rec := pdnsRecord{
@ -117,7 +117,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zone, err := c.getHostedZone(fqdn)
if err != nil {
@ -153,7 +153,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
func (c *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
var zone hostedZone
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return nil, err
}
@ -172,7 +172,7 @@ func (c *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
url = ""
for _, zone := range zones {
if acme.UnFqdn(zone.Name) == acme.UnFqdn(authZone) {
if acmev2.UnFqdn(zone.Name) == acmev2.UnFqdn(authZone) {
url = zone.URL
}
}
@ -215,7 +215,7 @@ func (c *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) {
}
for _, set := range zone.RRSets {
if (set.Name == acme.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" {
if (set.Name == acmev2.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" {
return &set, nil
}
}

View file

@ -11,13 +11,13 @@ import (
"os"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// 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
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
// used to store the reusable token and DNS API endpoint
type DNSProvider struct {
token string
@ -126,7 +126,7 @@ func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) {
// 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)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
@ -134,7 +134,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
rec := RackspaceRecords{
RackspaceRecord: []RackspaceRecord{{
Name: acme.UnFqdn(fqdn),
Name: acmev2.UnFqdn(fqdn),
Type: "TXT",
Data: value,
TTL: 300,
@ -156,7 +156,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
@ -187,12 +187,12 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (int, error) {
} `json:"domains"`
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return 0, err
}
result, err := c.makeRequest("GET", fmt.Sprintf("/domains?name=%s", acme.UnFqdn(authZone)), nil)
result, err := c.makeRequest("GET", fmt.Sprintf("/domains?name=%s", acmev2.UnFqdn(authZone)), nil)
if err != nil {
return 0, err
}
@ -213,7 +213,7 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (int, error) {
// 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)
result, err := c.makeRequest("GET", fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acmev2.UnFqdn(fqdn)), nil)
if err != nil {
return nil, err
}

View file

@ -10,10 +10,10 @@ import (
"time"
"github.com/miekg/dns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface that
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface that
// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
type DNSProvider struct {
nameserver string
@ -27,7 +27,7 @@ type DNSProvider struct {
// dynamic update. Configured with environment variables:
// RFC2136_NAMESERVER: Network address in the form "host" or "host:port".
// RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5).
// See https://github.com/miekg/dns/blob/master/tsig.go for supported values.
// See https://github.com/miekg/dns/blob/master/tsig.go for supported values.
// RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration.
// RFC2136_TSIG_SECRET: Secret key payload.
// RFC2136_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s)
@ -88,24 +88,24 @@ func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, t
// Returns the timeout configured with RFC2136_TIMEOUT, or 60s.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.timeout, 2 * time.Second
return d.timeout, 2 * time.Second
}
// 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)
fqdn, value, ttl := acmev2.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)
fqdn, value, ttl := acmev2.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})
zone, err := acmev2.FindZoneByFqdn(fqdn, []string{r.nameserver})
if err != nil {
return err
}

View file

@ -14,7 +14,7 @@ import (
"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"
"github.com/xenolf/lego/acmev2"
)
const (
@ -22,7 +22,7 @@ const (
route53TTL = 10
)
// DNSProvider implements the acme.ChallengeProvider interface
// DNSProvider implements the acmev2.ChallengeProvider interface
type DNSProvider struct {
client *route53.Route53
hostedZoneID string
@ -80,14 +80,14 @@ func NewDNSProvider() (*DNSProvider, error) {
// Present creates a TXT record using the specified parameters
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acmev2.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)
fqdn, value, _ := acmev2.DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("DELETE", fqdn, value, route53TTL)
}
@ -119,7 +119,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
statusID := resp.ChangeInfo.Id
return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) {
return acmev2.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) {
reqParams := &route53.GetChangeInput{
Id: statusID,
}
@ -139,14 +139,14 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
return r.hostedZoneID, nil
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
if err != nil {
return "", err
}
// .DNSName should not have a trailing dot
reqParams := &route53.ListHostedZonesByNameInput{
DNSName: aws.String(acme.UnFqdn(authZone)),
DNSName: aws.String(acmev2.UnFqdn(authZone)),
}
resp, err := r.client.ListHostedZonesByName(reqParams)
if err != nil {

View file

@ -9,10 +9,10 @@ import (
"strings"
vultr "github.com/JamesClonk/vultr/lib"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acmev2"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
// DNSProvider is an implementation of the acmev2.ChallengeProvider interface.
type DNSProvider struct {
client *vultr.Client
}
@ -40,7 +40,7 @@ func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
// 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)
fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
zoneDomain, err := c.getHostedZone(domain)
if err != nil {
@ -59,7 +59,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
zoneDomain, records, err := c.findTxtRecords(domain, fqdn)
if err != nil {
@ -119,7 +119,7 @@ func (c *DNSProvider) findTxtRecords(domain, fqdn string) (string, []vultr.DNSRe
}
func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
name := acmev2.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}