Migrate to go-acme/lego.

This commit is contained in:
Ludovic Fernandez 2019-03-14 11:04:04 +01:00 committed by Traefiker Bot
parent 4a68d29ce2
commit 87da7520de
286 changed files with 14021 additions and 2501 deletions

21
vendor/github.com/go-acme/lego/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-2017 Sebastian Erhart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

69
vendor/github.com/go-acme/lego/acme/api/account.go generated vendored Normal file
View file

@ -0,0 +1,69 @@
package api
import (
"encoding/base64"
"errors"
"fmt"
"github.com/go-acme/lego/acme"
)
type AccountService service
// New Creates a new account.
func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
var account acme.Account
resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account)
location := getLocation(resp)
if len(location) > 0 {
a.core.jws.SetKid(location)
}
if err != nil {
return acme.ExtendedAccount{Location: location}, err
}
return acme.ExtendedAccount{Account: account, Location: location}, nil
}
// NewEAB Creates a new account with an External Account Binding.
func (a *AccountService) NewEAB(accMsg acme.Account, kid string, hmacEncoded string) (acme.ExtendedAccount, error) {
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
if err != nil {
return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %v", err)
}
eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)
if err != nil {
return acme.ExtendedAccount{}, fmt.Errorf("acme: error signing eab content: %v", err)
}
accMsg.ExternalAccountBinding = eabJWS
return a.New(accMsg)
}
// Get Retrieves an account.
func (a *AccountService) Get(accountURL string) (acme.Account, error) {
if len(accountURL) == 0 {
return acme.Account{}, errors.New("account[get]: empty URL")
}
var account acme.Account
_, err := a.core.post(accountURL, acme.Account{}, &account)
if err != nil {
return acme.Account{}, err
}
return account, nil
}
// Deactivate Deactivates an account.
func (a *AccountService) Deactivate(accountURL string) error {
if len(accountURL) == 0 {
return errors.New("account[deactivate]: empty URL")
}
req := acme.Account{Status: acme.StatusDeactivated}
_, err := a.core.post(accountURL, req, nil)
return err
}

166
vendor/github.com/go-acme/lego/acme/api/api.go generated vendored Normal file
View file

@ -0,0 +1,166 @@
package api
import (
"bytes"
"context"
"crypto"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/cenkalti/backoff"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/acme/api/internal/nonces"
"github.com/go-acme/lego/acme/api/internal/secure"
"github.com/go-acme/lego/acme/api/internal/sender"
"github.com/go-acme/lego/log"
)
// Core ACME/LE core API.
type Core struct {
doer *sender.Doer
nonceManager *nonces.Manager
jws *secure.JWS
directory acme.Directory
HTTPClient *http.Client
common service // Reuse a single struct instead of allocating one for each service on the heap.
Accounts *AccountService
Authorizations *AuthorizationService
Certificates *CertificateService
Challenges *ChallengeService
Orders *OrderService
}
// New Creates a new Core.
func New(httpClient *http.Client, userAgent string, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) {
doer := sender.NewDoer(httpClient, userAgent)
dir, err := getDirectory(doer, caDirURL)
if err != nil {
return nil, err
}
nonceManager := nonces.NewManager(doer, dir.NewNonceURL)
jws := secure.NewJWS(privateKey, kid, nonceManager)
c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir, HTTPClient: httpClient}
c.common.core = c
c.Accounts = (*AccountService)(&c.common)
c.Authorizations = (*AuthorizationService)(&c.common)
c.Certificates = (*CertificateService)(&c.common)
c.Challenges = (*ChallengeService)(&c.common)
c.Orders = (*OrderService)(&c.common)
return c, nil
}
// post performs an HTTP POST request and parses the response body as JSON,
// into the provided respBody object.
func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) {
content, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.New("failed to marshal message")
}
return a.retrievablePost(uri, content, response)
}
// postAsGet performs an HTTP POST ("POST-as-GET") request.
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-6.3
func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) {
return a.retrievablePost(uri, []byte{}, response)
}
func (a *Core) retrievablePost(uri string, content []byte, response interface{}) (*http.Response, error) {
// during tests, allow to support ~90% of bad nonce with a minimum of attempts.
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = 200 * time.Millisecond
bo.MaxInterval = 5 * time.Second
bo.MaxElapsedTime = 20 * time.Second
ctx, cancel := context.WithCancel(context.Background())
var resp *http.Response
operation := func() error {
var err error
resp, err = a.signedPost(uri, content, response)
if err != nil {
switch err.(type) {
// Retry if the nonce was invalidated
case *acme.NonceError:
log.Infof("nonce error retry: %s", err)
return err
default:
cancel()
return err
}
}
return nil
}
err := backoff.Retry(operation, backoff.WithContext(bo, ctx))
if err != nil {
return nil, err
}
return resp, nil
}
func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) {
signedContent, err := a.jws.SignContent(uri, content)
if err != nil {
return nil, fmt.Errorf("failed to post JWS message -> failed to sign content -> %v", err)
}
signedBody := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response)
// nonceErr is ignored to keep the root error.
nonce, nonceErr := nonces.GetFromResponse(resp)
if nonceErr == nil {
a.nonceManager.Push(nonce)
}
return resp, err
}
func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) {
eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac)
if err != nil {
return nil, err
}
return []byte(eabJWS.FullSerialize()), nil
}
// GetKeyAuthorization Gets the key authorization
func (a *Core) GetKeyAuthorization(token string) (string, error) {
return a.jws.GetKeyAuthorization(token)
}
func (a *Core) GetDirectory() acme.Directory {
return a.directory
}
func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
var dir acme.Directory
if _, err := do.Get(caDirURL, &dir); err != nil {
return dir, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
}
if dir.NewAccountURL == "" {
return dir, errors.New("directory missing new registration URL")
}
if dir.NewOrderURL == "" {
return dir, errors.New("directory missing new order URL")
}
return dir, nil
}

View file

@ -0,0 +1,34 @@
package api
import (
"errors"
"github.com/go-acme/lego/acme"
)
type AuthorizationService service
// Get Gets an authorization.
func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) {
if len(authzURL) == 0 {
return acme.Authorization{}, errors.New("authorization[get]: empty URL")
}
var authz acme.Authorization
_, err := c.core.postAsGet(authzURL, &authz)
if err != nil {
return acme.Authorization{}, err
}
return authz, nil
}
// Deactivate Deactivates an authorization.
func (c *AuthorizationService) Deactivate(authzURL string) error {
if len(authzURL) == 0 {
return errors.New("authorization[deactivate]: empty URL")
}
var disabledAuth acme.Authorization
_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)
return err
}

99
vendor/github.com/go-acme/lego/acme/api/certificate.go generated vendored Normal file
View file

@ -0,0 +1,99 @@
package api
import (
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"net/http"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/certcrypto"
"github.com/go-acme/lego/log"
)
// maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024
type CertificateService service
// Get Returns the certificate and the issuer certificate.
// 'bundle' is only applied if the issuer is provided by the 'up' link.
func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) {
cert, up, err := c.get(certURL)
if err != nil {
return nil, nil, err
}
// Get issuerCert from bundled response from Let's Encrypt
// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962
_, issuer := pem.Decode(cert)
if issuer != nil {
return cert, issuer, nil
}
issuer, err = c.getIssuerFromLink(up)
if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err)
} else if len(issuer) > 0 {
// 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 {
cert = append(cert, issuer...)
}
}
return cert, issuer, nil
}
// Revoke Revokes a certificate.
func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error {
_, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil)
return err
}
// get Returns the certificate and the "up" link.
func (c *CertificateService) get(certURL string) ([]byte, string, error) {
if len(certURL) == 0 {
return nil, "", errors.New("certificate[get]: empty URL")
}
resp, err := c.core.postAsGet(certURL, nil)
if err != nil {
return nil, "", err
}
cert, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil {
return nil, "", err
}
// The issuer certificate link may be supplied via an "up" link
// in the response headers of a new certificate.
// See https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2
up := getLink(resp.Header, "up")
return cert, up, err
}
// getIssuerFromLink requests the issuer certificate
func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) {
if len(up) == 0 {
return nil, nil
}
log.Infof("acme: Requesting issuer cert from %s", up)
cert, _, err := c.get(up)
if err != nil {
return nil, err
}
_, err = x509.ParseCertificate(cert)
if err != nil {
return nil, err
}
return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert)), nil
}

45
vendor/github.com/go-acme/lego/acme/api/challenge.go generated vendored Normal file
View file

@ -0,0 +1,45 @@
package api
import (
"errors"
"github.com/go-acme/lego/acme"
)
type ChallengeService service
// New Creates a challenge.
func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
if len(chlgURL) == 0 {
return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL")
}
// Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`.
// We use an empty struct instance as the postJSON payload here to achieve this result.
var chlng acme.ExtendedChallenge
resp, err := c.core.post(chlgURL, struct{}{}, &chlng)
if err != nil {
return acme.ExtendedChallenge{}, err
}
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}
// Get Gets a challenge.
func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
if len(chlgURL) == 0 {
return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL")
}
var chlng acme.ExtendedChallenge
resp, err := c.core.postAsGet(chlgURL, &chlng)
if err != nil {
return acme.ExtendedChallenge{}, err
}
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}

View file

@ -0,0 +1,78 @@
package nonces
import (
"errors"
"fmt"
"net/http"
"sync"
"github.com/go-acme/lego/acme/api/internal/sender"
)
// Manager Manages nonces.
type Manager struct {
do *sender.Doer
nonceURL string
nonces []string
sync.Mutex
}
// NewManager Creates a new Manager.
func NewManager(do *sender.Doer, nonceURL string) *Manager {
return &Manager{
do: do,
nonceURL: nonceURL,
}
}
// Pop Pops a nonce.
func (n *Manager) 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
}
// Push Pushes a nonce.
func (n *Manager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
// Nonce implement jose.NonceSource
func (n *Manager) Nonce() (string, error) {
if nonce, ok := n.Pop(); ok {
return nonce, nil
}
return n.getNonce()
}
func (n *Manager) getNonce() (string, error) {
resp, err := n.do.Head(n.nonceURL)
if err != nil {
return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %v", err)
}
return GetFromResponse(resp)
}
// GetFromResponse Extracts a nonce from a HTTP response.
func GetFromResponse(resp *http.Response) (string, error) {
if resp == nil {
return "", errors.New("nil response")
}
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

@ -0,0 +1,134 @@
package secure
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"errors"
"fmt"
"github.com/go-acme/lego/acme/api/internal/nonces"
jose "gopkg.in/square/go-jose.v2"
)
// JWS Represents a JWS.
type JWS struct {
privKey crypto.PrivateKey
kid string // Key identifier
nonces *nonces.Manager
}
// NewJWS Create a new JWS.
func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS {
return &JWS{
privKey: privateKey,
nonces: nonceManager,
kid: kid,
}
}
// SetKid Sets a key identifier.
func (j *JWS) SetKid(kid string) {
j.kid = kid
}
// SignContent Signs a content with the JWS.
func (j *JWS) SignContent(url string, 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
}
}
signKey := jose.SigningKey{
Algorithm: alg,
Key: jose.JSONWebKey{Key: j.privKey, KeyID: j.kid},
}
options := jose.SignerOptions{
NonceSource: j.nonces,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
}
if j.kid == "" {
options.EmbedJWK = true
}
signer, err := jose.NewSigner(signKey, &options)
if err != nil {
return nil, fmt.Errorf("failed to create jose signer -> %v", err)
}
signed, err := signer.Sign(content)
if err != nil {
return nil, fmt.Errorf("failed to sign content -> %v", err)
}
return signed, nil
}
// SignEABContent Signs an external account binding content with the JWS.
func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
jwk := jose.JSONWebKey{Key: j.privKey}
jwkJSON, err := jwk.Public().MarshalJSON()
if err != nil {
return nil, fmt.Errorf("acme: error encoding eab jwk key: %v", err)
}
signer, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
&jose.SignerOptions{
EmbedJWK: false,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"kid": kid,
"url": url,
},
},
)
if err != nil {
return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %v", err)
}
signed, err := signer.Sign(jwkJSON)
if err != nil {
return nil, fmt.Errorf("failed to External Account Binding sign content -> %v", err)
}
return signed, nil
}
// GetKeyAuthorization Gets the key authorization for a token.
func (j *JWS) GetKeyAuthorization(token string) (string, error) {
var publicKey crypto.PublicKey
switch k := j.privKey.(type) {
case *ecdsa.PrivateKey:
publicKey = k.Public()
case *rsa.PrivateKey:
publicKey = k.Public()
}
// Generate the Key Authorization for the challenge
jwk := &jose.JSONWebKey{Key: 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.RawURLEncoding.EncodeToString(thumbBytes)
return token + "." + keyThumb, nil
}

View file

@ -0,0 +1,146 @@
package sender
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"runtime"
"strings"
"github.com/go-acme/lego/acme"
)
type RequestOption func(*http.Request) error
func contentType(ct string) RequestOption {
return func(req *http.Request) error {
req.Header.Set("Content-Type", ct)
return nil
}
}
type Doer struct {
httpClient *http.Client
userAgent string
}
// NewDoer Creates a new Doer.
func NewDoer(client *http.Client, userAgent string) *Doer {
return &Doer{
httpClient: client,
userAgent: userAgent,
}
}
// Get performs a GET request with a proper User-Agent string.
// If "response" is not provided, callers should close resp.Body when done reading from it.
func (d *Doer) Get(url string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return d.do(req, response)
}
// Head performs a HEAD request with a proper User-Agent string.
// The response body (resp.Body) is already closed when this function returns.
func (d *Doer) Head(url string) (*http.Response, error) {
req, err := d.newRequest(http.MethodHead, url, nil)
if err != nil {
return nil, err
}
return d.do(req, nil)
}
// Post performs a POST request with a proper User-Agent string.
// If "response" is not provided, callers should close resp.Body when done reading from it.
func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) {
req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))
if err != nil {
return nil, err
}
return d.do(req, response)
}
func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
req, err := http.NewRequest(method, uri, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("User-Agent", d.formatUserAgent())
for _, opt := range opts {
err = opt(req)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
}
return req, nil
}
func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) {
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, err
}
if err = checkError(req, resp); err != nil {
return resp, err
}
if response != nil {
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp, err
}
defer resp.Body.Close()
err = json.Unmarshal(raw, response)
if err != nil {
return resp, fmt.Errorf("failed to unmarshal %q to type %T: %v", raw, response, err)
}
}
return resp, nil
}
// formatUserAgent builds and returns the User-Agent string to use in requests.
func (d *Doer) formatUserAgent() string {
ua := fmt.Sprintf("%s %s (%s; %s; %s)", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)
return strings.TrimSpace(ua)
}
func checkError(req *http.Request, resp *http.Response) error {
if resp.StatusCode >= http.StatusBadRequest {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%d :: %s :: %s :: %v", resp.StatusCode, req.Method, req.URL, err)
}
var errorDetails *acme.ProblemDetails
err = json.Unmarshal(body, &errorDetails)
if err != nil {
return fmt.Errorf("%d ::%s :: %s :: %v :: %s", resp.StatusCode, req.Method, req.URL, err, string(body))
}
errorDetails.Method = req.Method
errorDetails.URL = req.URL.String()
// Check for errors we handle specifically
if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr {
return &acme.NonceError{ProblemDetails: errorDetails}
}
return errorDetails
}
return nil
}

View file

@ -0,0 +1,14 @@
package sender
// CODE GENERATED AUTOMATICALLY
// THIS FILE MUST NOT BE EDITED BY HAND
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/2.3.0"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "detach"
)

65
vendor/github.com/go-acme/lego/acme/api/order.go generated vendored Normal file
View file

@ -0,0 +1,65 @@
package api
import (
"encoding/base64"
"errors"
"github.com/go-acme/lego/acme"
)
type OrderService service
// New Creates a new order.
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
var identifiers []acme.Identifier
for _, domain := range domains {
identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain})
}
orderReq := acme.Order{Identifiers: identifiers}
var order acme.Order
resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
if err != nil {
return acme.ExtendedOrder{}, err
}
return acme.ExtendedOrder{
Location: resp.Header.Get("Location"),
Order: order,
}, nil
}
// Get Gets an order.
func (o *OrderService) Get(orderURL string) (acme.Order, error) {
if len(orderURL) == 0 {
return acme.Order{}, errors.New("order[get]: empty URL")
}
var order acme.Order
_, err := o.core.postAsGet(orderURL, &order)
if err != nil {
return acme.Order{}, err
}
return order, nil
}
// UpdateForCSR Updates an order for a CSR.
func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.Order, error) {
csrMsg := acme.CSRMessage{
Csr: base64.RawURLEncoding.EncodeToString(csr),
}
var order acme.Order
_, err := o.core.post(orderURL, csrMsg, &order)
if err != nil {
return acme.Order{}, err
}
if order.Status == acme.StatusInvalid {
return acme.Order{}, order.Error
}
return order, nil
}

45
vendor/github.com/go-acme/lego/acme/api/service.go generated vendored Normal file
View file

@ -0,0 +1,45 @@
package api
import (
"net/http"
"regexp"
)
type service struct {
core *Core
}
// getLink get a rel into the Link header
func getLink(header http.Header, rel string) string {
var linkExpr = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`)
for _, link := range header["Link"] {
for _, m := range linkExpr.FindAllStringSubmatch(link, -1) {
if len(m) != 3 {
continue
}
if m[2] == rel {
return m[1]
}
}
}
return ""
}
// getLocation get the value of the header Location
func getLocation(resp *http.Response) string {
if resp == nil {
return ""
}
return resp.Header.Get("Location")
}
// getRetryAfter get the value of the header Retry-After
func getRetryAfter(resp *http.Response) string {
if resp == nil {
return ""
}
return resp.Header.Get("Retry-After")
}

284
vendor/github.com/go-acme/lego/acme/commons.go generated vendored Normal file
View file

@ -0,0 +1,284 @@
// Package acme contains all objects related the ACME endpoints.
// https://tools.ietf.org/html/draft-ietf-acme-acme-16
package acme
import (
"encoding/json"
"time"
)
// Challenge statuses
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.6
const (
StatusPending = "pending"
StatusInvalid = "invalid"
StatusValid = "valid"
StatusProcessing = "processing"
StatusDeactivated = "deactivated"
StatusExpired = "expired"
StatusRevoked = "revoked"
)
// Directory the ACME directory object.
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.1
type Directory struct {
NewNonceURL string `json:"newNonce"`
NewAccountURL string `json:"newAccount"`
NewOrderURL string `json:"newOrder"`
NewAuthzURL string `json:"newAuthz"`
RevokeCertURL string `json:"revokeCert"`
KeyChangeURL string `json:"keyChange"`
Meta Meta `json:"meta"`
}
// Meta the ACME meta object (related to Directory).
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.1
type Meta struct {
// termsOfService (optional, string):
// A URL identifying the current terms of service.
TermsOfService string `json:"termsOfService"`
// website (optional, string):
// An HTTP or HTTPS URL locating a website providing more information about the ACME server.
Website string `json:"website"`
// caaIdentities (optional, array of string):
// The hostnames that the ACME server recognizes as referring to itself
// for the purposes of CAA record validation as defined in [RFC6844].
// Each string MUST represent the same sequence of ASCII code points
// that the server will expect to see as the "Issuer Domain Name" in a CAA issue or issuewild property tag.
// This allows clients to determine the correct issuer domain name to use when configuring CAA records.
CaaIdentities []string `json:"caaIdentities"`
// externalAccountRequired (optional, boolean):
// If this field is present and set to "true",
// then the CA requires that all new- account requests include an "externalAccountBinding" field
// associating the new account with an external account.
ExternalAccountRequired bool `json:"externalAccountRequired"`
}
// ExtendedAccount a extended Account.
type ExtendedAccount struct {
Account
// Contains the value of the response header `Location`
Location string `json:"-"`
}
// Account the ACME account Object.
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.2
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.3
type Account struct {
// status (required, string):
// The status of this account.
// Possible values are: "valid", "deactivated", and "revoked".
// The value "deactivated" should be used to indicate client-initiated deactivation
// whereas "revoked" should be used to indicate server- initiated deactivation. (See Section 7.1.6)
Status string `json:"status,omitempty"`
// contact (optional, array of string):
// An array of URLs that the server can use to contact the client for issues related to this account.
// For example, the server may wish to notify the client about server-initiated revocation or certificate expiration.
// For information on supported URL schemes, see Section 7.3
Contact []string `json:"contact,omitempty"`
// termsOfServiceAgreed (optional, boolean):
// Including this field in a new-account request,
// with a value of true, indicates the client's agreement with the terms of service.
// This field is not updateable by the client.
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
// orders (required, string):
// A URL from which a list of orders submitted by this account can be fetched via a POST-as-GET request,
// as described in Section 7.1.2.1.
Orders string `json:"orders,omitempty"`
// onlyReturnExisting (optional, boolean):
// If this field is present with the value "true",
// then the server MUST NOT create a new account if one does not already exist.
// This allows a client to look up an account URL based on an account key (see Section 7.3.1).
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
// externalAccountBinding (optional, object):
// An optional field for binding the new account with an existing non-ACME account (see Section 7.3.4).
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
}
// ExtendedOrder a extended Order.
type ExtendedOrder struct {
Order
// The order URL, contains the value of the response header `Location`
Location string `json:"-"`
}
// Order the ACME order Object.
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.3
type Order struct {
// status (required, string):
// The status of this order.
// Possible values are: "pending", "ready", "processing", "valid", and "invalid".
Status string `json:"status,omitempty"`
// expires (optional, string):
// The timestamp after which the server will consider this order invalid,
// encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED for objects with "pending" or "valid" in the status field.
Expires string `json:"expires,omitempty"`
// identifiers (required, array of object):
// An array of identifier objects that the order pertains to.
Identifiers []Identifier `json:"identifiers"`
// notBefore (optional, string):
// The requested value of the notBefore field in the certificate,
// in the date format defined in [RFC3339].
NotBefore string `json:"notBefore,omitempty"`
// notAfter (optional, string):
// The requested value of the notAfter field in the certificate,
// in the date format defined in [RFC3339].
NotAfter string `json:"notAfter,omitempty"`
// error (optional, object):
// The error that occurred while processing the order, if any.
// This field is structured as a problem document [RFC7807].
Error *ProblemDetails `json:"error,omitempty"`
// authorizations (required, array of string):
// For pending orders,
// the authorizations that the client needs to complete before the requested certificate can be issued (see Section 7.5),
// including unexpired authorizations that the client has completed in the past for identifiers specified in the order.
// The authorizations required are dictated by server policy
// and there may not be a 1:1 relationship between the order identifiers and the authorizations required.
// For final orders (in the "valid" or "invalid" state), the authorizations that were completed.
// Each entry is a URL from which an authorization can be fetched with a POST-as-GET request.
Authorizations []string `json:"authorizations,omitempty"`
// finalize (required, string):
// A URL that a CSR must be POSTed to once all of the order's authorizations are satisfied to finalize the order.
// The result of a successful finalization will be the population of the certificate URL for the order.
Finalize string `json:"finalize,omitempty"`
// certificate (optional, string):
// A URL for the certificate that has been issued in response to this order
Certificate string `json:"certificate,omitempty"`
}
// Authorization the ACME authorization object.
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.4
type Authorization struct {
// status (required, string):
// The status of this authorization.
// Possible values are: "pending", "valid", "invalid", "deactivated", "expired", and "revoked".
Status string `json:"status"`
// expires (optional, string):
// The timestamp after which the server will consider this authorization invalid,
// encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED for objects with "valid" in the "status" field.
Expires time.Time `json:"expires,omitempty"`
// identifier (required, object):
// The identifier that the account is authorized to represent
Identifier Identifier `json:"identifier,omitempty"`
// challenges (required, array of objects):
// For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier.
// For valid authorizations, the challenge that was validated.
// For invalid authorizations, the challenge that was attempted and failed.
// Each array entry is an object with parameters required to validate the challenge.
// A client should attempt to fulfill one of these challenges,
// and a server should consider any one of the challenges sufficient to make the authorization valid.
Challenges []Challenge `json:"challenges,omitempty"`
// wildcard (optional, boolean):
// For authorizations created as a result of a newOrder request containing a DNS identifier
// with a value that contained a wildcard prefix this field MUST be present, and true.
Wildcard bool `json:"wildcard,omitempty"`
}
// ExtendedChallenge a extended Challenge.
type ExtendedChallenge struct {
Challenge
// Contains the value of the response header `Retry-After`
RetryAfter string `json:"-"`
// Contains the value of the response header `Link` rel="up"
AuthorizationURL string `json:"-"`
}
// Challenge the ACME challenge object.
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.5
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8
type Challenge struct {
// type (required, string):
// The type of challenge encoded in the object.
Type string `json:"type"`
// url (required, string):
// The URL to which a response can be posted.
URL string `json:"url"`
// status (required, string):
// The status of this challenge. Possible values are: "pending", "processing", "valid", and "invalid".
Status string `json:"status"`
// validated (optional, string):
// The time at which the server validated this challenge,
// encoded in the format specified in RFC 3339 [RFC3339].
// This field is REQUIRED if the "status" field is "valid".
Validated time.Time `json:"validated,omitempty"`
// error (optional, object):
// Error that occurred while the server was validating the challenge, if any,
// structured as a problem document [RFC7807].
// Multiple errors can be indicated by using subproblems Section 6.7.1.
// A challenge object with an error MUST have status equal to "invalid".
Error *ProblemDetails `json:"error,omitempty"`
// token (required, string):
// A random value that uniquely identifies the challenge.
// This value MUST have at least 128 bits of entropy.
// It MUST NOT contain any characters outside the base64url alphabet,
// and MUST NOT include base64 padding characters ("=").
// See [RFC4086] for additional information on randomness requirements.
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.3
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.4
Token string `json:"token"`
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.1
KeyAuthorization string `json:"keyAuthorization"`
}
// Identifier the ACME identifier object.
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-9.7.7
type Identifier struct {
Type string `json:"type"`
Value string `json:"value"`
}
// CSRMessage Certificate Signing Request
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.4
type CSRMessage struct {
// csr (required, string):
// A CSR encoding the parameters for the certificate being requested [RFC2986].
// The CSR is sent in the base64url-encoded version of the DER format.
// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.).
Csr string `json:"csr"`
}
// RevokeCertMessage a certificate revocation message
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.6
// - https://tools.ietf.org/html/rfc5280#section-5.3.1
type RevokeCertMessage struct {
// certificate (required, string):
// The certificate to be revoked, in the base64url-encoded version of the DER format.
// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.)
Certificate string `json:"certificate"`
// reason (optional, int):
// One of the revocation reasonCodes defined in Section 5.3.1 of [RFC5280] to be used when generating OCSP responses and CRLs.
// If this field is not set the server SHOULD omit the reasonCode CRL entry extension when generating OCSP responses and CRLs.
// The server MAY disallow a subset of reasonCodes from being used by the user.
// If a request contains a disallowed reasonCode the server MUST reject it with the error type "urn:ietf:params:acme:error:badRevocationReason".
// The problem document detail SHOULD indicate which reasonCodes are allowed.
Reason *uint `json:"reason,omitempty"`
}

58
vendor/github.com/go-acme/lego/acme/errors.go generated vendored Normal file
View file

@ -0,0 +1,58 @@
package acme
import (
"fmt"
)
// Errors types
const (
errNS = "urn:ietf:params:acme:error:"
BadNonceErr = errNS + "badNonce"
)
// ProblemDetails the problem details object
// - https://tools.ietf.org/html/rfc7807#section-3.1
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.3.3
type ProblemDetails struct {
Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
HTTPStatus int `json:"status,omitempty"`
Instance string `json:"instance,omitempty"`
SubProblems []SubProblem `json:"subproblems,omitempty"`
// additional values to have a better error message (Not defined by the RFC)
Method string `json:"method,omitempty"`
URL string `json:"url,omitempty"`
}
// SubProblem a "subproblems"
// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-6.7.1
type SubProblem struct {
Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
Identifier Identifier `json:"identifier,omitempty"`
}
func (p ProblemDetails) Error() string {
msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus)
if len(p.Method) != 0 || len(p.URL) != 0 {
msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL)
}
msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail)
for _, sub := range p.SubProblems {
msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail)
}
if len(p.Instance) == 0 {
msg += ", url: " + p.Instance
}
return msg
}
// NonceError represents the error which is returned
// if the nonce sent by the client was not accepted by the server.
type NonceError struct {
*ProblemDetails
}

256
vendor/github.com/go-acme/lego/certcrypto/crypto.go generated vendored Normal file
View file

@ -0,0 +1,256 @@
package certcrypto
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"math/big"
"time"
"golang.org/x/crypto/ocsp"
)
// 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}
)
// KeyType represents the key algo as well as the key size or curve to use.
type KeyType string
type DERCertificateBytes []byte
// 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},
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 {
return pem.EncodeToMemory(PEMBlock(data))
}
func PEMBlock(data interface{}) *pem.Block {
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)}
case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
case DERCertificateBytes:
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(DERCertificateBytes))}
}
return 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 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)
}
// ParsePEMCertificate returns Certificate from a PEM encoded certificate.
// The certificate has to be PEM encoded. Any other encodings like DER will fail.
func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {
pemBlock, err := pemDecode(cert)
if pemBlock == nil {
return nil, err
}
// from a DER encoded certificate
return x509.ParseCertificate(pemBlock.Bytes)
}
func ExtractDomains(cert *x509.Certificate) []string {
domains := []string{cert.Subject.CommonName}
// Check for SAN certificate
for _, sanDomain := range cert.DNSNames {
if sanDomain == cert.Subject.CommonName {
continue
}
domains = append(domains, sanDomain)
}
return domains
}
func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
domains := []string{csr.Subject.CommonName}
// loop over the SubjectAltName DNS names
for _, sanName := range csr.DNSNames {
if containsSAN(domains, sanName) {
// Duplicate; skip this name
continue
}
// Name is unique
domains = append(domains, sanName)
}
return domains
}
func containsSAN(domains []string, sanName string) bool {
for _, existingName := range domains {
if existingName == sanName {
return true
}
}
return false
}
func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
derBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
}
func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]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},
ExtraExtensions: extensions,
}
return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
}

View file

@ -0,0 +1,69 @@
package certificate
import (
"time"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/log"
)
const (
// 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
)
func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) {
resc, errc := make(chan acme.Authorization), make(chan domainError)
delay := time.Second / overallRequestLimit
for _, authzURL := range order.Authorizations {
time.Sleep(delay)
go func(authzURL string) {
authz, err := c.core.Authorizations.Get(authzURL)
if err != nil {
errc <- domainError{Domain: authz.Identifier.Value, Error: err}
return
}
resc <- authz
}(authzURL)
}
var responses []acme.Authorization
failures := make(obtainError)
for i := 0; i < len(order.Authorizations); i++ {
select {
case res := <-resc:
responses = append(responses, res)
case err := <-errc:
failures[err.Domain] = err.Error
}
}
for i, auth := range order.Authorizations {
log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth)
}
close(resc)
close(errc)
// be careful to not return an empty failures map;
// even if empty, they become non-nil error values
if len(failures) > 0 {
return responses, failures
}
return responses, nil
}
func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder) {
for _, auth := range order.Authorizations {
if err := c.core.Authorizations.Deactivate(auth); err != nil {
log.Infof("Unable to deactivated authorizations: %s", auth)
}
}
}

View file

@ -0,0 +1,493 @@
package certificate
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/acme/api"
"github.com/go-acme/lego/certcrypto"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/wait"
"golang.org/x/crypto/ocsp"
"golang.org/x/net/idna"
)
// maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024
// Resource 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 Resource struct {
Domain string `json:"domain"`
CertURL string `json:"certUrl"`
CertStableURL string `json:"certStableUrl"`
PrivateKey []byte `json:"-"`
Certificate []byte `json:"-"`
IssuerCertificate []byte `json:"-"`
CSR []byte `json:"-"`
}
// ObtainRequest The request to obtain certificate.
//
// 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 the function Obtain.
// If you do not want that you can supply your own private key in the privateKey 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.
type ObtainRequest struct {
Domains []string
Bundle bool
PrivateKey crypto.PrivateKey
MustStaple bool
}
type resolver interface {
Solve(authorizations []acme.Authorization) error
}
type CertifierOptions struct {
KeyType certcrypto.KeyType
Timeout time.Duration
}
// Certifier A service to obtain/renew/revoke certificates.
type Certifier struct {
core *api.Core
resolver resolver
options CertifierOptions
}
// NewCertifier creates a Certifier.
func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier {
return &Certifier{
core: core,
resolver: resolver,
options: options,
}
}
// Obtain tries to obtain a single certificate using all domains passed into it.
//
// This function will never return a partial certificate.
// If one domain in the list fails, the whole certificate will fail.
func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
if len(request.Domains) == 0 {
return nil, errors.New("no domains to obtain a certificate for")
}
domains := sanitizeDomain(request.Domains)
if request.Bundle {
log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
} else {
log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
}
order, err := c.core.Orders.New(domains)
if err != nil {
return nil, err
}
authz, err := c.getAuthorizations(order)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order)
return nil, err
}
err = c.resolver.Solve(authz)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
return nil, err
}
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(obtainError)
cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple)
if err != nil {
for _, auth := range authz {
failures[challenge.GetTargetedDomain(auth)] = err
}
}
// Do not return an empty failures map, because
// it would still be a non-nil error value
if len(failures) > 0 {
return cert, failures
}
return cert, nil
}
// ObtainForCSR 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 *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Resource, error) {
// figure out what domains it concerns
// start with the common name
domains := certcrypto.ExtractDomainsCSR(&csr)
if bundle {
log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
} else {
log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
}
order, err := c.core.Orders.New(domains)
if err != nil {
return nil, err
}
authz, err := c.getAuthorizations(order)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c.deactivateAuthorizations(order)
return nil, err
}
err = c.resolver.Solve(authz)
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
return nil, err
}
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(obtainError)
cert, err := c.getForCSR(domains, order, bundle, csr.Raw, nil)
if err != nil {
for _, auth := range authz {
failures[challenge.GetTargetedDomain(auth)] = err
}
}
if cert != nil {
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = certcrypto.PEMEncode(&csr)
}
// Do not return an empty failures map,
// because it would still be a non-nil error value
if len(failures) > 0 {
return cert, failures
}
return cert, nil
}
func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool) (*Resource, error) {
if privateKey == nil {
var err error
privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType)
if err != nil {
return nil, err
}
}
// Determine certificate name(s) based on the authorization resources
commonName := domains[0]
// ACME draft Section 7.4 "Applying for Certificate Issuance"
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
// says:
// Clients SHOULD NOT make any assumptions about the sort order of
// "identifiers" or "authorizations" elements in the returned order
// object.
san := []string{commonName}
for _, auth := range order.Identifiers {
if auth.Value != commonName {
san = append(san, auth.Value)
}
}
// TODO: should the CSR be customizable?
csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple)
if err != nil {
return nil, err
}
return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey))
}
func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr []byte, privateKeyPem []byte) (*Resource, error) {
respOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr)
if err != nil {
return nil, err
}
commonName := domains[0]
certRes := &Resource{
Domain: commonName,
CertURL: respOrder.Certificate,
PrivateKey: privateKeyPem,
}
if respOrder.Status == acme.StatusValid {
// if the certificate is available right away, short cut!
ok, errR := c.checkResponse(respOrder, certRes, bundle)
if errR != nil {
return nil, errR
}
if ok {
return certRes, nil
}
}
timeout := c.options.Timeout
if c.options.Timeout <= 0 {
timeout = 30 * time.Second
}
err = wait.For("certificate", timeout, timeout/60, func() (bool, error) {
ord, errW := c.core.Orders.Get(order.Location)
if errW != nil {
return false, errW
}
done, errW := c.checkResponse(ord, certRes, bundle)
if errW != nil {
return false, errW
}
return done, nil
})
return certRes, err
}
// checkResponse checks to see if the certificate is ready and a link is contained in the response.
//
// If so, loads it into certRes and returns true.
// If the cert is not yet ready, it returns false.
//
// 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 *Certifier) checkResponse(order acme.Order, certRes *Resource, bundle bool) (bool, error) {
valid, err := checkOrderStatus(order)
if err != nil || !valid {
return valid, err
}
cert, issuer, err := c.core.Certificates.Get(order.Certificate, bundle)
if err != nil {
return false, err
}
log.Infof("[%s] Server responded with a certificate.", certRes.Domain)
certRes.IssuerCertificate = issuer
certRes.Certificate = cert
certRes.CertURL = order.Certificate
certRes.CertStableURL = order.Certificate
return true, nil
}
// Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func (c *Certifier) Revoke(cert []byte) error {
certificates, err := certcrypto.ParsePEMBundle(cert)
if err != nil {
return err
}
x509Cert := certificates[0]
if x509Cert.IsCA {
return fmt.Errorf("certificate bundle starts with a CA certificate")
}
revokeMsg := acme.RevokeCertMessage{
Certificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw),
}
return c.core.Certificates.Revoke(revokeMsg)
}
// Renew takes a Resource 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 Resource should be non-nil.
func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool) (*Resource, 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 := certcrypto.ParsePEMBundle(certRes.Certificate)
if err != nil {
return nil, err
}
x509Cert := certificates[0]
if x509Cert.IsCA {
return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", certRes.Domain)
}
// This is just meant to be informal for the user.
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
log.Infof("[%s] acme: Trying renewal with %d hours remaining", certRes.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(certRes.CSR) > 0 {
csr, errP := certcrypto.PemDecodeTox509CSR(certRes.CSR)
if errP != nil {
return nil, errP
}
return c.ObtainForCSR(*csr, bundle)
}
var privateKey crypto.PrivateKey
if certRes.PrivateKey != nil {
privateKey, err = certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)
if err != nil {
return nil, err
}
}
query := ObtainRequest{
Domains: certcrypto.ExtractDomains(x509Cert),
Bundle: bundle,
PrivateKey: privateKey,
MustStaple: mustStaple,
}
return c.Obtain(query)
}
// GetOCSP 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 (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) {
certificates, err := certcrypto.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, errC := c.core.HTTPClient.Get(issuedCert.IssuingCertificateURL[0])
if errC != nil {
return nil, nil, errC
}
defer resp.Body.Close()
issuerBytes, errC := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if errC != nil {
return nil, nil, errC
}
issuerCert, errC := x509.ParseCertificate(issuerBytes)
if errC != nil {
return nil, nil, errC
}
// 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
}
resp, err := c.core.HTTPClient.Post(issuedCert.OCSPServer[0], "application/ocsp-request", bytes.NewReader(ocspReq))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
ocspResBytes, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil {
return nil, nil, err
}
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
if err != nil {
return nil, nil, err
}
return ocspResBytes, ocspRes, nil
}
func checkOrderStatus(order acme.Order) (bool, error) {
switch order.Status {
case acme.StatusValid:
return true, nil
case acme.StatusInvalid:
return false, order.Error
default:
return false, nil
}
}
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.4
// The domain name MUST be encoded
// in the form in which it would appear in a certificate. That is, it
// MUST be encoded according to the rules in Section 7 of [RFC5280].
//
// https://tools.ietf.org/html/rfc5280#section-7
func sanitizeDomain(domains []string) []string {
var sanitizedDomains []string
for _, domain := range domains {
sanitizedDomain, err := idna.ToASCII(domain)
if err != nil {
log.Infof("skip domain %q: unable to sanitize (punnycode): %v", domain, err)
} else {
sanitizedDomains = append(sanitizedDomains, sanitizedDomain)
}
}
return sanitizedDomains
}

30
vendor/github.com/go-acme/lego/certificate/errors.go generated vendored Normal file
View file

@ -0,0 +1,30 @@
package certificate
import (
"bytes"
"fmt"
"sort"
)
// obtainError is returned when there are specific errors available per domain.
type obtainError map[string]error
func (e obtainError) Error() string {
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
var domains []string
for domain := range e {
domains = append(domains, domain)
}
sort.Strings(domains)
for _, domain := range domains {
buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain]))
}
return buffer.String()
}
type domainError struct {
Domain string
Error error
}

44
vendor/github.com/go-acme/lego/challenge/challenges.go generated vendored Normal file
View file

@ -0,0 +1,44 @@
package challenge
import (
"fmt"
"github.com/go-acme/lego/acme"
)
// Type is a string that identifies a particular challenge type and version of ACME challenge.
type Type string
const (
// HTTP01 is the "http-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.3
// Note: ChallengePath returns the URL path to fulfill this challenge
HTTP01 = Type("http-01")
// DNS01 is the "dns-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.4
// Note: GetRecord returns a DNS record which will fulfill this challenge
DNS01 = Type("dns-01")
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05
TLSALPN01 = Type("tls-alpn-01")
)
func (t Type) String() string {
return string(t)
}
func FindChallenge(chlgType Type, authz acme.Authorization) (acme.Challenge, error) {
for _, chlg := range authz.Challenges {
if chlg.Type == string(chlgType) {
return chlg, nil
}
}
return acme.Challenge{}, fmt.Errorf("[%s] acme: unable to find challenge %s", GetTargetedDomain(authz), chlgType)
}
func GetTargetedDomain(authz acme.Authorization) string {
if authz.Wildcard {
return "*." + authz.Identifier.Value
}
return authz.Identifier.Value
}

View file

@ -0,0 +1,16 @@
package dns01
import "github.com/miekg/dns"
// Update FQDN with CNAME if any
func updateDomainWithCName(r *dns.Msg, fqdn string) string {
for _, rr := range r.Answer {
if cn, ok := rr.(*dns.CNAME); ok {
if cn.Hdr.Name == fqdn {
return cn.Target
}
}
}
return fqdn
}

View file

@ -0,0 +1,188 @@
package dns01
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"strconv"
"time"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/acme/api"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/wait"
"github.com/miekg/dns"
)
const (
// DefaultPropagationTimeout default propagation timeout
DefaultPropagationTimeout = 60 * time.Second
// DefaultPollingInterval default polling interval
DefaultPollingInterval = 2 * time.Second
// DefaultTTL default TTL
DefaultTTL = 120
)
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
type ChallengeOption func(*Challenge) error
// CondOption Conditional challenge option.
func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
if !condition {
// NoOp options
return func(*Challenge) error {
return nil
}
}
return opt
}
// Challenge implements the dns-01 challenge
type Challenge struct {
core *api.Core
validate ValidateFunc
provider challenge.Provider
preCheck preCheck
dnsTimeout time.Duration
}
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
chlg := &Challenge{
core: core,
validate: validate,
provider: provider,
preCheck: newPreCheck(),
dnsTimeout: 10 * time.Second,
}
for _, opt := range opts {
err := opt(chlg)
if err != nil {
log.Infof("challenge option error: %v", err)
}
}
return chlg
}
// PreSolve just submits the txt record to the dns provider.
// It does not validate record propagation, or do anything at all with the acme server.
func (c *Challenge) PreSolve(authz acme.Authorization) error {
domain := challenge.GetTargetedDomain(authz)
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
if err != nil {
return err
}
if c.provider == nil {
return fmt.Errorf("[%s] acme: no DNS Provider configured", domain)
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] acme: error presenting token: %s", domain, err)
}
return nil
}
func (c *Challenge) Solve(authz acme.Authorization) error {
domain := challenge.GetTargetedDomain(authz)
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
if err != nil {
return err
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
fqdn, value := GetRecord(authz.Identifier.Value, keyAuth)
var timeout, interval time.Duration
switch provider := c.provider.(type) {
case challenge.ProviderTimeout:
timeout, interval = provider.Timeout()
default:
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
}
log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers)
err = wait.For("propagation", timeout, interval, func() (bool, error) {
stop, errP := c.preCheck.call(domain, fqdn, value)
if !stop || errP != nil {
log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
}
return stop, errP
})
if err != nil {
return err
}
chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng)
}
// CleanUp cleans the challenge.
func (c *Challenge) CleanUp(authz acme.Authorization) error {
log.Infof("[%s] acme: Cleaning DNS-01 challenge", challenge.GetTargetedDomain(authz))
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
if err != nil {
return err
}
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
}
func (c *Challenge) Sequential() (bool, time.Duration) {
if p, ok := c.provider.(sequential); ok {
return ok, p.Sequential()
}
return false, 0
}
type sequential interface {
Sequential() time.Duration
}
// GetRecord returns a DNS record which will fulfill the `dns-01` challenge
func GetRecord(domain, keyAuth string) (fqdn string, value string) {
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_EXPERIMENTAL_CNAME_SUPPORT")); ok {
r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true)
// Check if the domain has CNAME then return that
if err == nil && r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
}
}
return
}

View file

@ -0,0 +1,52 @@
package dns01
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 := GetRecord(domain, keyAuth)
authZone, err := FindZoneByFqdn(fqdn)
if err != nil {
return err
}
fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone)
fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, value)
fmt.Printf("lego: Press 'Enter' when you are done\n")
_, err = bufio.NewReader(os.Stdin).ReadBytes('\n')
return err
}
// CleanUp prints instructions for manually removing the TXT record
func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := GetRecord(domain, keyAuth)
authZone, err := FindZoneByFqdn(fqdn)
if err != nil {
return err
}
fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone)
fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, "...")
return nil
}

19
vendor/github.com/go-acme/lego/challenge/dns01/fqdn.go generated vendored Normal file
View file

@ -0,0 +1,19 @@
package dns01
// ToFqdn converts the name into a fqdn appending a trailing dot.
func ToFqdn(name string) string {
n := len(name)
if n == 0 || name[n-1] == '.' {
return name
}
return name + "."
}
// UnFqdn converts the fqdn into a name removing the trailing dot.
func UnFqdn(name string) string {
n := len(name)
if n != 0 && name[n-1] == '.' {
return name[:n-1]
}
return name
}

View file

@ -0,0 +1,232 @@
package dns01
import (
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
const defaultResolvConf = "/etc/resolv.conf"
// dnsTimeout is used to override the default DNS timeout of 10 seconds.
var dnsTimeout = 10 * time.Second
var (
fqdnToZone = map[string]string{}
muFqdnToZone sync.Mutex
)
var defaultNameservers = []string{
"google-public-dns-a.google.com:53",
"google-public-dns-b.google.com:53",
}
// recursiveNameservers are used to pre-check DNS propagation
var recursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
func ClearFqdnCache() {
muFqdnToZone.Lock()
fqdnToZone = map[string]string{}
muFqdnToZone.Unlock()
}
func AddDNSTimeout(timeout time.Duration) ChallengeOption {
return func(_ *Challenge) error {
dnsTimeout = timeout
return nil
}
}
func AddRecursiveNameservers(nameservers []string) ChallengeOption {
return func(_ *Challenge) error {
recursiveNameservers = ParseNameservers(nameservers)
return nil
}
}
// 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
}
return ParseNameservers(config.Servers)
}
func ParseNameservers(servers []string) []string {
var resolvers []string
for _, resolver := range servers {
// ensure all servers have a port number
if _, _, err := net.SplitHostPort(resolver); err != nil {
resolvers = append(resolvers, net.JoinHostPort(resolver, "53"))
} else {
resolvers = append(resolvers, resolver)
}
}
return resolvers
}
// lookupNameservers returns the authoritative nameservers for the given fqdn.
func lookupNameservers(fqdn string) ([]string, error) {
var authoritativeNss []string
zone, err := FindZoneByFqdn(fqdn)
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) (string, error) {
return FindZoneByFqdnCustom(fqdn, recursiveNameservers)
}
// FindZoneByFqdnCustom 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 FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {
muFqdnToZone.Lock()
defer muFqdnToZone.Unlock()
// Do we have it cached?
if zone, ok := fqdnToZone[fqdn]; ok {
return zone, nil
}
var err error
var in *dns.Msg
labelIndexes := dns.Split(fqdn)
for _, index := range labelIndexes {
domain := fqdn[index:]
in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)
if err != nil {
continue
}
if in == nil {
continue
}
switch in.Rcode {
case dns.RcodeSuccess:
// Check if we got a SOA RR in the answer section
if len(in.Answer) == 0 {
continue
}
// 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
}
}
case dns.RcodeNameError:
// NXDOMAIN
default:
// Any response code other than NOERROR and NXDOMAIN is treated as error
return "", fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain)
}
}
return "", fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err))
}
// 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
}
func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {
m := createDNSMsg(fqdn, rtype, recursive)
var in *dns.Msg
var err error
for _, ns := range nameservers {
in, err = sendDNSQuery(m, ns)
if err == nil && len(in.Answer) > 0 {
break
}
}
return in, err
}
func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {
m := new(dns.Msg)
m.SetQuestion(fqdn, rtype)
m.SetEdns0(4096, false)
if !recursive {
m.RecursionDesired = false
}
return m
}
func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {
udp := &dns.Client{Net: "udp", Timeout: dnsTimeout}
in, _, err := udp.Exchange(m, ns)
if in != nil && in.Truncated {
tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout}
// If the TCP request succeeds, the err will reset to nil
in, _, err = tcp.Exchange(m, ns)
}
return in, err
}
func formatDNSError(msg *dns.Msg, err error) string {
var parts []string
if msg != nil {
parts = append(parts, dns.RcodeToString[msg.Rcode])
}
if err != nil {
parts = append(parts, fmt.Sprintf("%v", err))
}
if len(parts) > 0 {
return ": " + strings.Join(parts, " ")
}
return ""
}

View file

@ -0,0 +1,127 @@
package dns01
import (
"errors"
"fmt"
"net"
"strings"
"github.com/miekg/dns"
)
// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready.
type PreCheckFunc func(fqdn, value string) (bool, error)
// WrapPreCheckFunc wraps a PreCheckFunc in order to do extra operations before or after
// the main check, put it in a loop, etc.
type WrapPreCheckFunc func(domain, fqdn, value string, check PreCheckFunc) (bool, error)
// WrapPreCheck Allow to define checks before notifying ACME that the DNS challenge is ready.
func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption {
return func(chlg *Challenge) error {
chlg.preCheck.checkFunc = wrap
return nil
}
}
// AddPreCheck Allow to define checks before notifying ACME that the DNS challenge is ready.
// Deprecated: use WrapPreCheck instead.
func AddPreCheck(preCheck PreCheckFunc) ChallengeOption {
// Prevent race condition
check := preCheck
return func(chlg *Challenge) error {
chlg.preCheck.checkFunc = func(_, fqdn, value string, _ PreCheckFunc) (bool, error) {
if check == nil {
return false, errors.New("invalid preCheck: preCheck is nil")
}
return check(fqdn, value)
}
return nil
}
}
func DisableCompletePropagationRequirement() ChallengeOption {
return func(chlg *Challenge) error {
chlg.preCheck.requireCompletePropagation = false
return nil
}
}
type preCheck struct {
// checks DNS propagation before notifying ACME that the DNS challenge is ready.
checkFunc WrapPreCheckFunc
// require the TXT record to be propagated to all authoritative name servers
requireCompletePropagation bool
}
func newPreCheck() preCheck {
return preCheck{
requireCompletePropagation: true,
}
}
func (p preCheck) call(domain, fqdn, value string) (bool, error) {
if p.checkFunc == nil {
return p.checkDNSPropagation(fqdn, value)
}
return p.checkFunc(domain, fqdn, value, p.checkDNSPropagation)
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func (p preCheck) 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 !p.requireCompletePropagation {
return true, nil
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
}
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 records []string
var found bool
for _, rr := range r.Answer {
if txt, ok := rr.(*dns.TXT); ok {
record := strings.Join(txt.Txt, "")
records = append(records, record)
if record == value {
found = true
break
}
}
}
if !found {
return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s, value: %s]: %s", ns, fqdn, value, strings.Join(records, " ,"))
}
}
return true, nil
}

View file

@ -0,0 +1,65 @@
package http01
import (
"fmt"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/acme/api"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/log"
)
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
// ChallengePath returns the URL path for the `http-01` challenge
func ChallengePath(token string) string {
return "/.well-known/acme-challenge/" + token
}
type Challenge struct {
core *api.Core
validate ValidateFunc
provider challenge.Provider
}
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
return &Challenge{
core: core,
validate: validate,
provider: provider,
}
}
func (c *Challenge) SetProvider(provider challenge.Provider) {
c.provider = provider
}
func (c *Challenge) Solve(authz acme.Authorization) error {
domain := challenge.GetTargetedDomain(authz)
log.Infof("[%s] acme: Trying to solve HTTP-01", domain)
chlng, err := challenge.FindChallenge(challenge.HTTP01, authz)
if err != nil {
return err
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] acme: error presenting token: %v", domain, err)
}
defer func() {
err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
if err != nil {
log.Warnf("[%s] acme: error cleaning up: %v", domain, err)
}
}()
chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng)
}

View file

@ -0,0 +1,96 @@
package http01
import (
"fmt"
"net"
"net/http"
"strings"
"github.com/go-acme/lego/log"
)
// ProviderServer implements ChallengeProvider for `http-01` challenge
// It may be instantiated without using the NewProviderServer function if
// you want only to use the default values.
type ProviderServer struct {
iface string
port string
done chan bool
listener net.Listener
}
// NewProviderServer creates a new ProviderServer 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 NewProviderServer(iface, port string) *ProviderServer {
return &ProviderServer{iface: iface, port: port}
}
// Present starts a web server and makes the token available at `ChallengePath(token)` for web requests.
func (s *ProviderServer) Present(domain, token, keyAuth string) error {
if s.port == "" {
s.port = "80"
}
var err error
s.listener, err = net.Listen("tcp", s.GetAddress())
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
}
func (s *ProviderServer) GetAddress() string {
return net.JoinHostPort(s.iface, s.port)
}
// CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`
func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
s.listener.Close()
<-s.done
return nil
}
func (s *ProviderServer) serve(domain, token, keyAuth string) {
path := ChallengePath(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 == http.MethodGet {
w.Header().Add("Content-Type", "text/plain")
_, err := w.Write([]byte(keyAuth))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Infof("[%s] Served key authentication", domain)
} else {
log.Warnf("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)
_, err := w.Write([]byte("TEST"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
})
httpServer := &http.Server{Handler: mux}
// Once httpServer is shut down
// we don't want any lingering connections, so disable KeepAlives.
httpServer.SetKeepAlivesEnabled(false)
err := httpServer.Serve(s.listener)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
log.Println(err)
}
s.done <- true
}

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

@ -0,0 +1,28 @@
package challenge
import "time"
// Provider 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 Provider interface {
Present(domain, token, keyAuth string) error
CleanUp(domain, token, keyAuth string) error
}
// ProviderTimeout allows for implementing a
// Provider where an unusually long timeout is required when
// waiting for an ACME challenge to be satisfied, such as when
// checking for DNS record propagation. If an implementor of a
// Provider 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 Provider.
type ProviderTimeout interface {
Provider
Timeout() (timeout, interval time.Duration)
}

View file

@ -0,0 +1,25 @@
package resolver
import (
"bytes"
"fmt"
"sort"
)
// obtainError is returned when there are specific errors available per domain.
type obtainError map[string]error
func (e obtainError) Error() string {
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
var domains []string
for domain := range e {
domains = append(domains, domain)
}
sort.Strings(domains)
for _, domain := range domains {
buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain]))
}
return buffer.String()
}

View file

@ -0,0 +1,173 @@
package resolver
import (
"fmt"
"time"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/log"
)
// Interface for all challenge solvers to implement.
type solver interface {
Solve(authorization acme.Authorization) error
}
// Interface for challenges like dns, where we can set a record in advance for ALL challenges.
// This saves quite a bit of time vs creating the records and solving them serially.
type preSolver interface {
PreSolve(authorization acme.Authorization) error
}
// Interface for challenges like dns, where we can solve all the challenges before to delete them.
type cleanup interface {
CleanUp(authorization acme.Authorization) error
}
type sequential interface {
Sequential() (bool, time.Duration)
}
// an authz with the solver we have chosen and the index of the challenge associated with it
type selectedAuthSolver struct {
authz acme.Authorization
solver solver
}
type Prober struct {
solverManager *SolverManager
}
func NewProber(solverManager *SolverManager) *Prober {
return &Prober{
solverManager: solverManager,
}
}
// Solve Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns.
func (p *Prober) Solve(authorizations []acme.Authorization) error {
failures := make(obtainError)
var authSolvers []*selectedAuthSolver
var authSolversSequential []*selectedAuthSolver
// Loop through the resources, basically through the domains.
// First pass just selects a solver for each authz.
for _, authz := range authorizations {
domain := challenge.GetTargetedDomain(authz)
if authz.Status == acme.StatusValid {
// Boulder might recycle recent validated authz (see issue #267)
log.Infof("[%s] acme: authorization already valid; skipping challenge", domain)
continue
}
if solvr := p.solverManager.chooseSolver(authz); solvr != nil {
authSolver := &selectedAuthSolver{authz: authz, solver: solvr}
switch s := solvr.(type) {
case sequential:
if ok, _ := s.Sequential(); ok {
authSolversSequential = append(authSolversSequential, authSolver)
} else {
authSolvers = append(authSolvers, authSolver)
}
default:
authSolvers = append(authSolvers, authSolver)
}
} else {
failures[domain] = fmt.Errorf("[%s] acme: could not determine solvers", domain)
}
}
parallelSolve(authSolvers, failures)
sequentialSolve(authSolversSequential, failures)
// Be careful not to return an empty failures map,
// for even an empty obtainError is a non-nil error value
if len(failures) > 0 {
return failures
}
return nil
}
func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
for i, authSolver := range authSolvers {
// Submit the challenge
domain := challenge.GetTargetedDomain(authSolver.authz)
if solvr, ok := authSolver.solver.(preSolver); ok {
err := solvr.PreSolve(authSolver.authz)
if err != nil {
failures[domain] = err
cleanUp(authSolver.solver, authSolver.authz)
continue
}
}
// Solve challenge
err := authSolver.solver.Solve(authSolver.authz)
if err != nil {
failures[domain] = err
cleanUp(authSolver.solver, authSolver.authz)
continue
}
// Clean challenge
cleanUp(authSolver.solver, authSolver.authz)
if len(authSolvers)-1 > i {
solvr := authSolver.solver.(sequential)
_, interval := solvr.Sequential()
log.Infof("sequence: wait for %s", interval)
time.Sleep(interval)
}
}
}
func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {
// For all valid preSolvers, first submit the challenges so they have max time to propagate
for _, authSolver := range authSolvers {
authz := authSolver.authz
if solvr, ok := authSolver.solver.(preSolver); ok {
err := solvr.PreSolve(authz)
if err != nil {
failures[challenge.GetTargetedDomain(authz)] = err
}
}
}
defer func() {
// Clean all created TXT records
for _, authSolver := range authSolvers {
cleanUp(authSolver.solver, authSolver.authz)
}
}()
// Finally solve all challenges for real
for _, authSolver := range authSolvers {
authz := authSolver.authz
domain := challenge.GetTargetedDomain(authz)
if failures[domain] != nil {
// already failed in previous loop
continue
}
err := authSolver.solver.Solve(authz)
if err != nil {
failures[domain] = err
}
}
}
func cleanUp(solvr solver, authz acme.Authorization) {
if solvr, ok := solvr.(cleanup); ok {
domain := challenge.GetTargetedDomain(authz)
err := solvr.CleanUp(authz)
if err != nil {
log.Warnf("[%s] acme: error cleaning up: %v ", domain, err)
}
}
}

View file

@ -0,0 +1,169 @@
package resolver
import (
"context"
"errors"
"fmt"
"sort"
"strconv"
"time"
"github.com/cenkalti/backoff"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/acme/api"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/challenge/http01"
"github.com/go-acme/lego/challenge/tlsalpn01"
"github.com/go-acme/lego/log"
)
type byType []acme.Challenge
func (a byType) Len() int { return len(a) }
func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byType) Less(i, j int) bool { return a[i].Type > a[j].Type }
type SolverManager struct {
core *api.Core
solvers map[challenge.Type]solver
}
func NewSolversManager(core *api.Core) *SolverManager {
return &SolverManager{
solvers: map[challenge.Type]solver{},
core: core,
}
}
// SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge.
func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error {
c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p)
return nil
}
// SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge.
func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error {
c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p)
return nil
}
// SetDNS01Provider specifies a custom provider p that can solve the given DNS-01 challenge.
func (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.ChallengeOption) error {
c.solvers[challenge.DNS01] = dns01.NewChallenge(c.core, validate, p, opts...)
return nil
}
// Remove Remove a challenge type from the available solvers.
func (c *SolverManager) Remove(chlgType challenge.Type) {
delete(c.solvers, chlgType)
}
// Checks all challenges from the server in order and returns the first matching solver.
func (c *SolverManager) chooseSolver(authz acme.Authorization) solver {
// Allow to have a deterministic challenge order
sort.Sort(byType(authz.Challenges))
domain := challenge.GetTargetedDomain(authz)
for _, chlg := range authz.Challenges {
if solvr, ok := c.solvers[challenge.Type(chlg.Type)]; ok {
log.Infof("[%s] acme: use %s solver", domain, chlg.Type)
return solvr
}
log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type)
}
return nil
}
func validate(core *api.Core, domain string, chlg acme.Challenge) error {
chlng, err := core.Challenges.New(chlg.URL)
if err != nil {
return fmt.Errorf("failed to initiate challenge: %v", err)
}
valid, err := checkChallengeStatus(chlng)
if err != nil {
return err
}
if valid {
log.Infof("[%s] The server validated our request", domain)
return nil
}
ra, err := strconv.Atoi(chlng.RetryAfter)
if err != nil {
// The ACME server MUST return a Retry-After.
// If it doesn't, we'll just poll hard.
// Boulder does not implement the ability to retry challenges or the Retry-After header.
// https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82
ra = 5
}
initialInterval := time.Duration(ra) * time.Second
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = initialInterval
bo.MaxInterval = 10 * initialInterval
bo.MaxElapsedTime = 100 * initialInterval
ctx, cancel := context.WithCancel(context.Background())
// After the path is sent, the ACME server will access our server.
// Repeatedly check the server for an updated status on our request.
operation := func() error {
authz, err := core.Authorizations.Get(chlng.AuthorizationURL)
if err != nil {
cancel()
return err
}
valid, err := checkAuthorizationStatus(authz)
if err != nil {
cancel()
return err
}
if valid {
log.Infof("[%s] The server validated our request", domain)
return nil
}
return errors.New("the server didn't respond to our request")
}
return backoff.Retry(operation, backoff.WithContext(bo, ctx))
}
func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {
switch chlng.Status {
case acme.StatusValid:
return true, nil
case acme.StatusPending, acme.StatusProcessing:
return false, nil
case acme.StatusInvalid:
return false, chlng.Error
default:
return false, errors.New("the server returned an unexpected state")
}
}
func checkAuthorizationStatus(authz acme.Authorization) (bool, error) {
switch authz.Status {
case acme.StatusValid:
return true, nil
case acme.StatusPending, acme.StatusProcessing:
return false, nil
case acme.StatusDeactivated, acme.StatusExpired, acme.StatusRevoked:
return false, fmt.Errorf("the authorization state %s", authz.Status)
case acme.StatusInvalid:
for _, chlg := range authz.Challenges {
if chlg.Status == acme.StatusInvalid && chlg.Error != nil {
return false, chlg.Error
}
}
return false, fmt.Errorf("the authorization state %s", authz.Status)
default:
return false, errors.New("the server returned an unexpected state")
}
}

View file

@ -0,0 +1,129 @@
package tlsalpn01
import (
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"github.com/go-acme/lego/acme"
"github.com/go-acme/lego/acme/api"
"github.com/go-acme/lego/certcrypto"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/log"
)
// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1
var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
type Challenge struct {
core *api.Core
validate ValidateFunc
provider challenge.Provider
}
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge {
return &Challenge{
core: core,
validate: validate,
provider: provider,
}
}
func (c *Challenge) SetProvider(provider challenge.Provider) {
c.provider = provider
}
// Solve manages the provider to validate and solve the challenge.
func (c *Challenge) Solve(authz acme.Authorization) error {
domain := authz.Identifier.Value
log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", challenge.GetTargetedDomain(authz))
chlng, err := challenge.FindChallenge(challenge.TLSALPN01, authz)
if err != nil {
return err
}
// Generate the Key Authorization for the challenge
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
if err != nil {
return err
}
err = c.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] acme: error presenting token: %v", challenge.GetTargetedDomain(authz), err)
}
defer func() {
err := c.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Warnf("[%s] acme: error cleaning up: %v", challenge.GetTargetedDomain(authz), err)
}
}()
chlng.KeyAuthorization = keyAuth
return c.validate(c.core, domain, chlng)
}
// ChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func ChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {
// Compute the SHA-256 digest of the key authorization.
zBytes := sha256.Sum256([]byte(keyAuth))
value, err := asn1.Marshal(zBytes[:sha256.Size])
if err != nil {
return nil, nil, err
}
// Add the keyAuth digest as the acmeValidation-v1 extension
// (marked as critical such that it won't be used by non-ACME software).
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3
extensions := []pkix.Extension{
{
Id: idPeAcmeIdentifierV1,
Critical: true,
Value: value,
},
}
// Generate a new RSA key for the certificates.
tempPrivateKey, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048)
if err != nil {
return nil, nil, err
}
rsaPrivateKey := tempPrivateKey.(*rsa.PrivateKey)
// Generate the PEM certificate using the provided private key, domain, and extra extensions.
tempCertPEM, err := certcrypto.GeneratePemCert(rsaPrivateKey, domain, extensions)
if err != nil {
return nil, nil, err
}
// Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair.
rsaPrivatePEM := certcrypto.PEMEncode(rsaPrivateKey)
return tempCertPEM, rsaPrivatePEM, nil
}
// ChallengeCert returns a certificate with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func ChallengeCert(domain, keyAuth string) (*tls.Certificate, error) {
tempCertPEM, rsaPrivatePEM, err := ChallengeBlocks(domain, keyAuth)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(tempCertPEM, rsaPrivatePEM)
if err != nil {
return nil, err
}
return &cert, nil
}

View file

@ -0,0 +1,95 @@
package tlsalpn01
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"github.com/go-acme/lego/log"
)
const (
// ACMETLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol.
ACMETLS1Protocol = "acme-tls/1"
// defaultTLSPort is the port that the ProviderServer will default to
// when no other port is provided.
defaultTLSPort = "443"
)
// ProviderServer implements ChallengeProvider for `TLS-ALPN-01` challenge.
// It may be instantiated without using the NewProviderServer
// if you want only to use the default values.
type ProviderServer struct {
iface string
port string
listener net.Listener
}
// NewProviderServer creates a new ProviderServer 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 NewProviderServer(iface, port string) *ProviderServer {
return &ProviderServer{iface: iface, port: port}
}
func (s *ProviderServer) GetAddress() string {
return net.JoinHostPort(s.iface, s.port)
}
// Present generates a certificate with a SHA-256 digest of the keyAuth provided
// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN spec.
func (s *ProviderServer) Present(domain, token, keyAuth string) error {
if s.port == "" {
// Fallback to port 443 if the port was not provided.
s.port = defaultTLSPort
}
// Generate the challenge certificate using the provided keyAuth and domain.
cert, err := ChallengeCert(domain, keyAuth)
if err != nil {
return err
}
// Place the generated certificate with the extension into the TLS config
// so that it can serve the correct details.
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{*cert}
// We must set that the `acme-tls/1` application level protocol is supported
// so that the protocol negotiation can succeed. Reference:
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2
tlsConf.NextProtos = []string{ACMETLS1Protocol}
// Create the listener with the created tls.Config.
s.listener, err = tls.Listen("tcp", s.GetAddress(), tlsConf)
if err != nil {
return fmt.Errorf("could not start HTTPS server for challenge -> %v", err)
}
// Shut the server down when we're finished.
go func() {
err := http.Serve(s.listener, nil)
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
log.Println(err)
}
}()
return nil
}
// CleanUp closes the HTTPS server.
func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {
if s.listener == nil {
return nil
}
// Server was created, close it.
if err := s.listener.Close(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}

74
vendor/github.com/go-acme/lego/lego/client.go generated vendored Normal file
View file

@ -0,0 +1,74 @@
package lego
import (
"errors"
"net/url"
"github.com/go-acme/lego/acme/api"
"github.com/go-acme/lego/certificate"
"github.com/go-acme/lego/challenge/resolver"
"github.com/go-acme/lego/registration"
)
// Client is the user-friendly way to ACME
type Client struct {
Certificate *certificate.Certifier
Challenge *resolver.SolverManager
Registration *registration.Registrar
core *api.Core
}
// 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 constants) will be generated when requesting a new certificate if one isn't provided.
func NewClient(config *Config) (*Client, error) {
if config == nil {
return nil, errors.New("a configuration must be provided")
}
_, err := url.Parse(config.CADirURL)
if err != nil {
return nil, err
}
if config.HTTPClient == nil {
return nil, errors.New("the HTTP client cannot be nil")
}
privateKey := config.User.GetPrivateKey()
if privateKey == nil {
return nil, errors.New("private key was nil")
}
var kid string
if reg := config.User.GetRegistration(); reg != nil {
kid = reg.URI
}
core, err := api.New(config.HTTPClient, config.UserAgent, config.CADirURL, kid, privateKey)
if err != nil {
return nil, err
}
solversManager := resolver.NewSolversManager(core)
prober := resolver.NewProber(solversManager)
certifier := certificate.NewCertifier(core, prober, certificate.CertifierOptions{KeyType: config.Certificate.KeyType, Timeout: config.Certificate.Timeout})
return &Client{
Certificate: certifier,
Challenge: solversManager,
Registration: registration.NewRegistrar(core, config.User),
core: core,
}, nil
}
// GetToSURL returns the current ToS URL from the Directory
func (c *Client) GetToSURL() string {
return c.core.GetDirectory().Meta.TermsOfService
}
// GetExternalAccountRequired returns the External Account Binding requirement of the Directory
func (c *Client) GetExternalAccountRequired() bool {
return c.core.GetDirectory().Meta.ExternalAccountRequired
}

104
vendor/github.com/go-acme/lego/lego/client_config.go generated vendored Normal file
View file

@ -0,0 +1,104 @@
package lego
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"time"
"github.com/go-acme/lego/certcrypto"
"github.com/go-acme/lego/registration"
)
const (
// caCertificatesEnvVar is the environment variable name that can be used to
// specify the path to PEM encoded CA Certificates that can be used to
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list.
caCertificatesEnvVar = "LEGO_CA_CERTIFICATES"
// caServerNameEnvVar is the environment variable name that can be used to
// specify the CA server name that can be used to
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list.
caServerNameEnvVar = "LEGO_CA_SERVER_NAME"
// LEDirectoryProduction URL to the Let's Encrypt production
LEDirectoryProduction = "https://acme-v02.api.letsencrypt.org/directory"
// LEDirectoryStaging URL to the Let's Encrypt staging
LEDirectoryStaging = "https://acme-staging-v02.api.letsencrypt.org/directory"
)
type Config struct {
CADirURL string
User registration.User
UserAgent string
HTTPClient *http.Client
Certificate CertificateConfig
}
func NewConfig(user registration.User) *Config {
return &Config{
CADirURL: LEDirectoryProduction,
User: user,
HTTPClient: createDefaultHTTPClient(),
Certificate: CertificateConfig{
KeyType: certcrypto.RSA2048,
Timeout: 30 * time.Second,
},
}
}
type CertificateConfig struct {
KeyType certcrypto.KeyType
Timeout time.Duration
}
// createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value
// and potentially a custom *x509.CertPool
// based on the caCertificatesEnvVar environment variable (see the `initCertPool` function)
func createDefaultHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
ServerName: os.Getenv(caServerNameEnvVar),
RootCAs: initCertPool(),
},
},
}
}
// initCertPool creates a *x509.CertPool populated with the PEM certificates
// found in the filepath specified in the caCertificatesEnvVar OS environment
// variable. If the caCertificatesEnvVar is not set then initCertPool will
// return nil. If there is an error creating a *x509.CertPool from the provided
// caCertificatesEnvVar value then initCertPool will panic.
func initCertPool() *x509.CertPool {
if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" {
customCAs, err := ioutil.ReadFile(customCACertsPath)
if err != nil {
panic(fmt.Sprintf("error reading %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
}
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(customCAs); !ok {
panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
}
return certPool
}
return nil
}

59
vendor/github.com/go-acme/lego/log/logger.go generated vendored Normal file
View file

@ -0,0 +1,59 @@
package log
import (
"log"
"os"
)
// Logger is an optional custom logger.
var Logger StdLogger = log.New(os.Stdout, "", log.LstdFlags)
// StdLogger interface for Standard Logger.
type StdLogger interface {
Fatal(args ...interface{})
Fatalln(args ...interface{})
Fatalf(format string, args ...interface{})
Print(args ...interface{})
Println(args ...interface{})
Printf(format string, args ...interface{})
}
// Fatal writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatal(args ...interface{}) {
Logger.Fatal(args...)
}
// Fatalf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatalf(format string, args ...interface{}) {
Logger.Fatalf(format, args...)
}
// Print writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Print(args ...interface{}) {
Logger.Print(args...)
}
// Println writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Println(args ...interface{}) {
Logger.Println(args...)
}
// Printf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Printf(format string, args ...interface{}) {
Logger.Printf(format, args...)
}
// Warnf writes a log entry.
func Warnf(format string, args ...interface{}) {
Printf("[WARN] "+format, args...)
}
// Infof writes a log entry.
func Infof(format string, args ...interface{}) {
Printf("[INFO] "+format, args...)
}

View file

@ -0,0 +1,163 @@
package env
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/log"
)
// Get environment variables
func Get(names ...string) (map[string]string, error) {
values := map[string]string{}
var missingEnvVars []string
for _, envVar := range names {
value := GetOrFile(envVar)
if value == "" {
missingEnvVars = append(missingEnvVars, envVar)
}
values[envVar] = value
}
if len(missingEnvVars) > 0 {
return nil, fmt.Errorf("some credentials information are missing: %s", strings.Join(missingEnvVars, ","))
}
return values, nil
}
// GetWithFallback Get environment variable values
// The first name in each group is use as key in the result map
//
// // LEGO_ONE="ONE"
// // LEGO_TWO="TWO"
// env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
// // => "LEGO_ONE" = "ONE"
//
// ----
//
// // LEGO_ONE=""
// // LEGO_TWO="TWO"
// env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
// // => "LEGO_ONE" = "TWO"
//
// ----
//
// // LEGO_ONE=""
// // LEGO_TWO=""
// env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
// // => error
//
func GetWithFallback(groups ...[]string) (map[string]string, error) {
values := map[string]string{}
var missingEnvVars []string
for _, names := range groups {
if len(names) == 0 {
return nil, errors.New("undefined environment variable names")
}
value, envVar := getOneWithFallback(names[0], names[1:]...)
if len(value) == 0 {
missingEnvVars = append(missingEnvVars, envVar)
continue
}
values[envVar] = value
}
if len(missingEnvVars) > 0 {
return nil, fmt.Errorf("some credentials information are missing: %s", strings.Join(missingEnvVars, ","))
}
return values, nil
}
func getOneWithFallback(main string, names ...string) (string, string) {
value := GetOrFile(main)
if len(value) > 0 {
return value, main
}
for _, name := range names {
value := GetOrFile(name)
if len(value) > 0 {
return value, main
}
}
return "", main
}
// GetOrDefaultInt returns the given environment variable value as an integer.
// Returns the default if the envvar cannot be coopered to an int, or is not found.
func GetOrDefaultInt(envVar string, defaultValue int) int {
v, err := strconv.Atoi(GetOrFile(envVar))
if err != nil {
return defaultValue
}
return v
}
// GetOrDefaultSecond returns the given environment variable value as an time.Duration (second).
// Returns the default if the envvar cannot be coopered to an int, or is not found.
func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration {
v := GetOrDefaultInt(envVar, -1)
if v < 0 {
return defaultValue
}
return time.Duration(v) * time.Second
}
// GetOrDefaultString returns the given environment variable value as a string.
// Returns the default if the envvar cannot be find.
func GetOrDefaultString(envVar string, defaultValue string) string {
v := GetOrFile(envVar)
if len(v) == 0 {
return defaultValue
}
return v
}
// GetOrDefaultBool returns the given environment variable value as a boolean.
// Returns the default if the envvar cannot be coopered to a boolean, or is not found.
func GetOrDefaultBool(envVar string, defaultValue bool) bool {
v, err := strconv.ParseBool(GetOrFile(envVar))
if err != nil {
return defaultValue
}
return v
}
// GetOrFile Attempts to resolve 'key' as an environment variable.
// Failing that, it will check to see if '<key>_FILE' exists.
// If so, it will attempt to read from the referenced file to populate a value.
func GetOrFile(envVar string) string {
envVarValue := os.Getenv(envVar)
if envVarValue != "" {
return envVarValue
}
fileVar := envVar + "_FILE"
fileVarValue := os.Getenv(fileVar)
if fileVarValue == "" {
return envVarValue
}
fileContents, err := ioutil.ReadFile(fileVarValue)
if err != nil {
log.Printf("Failed to read the file %s (defined by env var %s): %s", fileVarValue, fileVar, err)
return ""
}
return string(fileContents)
}

33
vendor/github.com/go-acme/lego/platform/wait/wait.go generated vendored Normal file
View file

@ -0,0 +1,33 @@
package wait
import (
"fmt"
"time"
"github.com/go-acme/lego/log"
)
// For polls the given function 'f', once every 'interval', up to 'timeout'.
func For(msg string, timeout, interval time.Duration, f func() (bool, error)) error {
log.Infof("Wait for %s [timeout: %s, interval: %s]", msg, timeout, interval)
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

@ -0,0 +1,162 @@
// Package acmedns implements a DNS provider for solving DNS-01 challenges using Joohoi's acme-dns project.
// For more information see the ACME-DNS homepage: https://github.com/joohoi/acme-dns
package acmedns
import (
"errors"
"fmt"
"github.com/cpu/goacmedns"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const (
// envNamespace is the prefix for ACME-DNS environment variables.
envNamespace = "ACME_DNS_"
// apiBaseEnvVar is the environment variable name for the ACME-DNS API address
// (e.g. https://acmedns.your-domain.com).
apiBaseEnvVar = envNamespace + "API_BASE"
// storagePathEnvVar is the environment variable name for the ACME-DNS JSON account data file.
// A per-domain account will be registered/persisted to this file and used for TXT updates.
storagePathEnvVar = envNamespace + "STORAGE_PATH"
)
// acmeDNSClient is an interface describing the goacmedns.Client functions the DNSProvider uses.
// It makes it easier for tests to shim a mock Client into the DNSProvider.
type acmeDNSClient interface {
// UpdateTXTRecord updates the provided account's TXT record
// to the given value or returns an error.
UpdateTXTRecord(goacmedns.Account, string) error
// RegisterAccount registers and returns a new account
// with the given allowFrom restriction or returns an error.
RegisterAccount([]string) (goacmedns.Account, error)
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface for
// an ACME-DNS server.
type DNSProvider struct {
client acmeDNSClient
storage goacmedns.Storage
}
// NewDNSProvider creates an ACME-DNS provider using file based account storage.
// Its configuration is loaded from the environment by reading apiBaseEnvVar and storagePathEnvVar.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(apiBaseEnvVar, storagePathEnvVar)
if err != nil {
return nil, fmt.Errorf("acme-dns: %v", err)
}
client := goacmedns.NewClient(values[apiBaseEnvVar])
storage := goacmedns.NewFileStorage(values[storagePathEnvVar], 0600)
return NewDNSProviderClient(client, storage)
}
// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and goacmedns.Storage.
func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNSProvider, error) {
if client == nil {
return nil, errors.New("ACME-DNS Client must be not nil")
}
if storage == nil {
return nil, errors.New("ACME-DNS Storage must be not nil")
}
return &DNSProvider{
client: client,
storage: storage,
}, nil
}
// ErrCNAMERequired is returned by Present when the Domain indicated had no
// existing ACME-DNS account in the Storage and additional setup is required.
// The user must create a CNAME in the DNS zone for Domain that aliases FQDN
// to Target in order to complete setup for the ACME-DNS account that was created.
type ErrCNAMERequired struct {
// The Domain that is being issued for.
Domain string
// The alias of the CNAME (left hand DNS label).
FQDN string
// The RDATA of the CNAME (right hand side, canonical name).
Target string
}
// Error returns a descriptive message for the ErrCNAMERequired instance telling
// the user that a CNAME needs to be added to the DNS zone of c.Domain before
// the ACME-DNS hook will work. The CNAME to be created should be of the form:
// {{ c.FQDN }} CNAME {{ c.Target }}
func (e ErrCNAMERequired) Error() string {
return fmt.Sprintf("acme-dns: new account created for %q. "+
"To complete setup for %q you must provision the following "+
"CNAME in your DNS zone and re-run this provider when it is "+
"in place:\n"+
"%s CNAME %s.",
e.Domain, e.Domain, e.FQDN, e.Target)
}
// Present creates a TXT record to fulfill the DNS-01 challenge.
// If there is an existing account for the domain in the provider's storage
// then it will be used to set the challenge response TXT record with the ACME-DNS server and issuance will continue.
// If there is not an account for the given domain present in the DNSProvider storage
// one will be created and registered with the ACME DNS server and an ErrCNAMERequired error is returned.
// This will halt issuance and indicate to the user that a one-time manual setup is required for the domain.
func (d *DNSProvider) Present(domain, _, keyAuth string) error {
// Compute the challenge response FQDN and TXT value for the domain based
// on the keyAuth.
fqdn, value := dns01.GetRecord(domain, keyAuth)
// Check if credentials were previously saved for this domain.
account, err := d.storage.Fetch(domain)
// Errors other than goacmeDNS.ErrDomainNotFound are unexpected.
if err != nil && err != goacmedns.ErrDomainNotFound {
return err
}
if err == goacmedns.ErrDomainNotFound {
// The account did not exist. Create a new one and return an error
// indicating the required one-time manual CNAME setup.
return d.register(domain, fqdn)
}
// Update the acme-dns TXT record.
return d.client.UpdateTXTRecord(account, value)
}
// CleanUp removes the record matching the specified parameters. It is not
// implemented for the ACME-DNS provider.
func (d *DNSProvider) CleanUp(_, _, _ string) error {
// ACME-DNS doesn't support the notion of removing a record.
// For users of ACME-DNS it is expected the stale records remain in-place.
return nil
}
// register creates a new ACME-DNS account for the given domain.
// If account creation works as expected a ErrCNAMERequired error is returned describing
// the one-time manual CNAME setup required to complete setup of the ACME-DNS hook for the domain.
// If any other error occurs it is returned as-is.
func (d *DNSProvider) register(domain, fqdn string) error {
// TODO(@cpu): Read CIDR whitelists from the environment
newAcct, err := d.client.RegisterAccount(nil)
if err != nil {
return err
}
// Store the new account in the storage and call save to persist the data.
err = d.storage.Put(domain, newAcct)
if err != nil {
return err
}
err = d.storage.Save()
if err != nil {
return err
}
// Stop issuance by returning an error.
// The user needs to perform a manual one-time CNAME setup in their DNS zone
// to complete the setup of the new account we created.
return ErrCNAMERequired{
Domain: domain,
FQDN: fqdn,
Target: newAcct.FullDomain,
}
}

View file

@ -0,0 +1,220 @@
// Package alidns implements a DNS provider for solving the DNS-01 challenge using Alibaba Cloud DNS.
package alidns
import (
"errors"
"fmt"
"strings"
"time"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const defaultRegionID = "cn-hangzhou"
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
SecretKey string
RegionID string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("ALICLOUD_TTL", 600),
PropagationTimeout: env.GetOrDefaultSecond("ALICLOUD_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("ALICLOUD_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond("ALICLOUD_HTTP_TIMEOUT", 10*time.Second),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
client *alidns.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Alibaba Cloud DNS.
// Credentials must be passed in the environment variables: ALICLOUD_ACCESS_KEY and ALICLOUD_SECRET_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("ALICLOUD_ACCESS_KEY", "ALICLOUD_SECRET_KEY")
if err != nil {
return nil, fmt.Errorf("alicloud: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["ALICLOUD_ACCESS_KEY"]
config.SecretKey = values["ALICLOUD_SECRET_KEY"]
config.RegionID = env.GetOrFile("ALICLOUD_REGION_ID")
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for alidns.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("alicloud: the configuration of the DNS provider is nil")
}
if config.APIKey == "" || config.SecretKey == "" {
return nil, fmt.Errorf("alicloud: credentials missing")
}
if len(config.RegionID) == 0 {
config.RegionID = defaultRegionID
}
conf := sdk.NewConfig().WithTimeout(config.HTTPTimeout)
credential := credentials.NewAccessKeyCredential(config.APIKey, config.SecretKey)
client, err := alidns.NewClientWithOptions(config.RegionID, conf, credential)
if err != nil {
return nil, fmt.Errorf("alicloud: credentials failed: %v", err)
}
return &DNSProvider{config: config, client: client}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
_, zoneName, err := d.getHostedZone(domain)
if err != nil {
return fmt.Errorf("alicloud: %v", err)
}
recordAttributes := d.newTxtRecord(zoneName, fqdn, value)
_, err = d.client.AddDomainRecord(recordAttributes)
if err != nil {
return fmt.Errorf("alicloud: API call failed: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
records, err := d.findTxtRecords(domain, fqdn)
if err != nil {
return fmt.Errorf("alicloud: %v", err)
}
_, _, err = d.getHostedZone(domain)
if err != nil {
return fmt.Errorf("alicloud: %v", err)
}
for _, rec := range records {
request := alidns.CreateDeleteDomainRecordRequest()
request.RecordId = rec.RecordId
_, err = d.client.DeleteDomainRecord(request)
if err != nil {
return fmt.Errorf("alicloud: %v", err)
}
}
return nil
}
func (d *DNSProvider) getHostedZone(domain string) (string, string, error) {
request := alidns.CreateDescribeDomainsRequest()
var domains []alidns.Domain
startPage := 1
for {
request.PageNumber = requests.NewInteger(startPage)
response, err := d.client.DescribeDomains(request)
if err != nil {
return "", "", fmt.Errorf("API call failed: %v", err)
}
domains = append(domains, response.Domains.Domain...)
if response.PageNumber*response.PageSize >= response.TotalCount {
break
}
startPage++
}
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return "", "", err
}
var hostedZone alidns.Domain
for _, zone := range domains {
if zone.DomainName == dns01.UnFqdn(authZone) {
hostedZone = zone
}
}
if hostedZone.DomainId == "" {
return "", "", fmt.Errorf("zone %s not found in AliDNS for domain %s", authZone, domain)
}
return fmt.Sprintf("%v", hostedZone.DomainId), hostedZone.DomainName, nil
}
func (d *DNSProvider) newTxtRecord(zone, fqdn, value string) *alidns.AddDomainRecordRequest {
request := alidns.CreateAddDomainRecordRequest()
request.Type = "TXT"
request.DomainName = zone
request.RR = d.extractRecordName(fqdn, zone)
request.Value = value
request.TTL = requests.NewInteger(d.config.TTL)
return request
}
func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]alidns.Record, error) {
_, zoneName, err := d.getHostedZone(domain)
if err != nil {
return nil, err
}
request := alidns.CreateDescribeDomainRecordsRequest()
request.DomainName = zoneName
request.PageSize = requests.NewInteger(500)
var records []alidns.Record
result, err := d.client.DescribeDomainRecords(request)
if err != nil {
return records, fmt.Errorf("API call has failed: %v", err)
}
recordName := d.extractRecordName(fqdn, zoneName)
for _, record := range result.DomainRecords.Record {
if record.RR == recordName {
records = append(records, record)
}
}
return records, nil
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
name := dns01.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}

View file

@ -0,0 +1,191 @@
// Package auroradns implements a DNS provider for solving the DNS-01 challenge using Aurora DNS.
package auroradns
import (
"errors"
"fmt"
"sync"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/nrdcg/auroradns"
)
const defaultBaseURL = "https://api.auroradns.eu"
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
UserID string
Key string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("AURORA_TTL", 300),
PropagationTimeout: env.GetOrDefaultSecond("AURORA_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("AURORA_POLLING_INTERVAL", dns01.DefaultPollingInterval),
}
}
// DNSProvider describes a provider for AuroraDNS
type DNSProvider struct {
recordIDs map[string]string
recordIDsMu sync.Mutex
config *Config
client *auroradns.Client
}
// NewDNSProvider returns a DNSProvider instance configured for AuroraDNS.
// Credentials must be passed in the environment variables:
// AURORA_USER_ID and AURORA_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("AURORA_USER_ID", "AURORA_KEY")
if err != nil {
return nil, fmt.Errorf("aurora: %v", err)
}
config := NewDefaultConfig()
config.BaseURL = env.GetOrFile("AURORA_ENDPOINT")
config.UserID = values["AURORA_USER_ID"]
config.Key = values["AURORA_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for AuroraDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("aurora: the configuration of the DNS provider is nil")
}
if config.UserID == "" || config.Key == "" {
return nil, errors.New("aurora: some credentials information are missing")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
tr, err := auroradns.NewTokenTransport(config.UserID, config.Key)
if err != nil {
return nil, fmt.Errorf("aurora: %v", err)
}
client, err := auroradns.NewClient(tr.Client(), auroradns.WithBaseURL(config.BaseURL))
if err != nil {
return nil, fmt.Errorf("aurora: %v", err)
}
return &DNSProvider{
config: config,
client: client,
recordIDs: make(map[string]string),
}, nil
}
// Present creates a record with a secret
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return fmt.Errorf("aurora: could not determine zone for domain: '%s'. %s", domain, err)
}
// 1. Aurora will happily create the TXT record when it is provided a fqdn,
// but it will only appear in the control panel and will not be
// propagated to DNS servers. Extract and use subdomain instead.
// 2. A trailing dot in the fqdn will cause Aurora to add a trailing dot to
// the subdomain, resulting in _acme-challenge..<domain> rather
// than _acme-challenge.<domain>
subdomain := fqdn[0 : len(fqdn)-len(authZone)-1]
authZone = dns01.UnFqdn(authZone)
zone, err := d.getZoneInformationByName(authZone)
if err != nil {
return fmt.Errorf("aurora: could not create record: %v", err)
}
record := auroradns.Record{
RecordType: "TXT",
Name: subdomain,
Content: value,
TTL: d.config.TTL,
}
newRecord, _, err := d.client.CreateRecord(zone.ID, record)
if err != nil {
return fmt.Errorf("aurora: could not create record: %v", err)
}
d.recordIDsMu.Lock()
d.recordIDs[fqdn] = newRecord.ID
d.recordIDsMu.Unlock()
return nil
}
// CleanUp removes a given record that was generated by Present
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[fqdn]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("unknown recordID for %q", fqdn)
}
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return fmt.Errorf("could not determine zone for domain: %q. %v", domain, err)
}
authZone = dns01.UnFqdn(authZone)
zone, err := d.getZoneInformationByName(authZone)
if err != nil {
return err
}
_, _, err = d.client.DeleteRecord(zone.ID, recordID)
if err != nil {
return err
}
d.recordIDsMu.Lock()
delete(d.recordIDs, fqdn)
d.recordIDsMu.Unlock()
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getZoneInformationByName(name string) (auroradns.Zone, error) {
zs, _, err := d.client.ListZones()
if err != nil {
return auroradns.Zone{}, err
}
for _, element := range zs {
if element.Name == name {
return element, nil
}
}
return auroradns.Zone{}, fmt.Errorf("could not find Zone record")
}

View file

@ -0,0 +1,274 @@
// Package azure implements a DNS provider for solving the DNS-01 challenge using azure DNS.
// Azure doesn't like trailing dots on domain names, most of the acme code does.
package azure
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const defaultMetadataEndpoint = "http://169.254.169.254"
// Config is used to configure the creation of the DNSProvider
type Config struct {
// optional if using instance metadata service
ClientID string
ClientSecret string
TenantID string
SubscriptionID string
ResourceGroup string
MetadataEndpoint string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("AZURE_TTL", 60),
PropagationTimeout: env.GetOrDefaultSecond("AZURE_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("AZURE_POLLING_INTERVAL", 2*time.Second),
MetadataEndpoint: env.GetOrFile("AZURE_METADATA_ENDPOINT"),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
authorizer autorest.Authorizer
}
// NewDNSProvider returns a DNSProvider instance configured for azure.
// Credentials can be passed in the environment variables:
// AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP
// If the credentials are _not_ set via the environment,
// then it will attempt to get a bearer token via the instance metadata service.
// see: https://github.com/Azure/go-autorest/blob/v10.14.0/autorest/azure/auth/auth.go#L38-L42
func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.SubscriptionID = env.GetOrFile("AZURE_SUBSCRIPTION_ID")
config.ResourceGroup = env.GetOrFile("AZURE_RESOURCE_GROUP")
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Azure.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("azure: the configuration of the DNS provider is nil")
}
if config.HTTPClient == nil {
config.HTTPClient = http.DefaultClient
}
authorizer, err := getAuthorizer(config)
if err != nil {
return nil, err
}
if config.SubscriptionID == "" {
subsID, err := getMetadata(config, "subscriptionId")
if err != nil {
return nil, fmt.Errorf("azure: %v", err)
}
if subsID == "" {
return nil, errors.New("azure: SubscriptionID is missing")
}
config.SubscriptionID = subsID
}
if config.ResourceGroup == "" {
resGroup, err := getMetadata(config, "resourceGroupName")
if err != nil {
return nil, fmt.Errorf("azure: %v", err)
}
if resGroup == "" {
return nil, errors.New("azure: ResourceGroup is missing")
}
config.ResourceGroup = resGroup
}
return &DNSProvider{config: config, authorizer: authorizer}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
ctx := context.Background()
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZoneID(ctx, fqdn)
if err != nil {
return fmt.Errorf("azure: %v", err)
}
rsc := dns.NewRecordSetsClient(d.config.SubscriptionID)
rsc.Authorizer = d.authorizer
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
// Get existing record set
rset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, relative, dns.TXT)
if err != nil {
detailedError, ok := err.(autorest.DetailedError)
if !ok || detailedError.StatusCode != http.StatusNotFound {
return fmt.Errorf("azure: %v", err)
}
}
// Construct unique TXT records using map
uniqRecords := map[string]struct{}{value: {}}
if rset.RecordSetProperties != nil && rset.TxtRecords != nil {
for _, txtRecord := range *rset.TxtRecords {
// Assume Value doesn't contain multiple strings
if txtRecord.Value != nil && len(*txtRecord.Value) > 0 {
uniqRecords[(*txtRecord.Value)[0]] = struct{}{}
}
}
}
var txtRecords []dns.TxtRecord
for txt := range uniqRecords {
txtRecords = append(txtRecords, dns.TxtRecord{Value: &[]string{txt}})
}
rec := dns.RecordSet{
Name: &relative,
RecordSetProperties: &dns.RecordSetProperties{
TTL: to.Int64Ptr(int64(d.config.TTL)),
TxtRecords: &txtRecords,
},
}
_, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, rec, "", "")
if err != nil {
return fmt.Errorf("azure: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
ctx := context.Background()
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZoneID(ctx, fqdn)
if err != nil {
return fmt.Errorf("azure: %v", err)
}
relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone))
rsc := dns.NewRecordSetsClient(d.config.SubscriptionID)
rsc.Authorizer = d.authorizer
_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, "")
if err != nil {
return fmt.Errorf("azure: %v", err)
}
return nil
}
// Checks that azure has a zone for this domain name.
func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return "", err
}
dc := dns.NewZonesClient(d.config.SubscriptionID)
dc.Authorizer = d.authorizer
zone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone))
if err != nil {
return "", err
}
// zone.Name shouldn't have a trailing dot(.)
return to.String(zone.Name), nil
}
// Returns the relative record to the domain
func toRelativeRecord(domain, zone string) string {
return dns01.UnFqdn(strings.TrimSuffix(domain, zone))
}
func getAuthorizer(config *Config) (autorest.Authorizer, error) {
if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" {
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, config.TenantID)
if err != nil {
return nil, err
}
spt, err := adal.NewServicePrincipalToken(*oauthConfig, config.ClientID, config.ClientSecret, azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return nil, err
}
spt.SetSender(config.HTTPClient)
return autorest.NewBearerAuthorizer(spt), nil
}
return auth.NewAuthorizerFromEnvironment()
}
// Fetches metadata from environment or he instance metadata service
// borrowed from https://github.com/Microsoft/azureimds/blob/master/imdssample.go
func getMetadata(config *Config, field string) (string, error) {
metadataEndpoint := config.MetadataEndpoint
if len(metadataEndpoint) == 0 {
metadataEndpoint = defaultMetadataEndpoint
}
resource := fmt.Sprintf("%s/metadata/instance/compute/%s", metadataEndpoint, field)
req, err := http.NewRequest(http.MethodGet, resource, nil)
if err != nil {
return "", err
}
req.Header.Add("Metadata", "True")
q := req.URL.Query()
q.Add("format", "text")
q.Add("api-version", "2017-12-01")
req.URL.RawQuery = q.Encode()
resp, err := config.HTTPClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(respBody), nil
}

View file

@ -0,0 +1,201 @@
// Package bluecat implements a DNS provider for solving the DNS-01 challenge using a self-hosted Bluecat Address Manager.
package bluecat
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const (
configType = "Configuration"
viewType = "View"
zoneType = "Zone"
txtType = "TXTRecord"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
UserName string
Password string
ConfigName string
DNSView string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("BLUECAT_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("BLUECAT_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("BLUECAT_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("BLUECAT_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// Bluecat's Address Manager REST API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
token string
}
// 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) {
values, err := env.Get("BLUECAT_SERVER_URL", "BLUECAT_USER_NAME", "BLUECAT_PASSWORD", "BLUECAT_CONFIG_NAME", "BLUECAT_DNS_VIEW")
if err != nil {
return nil, fmt.Errorf("bluecat: %v", err)
}
config := NewDefaultConfig()
config.BaseURL = values["BLUECAT_SERVER_URL"]
config.UserName = values["BLUECAT_USER_NAME"]
config.Password = values["BLUECAT_PASSWORD"]
config.ConfigName = values["BLUECAT_CONFIG_NAME"]
config.DNSView = values["BLUECAT_DNS_VIEW"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("bluecat: the configuration of the DNS provider is nil")
}
if config.BaseURL == "" || config.UserName == "" || config.Password == "" || config.ConfigName == "" || config.DNSView == "" {
return nil, fmt.Errorf("bluecat: credentials missing")
}
return &DNSProvider{config: config}, 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 := dns01.GetRecord(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewID, err := d.lookupViewID(d.config.DNSView)
if err != nil {
return err
}
parentZoneID, name, err := d.lookupParentZoneID(viewID, fqdn)
if err != nil {
return err
}
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|", d.config.TTL, fqdn, value),
}
resp, err := d.sendRequest(http.MethodPost, "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: addEntity request failed: %s", addTxtResp)
}
err = d.deploy(parentZoneID)
if err != nil {
return err
}
return d.logout()
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
err := d.login()
if err != nil {
return err
}
viewID, err := d.lookupViewID(d.config.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(http.MethodGet, "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 fmt.Errorf("bluecat: %v", err)
}
queryArgs = map[string]string{
"objectId": strconv.FormatUint(uint64(txtRec.ID), 10),
}
resp, err = d.sendRequest(http.MethodDelete, http.MethodDelete, nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
err = d.deploy(parentID)
if err != nil {
return err
}
return d.logout()
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,249 @@
package bluecat
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
)
// 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"`
}
type entityResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Properties string `json:"properties"`
}
// 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.config.UserName,
"password": d.config.Password,
}
resp, err := d.sendRequest(http.MethodGet, "login", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
authBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("bluecat: %v", err)
}
authResp := string(authBytes)
if strings.Contains(authResp, "Authentication Error") {
msg := strings.Trim(authResp, "\"")
return fmt.Errorf("bluecat: request failed: %s", msg)
}
// Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username"
d.token = regexp.MustCompile("BAMAuthToken: [^ ]+").FindString(authResp)
return nil
}
// Destroys Bluecat Session
func (d *DNSProvider) logout() error {
if len(d.token) == 0 {
// nothing to do
return nil
}
resp, err := d.sendRequest(http.MethodGet, "logout", nil, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("bluecat: request failed to delete session with HTTP status code %d", resp.StatusCode)
}
authBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
authResp := string(authBytes)
if !strings.Contains(authResp, "successfully") {
msg := strings.Trim(authResp, "\"")
return fmt.Errorf("bluecat: 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.config.ConfigName,
"type": configType,
}
resp, err := d.sendRequest(http.MethodGet, "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, fmt.Errorf("bluecat: %v", 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": viewName,
"type": viewType,
}
resp, err := d.sendRequest(http.MethodGet, "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, fmt.Errorf("bluecat: %v", 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(http.MethodGet, "getEntityByName", nil, queryArgs)
// Return an empty zone if the named zone doesn't exist
if resp != nil && resp.StatusCode == http.StatusNotFound {
return 0, fmt.Errorf("bluecat: 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, fmt.Errorf("bluecat: %v", err)
}
return zone.ID, 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(http.MethodPost, "quickDeploy", nil, queryArgs)
if err != nil {
return err
}
defer resp.Body.Close()
return 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/Services/REST/v1/%s", d.config.BaseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("bluecat: %v", err)
}
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("bluecat: %v", 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.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("bluecat: %v", err)
}
if resp.StatusCode >= 400 {
errBytes, _ := ioutil.ReadAll(resp.Body)
errResp := string(errBytes)
return nil, fmt.Errorf("bluecat: request failed with HTTP status code %d\n Full message: %s",
resp.StatusCode, errResp)
}
return resp, nil
}

View file

@ -0,0 +1,157 @@
// Package cloudflare implements a DNS provider for solving the DNS-01 challenge using cloudflare DNS.
package cloudflare
import (
"errors"
"fmt"
"net/http"
"time"
cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
)
const (
minTTL = 120
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
AuthEmail string
AuthKey string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("CLOUDFLARE_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("CLOUDFLARE_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("CLOUDFLARE_POLLING_INTERVAL", 2*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("CLOUDFLARE_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
client *cloudflare.API
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for Cloudflare.
// Credentials must be passed in the environment variables:
// CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.GetWithFallback(
[]string{"CLOUDFLARE_EMAIL", "CF_API_EMAIL"},
[]string{"CLOUDFLARE_API_KEY", "CF_API_KEY"})
if err != nil {
return nil, fmt.Errorf("cloudflare: %v", err)
}
config := NewDefaultConfig()
config.AuthEmail = values["CLOUDFLARE_EMAIL"]
config.AuthKey = values["CLOUDFLARE_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Cloudflare.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("cloudflare: the configuration of the DNS provider is nil")
}
if config.TTL < minTTL {
return nil, fmt.Errorf("cloudflare: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
client, err := cloudflare.New(config.AuthKey, config.AuthEmail, cloudflare.HTTPClient(config.HTTPClient))
if err != nil {
return nil, err
}
return &DNSProvider{client: client, config: config}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("cloudflare: %v", err)
}
zoneID, err := d.client.ZoneIDByName(dns01.UnFqdn(authZone))
if err != nil {
return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err)
}
dnsRecord := cloudflare.DNSRecord{
Type: "TXT",
Name: dns01.UnFqdn(fqdn),
Content: value,
TTL: d.config.TTL,
}
response, err := d.client.CreateDNSRecord(zoneID, dnsRecord)
if err != nil {
return fmt.Errorf("cloudflare: failed to create TXT record: %v", err)
}
if !response.Success {
return fmt.Errorf("cloudflare: failed to create TXT record: %+v %+v", response.Errors, response.Messages)
}
log.Infof("cloudflare: new record for %s, ID %s", domain, response.Result.ID)
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("cloudflare: %v", err)
}
zoneID, err := d.client.ZoneIDByName(dns01.UnFqdn(authZone))
if err != nil {
return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err)
}
dnsRecord := cloudflare.DNSRecord{
Type: "TXT",
Name: dns01.UnFqdn(fqdn),
}
records, err := d.client.DNSRecords(zoneID, dnsRecord)
if err != nil {
return fmt.Errorf("cloudflare: failed to find TXT records: %v", err)
}
for _, record := range records {
err = d.client.DeleteDNSRecord(zoneID, record.ID)
if err != nil {
log.Printf("cloudflare: failed to delete TXT record: %v", err)
}
}
return nil
}

View file

@ -0,0 +1,108 @@
// Package cloudns implements a DNS provider for solving the DNS-01 challenge using ClouDNS DNS.
package cloudns
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/go-acme/lego/providers/dns/cloudns/internal"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
AuthID string
AuthPassword string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("CLOUDNS_PROPAGATION_TIMEOUT", 120*time.Second),
PollingInterval: env.GetOrDefaultSecond("CLOUDNS_POLLING_INTERVAL", 4*time.Second),
TTL: env.GetOrDefaultInt("CLOUDNS_TTL", dns01.DefaultTTL),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("CLOUDNS_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
client *internal.Client
}
// NewDNSProvider returns a DNSProvider instance configured for ClouDNS.
// Credentials must be passed in the environment variables:
// CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("CLOUDNS_AUTH_ID", "CLOUDNS_AUTH_PASSWORD")
if err != nil {
return nil, fmt.Errorf("ClouDNS: %v", err)
}
config := NewDefaultConfig()
config.AuthID = values["CLOUDNS_AUTH_ID"]
config.AuthPassword = values["CLOUDNS_AUTH_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for ClouDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ClouDNS: the configuration of the DNS provider is nil")
}
client, err := internal.NewClient(config.AuthID, config.AuthPassword)
if err != nil {
return nil, err
}
client.HTTPClient = config.HTTPClient
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.client.GetZone(fqdn)
if err != nil {
return err
}
return d.client.AddTxtRecord(zone.Name, fqdn, value, d.config.TTL)
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := d.client.GetZone(fqdn)
if err != nil {
return err
}
record, err := d.client.FindTxtRecord(zone.Name, fqdn)
if err != nil {
return err
}
return d.client.RemoveTxtRecord(record.ID, zone.Name)
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,209 @@
package internal
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/go-acme/lego/challenge/dns01"
)
const defaultBaseURL = "https://api.cloudns.net/dns/"
type Zone struct {
Name string
Type string
Zone string
Status string // is an integer, but cast as string
}
// TXTRecord a TXT record
type TXTRecord struct {
ID int `json:"id,string"`
Type string `json:"type"`
Host string `json:"host"`
Record string `json:"record"`
Failover int `json:"failover,string"`
TTL int `json:"ttl,string"`
Status int `json:"status"`
}
type TXTRecords map[string]TXTRecord
// NewClient creates a ClouDNS client
func NewClient(authID string, authPassword string) (*Client, error) {
if authID == "" {
return nil, fmt.Errorf("ClouDNS: credentials missing: authID")
}
if authPassword == "" {
return nil, fmt.Errorf("ClouDNS: credentials missing: authPassword")
}
baseURL, err := url.Parse(defaultBaseURL)
if err != nil {
return nil, err
}
return &Client{
authID: authID,
authPassword: authPassword,
HTTPClient: &http.Client{},
BaseURL: baseURL,
}, nil
}
// Client ClouDNS client
type Client struct {
authID string
authPassword string
HTTPClient *http.Client
BaseURL *url.URL
}
// GetZone Get domain name information for a FQDN
func (c *Client) GetZone(authFQDN string) (*Zone, error) {
authZone, err := dns01.FindZoneByFqdn(authFQDN)
if err != nil {
return nil, err
}
authZoneName := dns01.UnFqdn(authZone)
reqURL := *c.BaseURL
reqURL.Path += "get-zone-info.json"
q := reqURL.Query()
q.Add("domain-name", authZoneName)
reqURL.RawQuery = q.Encode()
result, err := c.doRequest(http.MethodGet, &reqURL)
if err != nil {
return nil, err
}
var zone Zone
if len(result) > 0 {
if err = json.Unmarshal(result, &zone); err != nil {
return nil, fmt.Errorf("ClouDNS: zone unmarshaling error: %v", err)
}
}
if zone.Name == authZoneName {
return &zone, nil
}
return nil, fmt.Errorf("ClouDNS: zone %s not found for authFQDN %s", authZoneName, authFQDN)
}
// FindTxtRecord return the TXT record a zone ID and a FQDN
func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
reqURL := *c.BaseURL
reqURL.Path += "records.json"
q := reqURL.Query()
q.Add("domain-name", zoneName)
q.Add("host", host)
q.Add("type", "TXT")
reqURL.RawQuery = q.Encode()
result, err := c.doRequest(http.MethodGet, &reqURL)
if err != nil {
return nil, err
}
var records TXTRecords
if err = json.Unmarshal(result, &records); err != nil {
return nil, fmt.Errorf("ClouDNS: TXT record unmarshaling error: %v", err)
}
for _, record := range records {
if record.Host == host && record.Type == "TXT" {
return &record, nil
}
}
return nil, fmt.Errorf("ClouDNS: no existing record found for %q", fqdn)
}
// AddTxtRecord add a TXT record
func (c *Client) AddTxtRecord(zoneName string, fqdn, value string, ttl int) error {
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
reqURL := *c.BaseURL
reqURL.Path += "add-record.json"
q := reqURL.Query()
q.Add("domain-name", zoneName)
q.Add("host", host)
q.Add("record", value)
q.Add("ttl", strconv.Itoa(ttl))
q.Add("record-type", "TXT")
reqURL.RawQuery = q.Encode()
_, err := c.doRequest(http.MethodPost, &reqURL)
return err
}
// RemoveTxtRecord remove a TXT record
func (c *Client) RemoveTxtRecord(recordID int, zoneName string) error {
reqURL := *c.BaseURL
reqURL.Path += "delete-record.json"
q := reqURL.Query()
q.Add("domain-name", zoneName)
q.Add("record-id", strconv.Itoa(recordID))
reqURL.RawQuery = q.Encode()
_, err := c.doRequest(http.MethodPost, &reqURL)
return err
}
func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error) {
req, err := c.buildRequest(method, url)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("ClouDNS: %v", err)
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("ClouDNS: %s", toUnreadableBodyMessage(req, content))
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("ClouDNS: invalid code (%v), error: %s", resp.StatusCode, content)
}
return content, nil
}
func (c *Client) buildRequest(method string, url *url.URL) (*http.Request, error) {
q := url.Query()
q.Add("auth-id", c.authID)
q.Add("auth-password", c.authPassword)
url.RawQuery = q.Encode()
req, err := http.NewRequest(method, url.String(), nil)
if err != nil {
return nil, fmt.Errorf("ClouDNS: invalid request: %v", err)
}
return req, nil
}
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
}

View file

@ -0,0 +1,108 @@
// Package cloudxns implements a DNS provider for solving the DNS-01 challenge using CloudXNS DNS.
package cloudxns
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/go-acme/lego/providers/dns/cloudxns/internal"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
SecretKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("CLOUDXNS_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("CLOUDXNS_POLLING_INTERVAL", dns01.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("CLOUDXNS_TTL", dns01.DefaultTTL),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("CLOUDXNS_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
client *internal.Client
}
// NewDNSProvider returns a DNSProvider instance configured for CloudXNS.
// Credentials must be passed in the environment variables:
// CLOUDXNS_API_KEY and CLOUDXNS_SECRET_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("CLOUDXNS_API_KEY", "CLOUDXNS_SECRET_KEY")
if err != nil {
return nil, fmt.Errorf("CloudXNS: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["CLOUDXNS_API_KEY"]
config.SecretKey = values["CLOUDXNS_SECRET_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for CloudXNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("CloudXNS: the configuration of the DNS provider is nil")
}
client, err := internal.NewClient(config.APIKey, config.SecretKey)
if err != nil {
return nil, err
}
client.HTTPClient = config.HTTPClient
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
info, err := d.client.GetDomainInformation(fqdn)
if err != nil {
return err
}
return d.client.AddTxtRecord(info, fqdn, value, d.config.TTL)
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
info, err := d.client.GetDomainInformation(fqdn)
if err != nil {
return err
}
record, err := d.client.FindTxtRecord(info.ID, fqdn)
if err != nil {
return err
}
return d.client.RemoveTxtRecord(record.RecordID, info.ID)
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,208 @@
package internal
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
)
const defaultBaseURL = "https://www.cloudxns.net/api2/"
type apiResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data,omitempty"`
}
// Data Domain information
type Data struct {
ID string `json:"id"`
Domain string `json:"domain"`
TTL int `json:"ttl,omitempty"`
}
// TXTRecord a TXT record
type TXTRecord struct {
ID int `json:"domain_id,omitempty"`
RecordID string `json:"record_id,omitempty"`
Host string `json:"host"`
Value string `json:"value"`
Type string `json:"type"`
LineID int `json:"line_id,string"`
TTL int `json:"ttl,string"`
}
// NewClient creates a CloudXNS client
func NewClient(apiKey string, secretKey string) (*Client, error) {
if apiKey == "" {
return nil, fmt.Errorf("CloudXNS: credentials missing: apiKey")
}
if secretKey == "" {
return nil, fmt.Errorf("CloudXNS: credentials missing: secretKey")
}
return &Client{
apiKey: apiKey,
secretKey: secretKey,
HTTPClient: &http.Client{},
BaseURL: defaultBaseURL,
}, nil
}
// Client CloudXNS client
type Client struct {
apiKey string
secretKey string
HTTPClient *http.Client
BaseURL string
}
// GetDomainInformation Get domain name information for a FQDN
func (c *Client) GetDomainInformation(fqdn string) (*Data, error) {
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return nil, err
}
result, err := c.doRequest(http.MethodGet, "domain", nil)
if err != nil {
return nil, err
}
var domains []Data
if len(result) > 0 {
err = json.Unmarshal(result, &domains)
if err != nil {
return nil, fmt.Errorf("CloudXNS: domains unmarshaling error: %v", err)
}
}
for _, data := range domains {
if data.Domain == authZone {
return &data, nil
}
}
return nil, fmt.Errorf("CloudXNS: zone %s not found for domain %s", authZone, fqdn)
}
// FindTxtRecord return the TXT record a zone ID and a FQDN
func (c *Client) FindTxtRecord(zoneID, fqdn string) (*TXTRecord, error) {
result, err := c.doRequest(http.MethodGet, fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil)
if err != nil {
return nil, err
}
var records []TXTRecord
err = json.Unmarshal(result, &records)
if err != nil {
return nil, fmt.Errorf("CloudXNS: TXT record unmarshaling error: %v", err)
}
for _, record := range records {
if record.Host == dns01.UnFqdn(fqdn) && record.Type == "TXT" {
return &record, nil
}
}
return nil, fmt.Errorf("CloudXNS: no existing record found for %q", fqdn)
}
// AddTxtRecord add a TXT record
func (c *Client) AddTxtRecord(info *Data, fqdn, value string, ttl int) error {
id, err := strconv.Atoi(info.ID)
if err != nil {
return fmt.Errorf("CloudXNS: invalid zone ID: %v", err)
}
payload := TXTRecord{
ID: id,
Host: dns01.UnFqdn(strings.TrimSuffix(fqdn, info.Domain)),
Value: value,
Type: "TXT",
LineID: 1,
TTL: ttl,
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("CloudXNS: record unmarshaling error: %v", err)
}
_, err = c.doRequest(http.MethodPost, "record", body)
return err
}
// RemoveTxtRecord remove a TXT record
func (c *Client) RemoveTxtRecord(recordID, zoneID string) error {
_, err := c.doRequest(http.MethodDelete, fmt.Sprintf("record/%s/%s", recordID, zoneID), nil)
return err
}
func (c *Client) doRequest(method, uri string, body []byte) (json.RawMessage, error) {
req, err := c.buildRequest(method, uri, body)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("CloudXNS: %v", err)
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("CloudXNS: %s", toUnreadableBodyMessage(req, content))
}
var r apiResponse
err = json.Unmarshal(content, &r)
if err != nil {
return nil, fmt.Errorf("CloudXNS: response unmashaling error: %v: %s", err, toUnreadableBodyMessage(req, content))
}
if r.Code != 1 {
return nil, fmt.Errorf("CloudXNS: invalid code (%v), error: %s", r.Code, r.Message)
}
return r.Data, nil
}
func (c *Client) buildRequest(method, uri string, body []byte) (*http.Request, error) {
url := c.BaseURL + uri
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("CloudXNS: invalid request: %v", err)
}
requestDate := time.Now().Format(time.RFC1123Z)
req.Header.Set("API-KEY", c.apiKey)
req.Header.Set("API-REQUEST-DATE", requestDate)
req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body)))
req.Header.Set("API-FORMAT", "json")
return req, nil
}
func (c *Client) hmac(url, date, body string) string {
sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey))
return hex.EncodeToString(sum[:])
}
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
}

View file

@ -0,0 +1,148 @@
// Package conoha implements a DNS provider for solving the DNS-01 challenge using ConoHa DNS.
package conoha
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/go-acme/lego/providers/dns/conoha/internal"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Region string
TenantID string
Username string
Password string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
Region: env.GetOrDefaultString("CONOHA_REGION", "tyo1"),
TTL: env.GetOrDefaultInt("CONOHA_TTL", 60),
PropagationTimeout: env.GetOrDefaultSecond("CONOHA_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("CONOHA_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("CONOHA_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
client *internal.Client
}
// NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS.
// Credentials must be passed in the environment variables: CONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("CONOHA_TENANT_ID", "CONOHA_API_USERNAME", "CONOHA_API_PASSWORD")
if err != nil {
return nil, fmt.Errorf("conoha: %v", err)
}
config := NewDefaultConfig()
config.TenantID = values["CONOHA_TENANT_ID"]
config.Username = values["CONOHA_API_USERNAME"]
config.Password = values["CONOHA_API_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for ConoHa DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("conoha: the configuration of the DNS provider is nil")
}
if config.TenantID == "" || config.Username == "" || config.Password == "" {
return nil, errors.New("conoha: some credentials information are missing")
}
auth := internal.Auth{
TenantID: config.TenantID,
PasswordCredentials: internal.PasswordCredentials{
Username: config.Username,
Password: config.Password,
},
}
client, err := internal.NewClient(config.Region, auth, config.HTTPClient)
if err != nil {
return nil, fmt.Errorf("conoha: failed to create client: %v", err)
}
return &DNSProvider{config: config, client: client}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return err
}
id, err := d.client.GetDomainID(authZone)
if err != nil {
return fmt.Errorf("conoha: failed to get domain ID: %v", err)
}
record := internal.Record{
Name: fqdn,
Type: "TXT",
Data: value,
TTL: d.config.TTL,
}
err = d.client.CreateRecord(id, record)
if err != nil {
return fmt.Errorf("conoha: failed to create record: %v", err)
}
return nil
}
// CleanUp clears ConoHa DNS TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return err
}
domID, err := d.client.GetDomainID(authZone)
if err != nil {
return fmt.Errorf("conoha: failed to get domain ID: %v", err)
}
recID, err := d.client.GetRecordID(domID, fqdn, "TXT", value)
if err != nil {
return fmt.Errorf("conoha: failed to get record ID: %v", err)
}
err = d.client.DeleteRecord(domID, recID)
if err != nil {
return fmt.Errorf("conoha: failed to delete record: %v", err)
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,205 @@
package internal
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
)
const (
identityBaseURL = "https://identity.%s.conoha.io"
dnsServiceBaseURL = "https://dns-service.%s.conoha.io"
)
// IdentityRequest is an authentication request body.
type IdentityRequest struct {
Auth Auth `json:"auth"`
}
// Auth is an authentication information.
type Auth struct {
TenantID string `json:"tenantId"`
PasswordCredentials PasswordCredentials `json:"passwordCredentials"`
}
// PasswordCredentials is API-user's credentials.
type PasswordCredentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
// IdentityResponse is an authentication response body.
type IdentityResponse struct {
Access Access `json:"access"`
}
// Access is an identity information.
type Access struct {
Token Token `json:"token"`
}
// Token is an api access token.
type Token struct {
ID string `json:"id"`
}
// DomainListResponse is a response of a domain listing request.
type DomainListResponse struct {
Domains []Domain `json:"domains"`
}
// Domain is a hosted domain entry.
type Domain struct {
ID string `json:"id"`
Name string `json:"name"`
}
// RecordListResponse is a response of record listing request.
type RecordListResponse struct {
Records []Record `json:"records"`
}
// Record is a record entry.
type Record struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Data string `json:"data"`
TTL int `json:"ttl"`
}
// Client is a ConoHa API client.
type Client struct {
token string
endpoint string
httpClient *http.Client
}
// NewClient returns a client instance logged into the ConoHa service.
func NewClient(region string, auth Auth, httpClient *http.Client) (*Client, error) {
if httpClient == nil {
httpClient = &http.Client{}
}
c := &Client{httpClient: httpClient}
c.endpoint = fmt.Sprintf(identityBaseURL, region)
identity, err := c.getIdentity(auth)
if err != nil {
return nil, fmt.Errorf("failed to login: %v", err)
}
c.token = identity.Access.Token.ID
c.endpoint = fmt.Sprintf(dnsServiceBaseURL, region)
return c, nil
}
func (c *Client) getIdentity(auth Auth) (*IdentityResponse, error) {
req := &IdentityRequest{Auth: auth}
identity := &IdentityResponse{}
err := c.do(http.MethodPost, "/v2.0/tokens", req, identity)
if err != nil {
return nil, err
}
return identity, nil
}
// GetDomainID returns an ID of specified domain.
func (c *Client) GetDomainID(domainName string) (string, error) {
domainList := &DomainListResponse{}
err := c.do(http.MethodGet, "/v1/domains", nil, domainList)
if err != nil {
return "", err
}
for _, domain := range domainList.Domains {
if domain.Name == domainName {
return domain.ID, nil
}
}
return "", fmt.Errorf("no such domain: %s", domainName)
}
// GetRecordID returns an ID of specified record.
func (c *Client) GetRecordID(domainID, recordName, recordType, data string) (string, error) {
recordList := &RecordListResponse{}
err := c.do(http.MethodGet, fmt.Sprintf("/v1/domains/%s/records", domainID), nil, recordList)
if err != nil {
return "", err
}
for _, record := range recordList.Records {
if record.Name == recordName && record.Type == recordType && record.Data == data {
return record.ID, nil
}
}
return "", errors.New("no such record")
}
// CreateRecord adds new record.
func (c *Client) CreateRecord(domainID string, record Record) error {
return c.do(http.MethodPost, fmt.Sprintf("/v1/domains/%s/records", domainID), record, nil)
}
// DeleteRecord removes specified record.
func (c *Client) DeleteRecord(domainID, recordID string) error {
return c.do(http.MethodDelete, fmt.Sprintf("/v1/domains/%s/records/%s", domainID, recordID), nil, nil)
}
func (c *Client) do(method, path string, payload, result interface{}) error {
body := bytes.NewReader(nil)
if payload != nil {
bodyBytes, err := json.Marshal(payload)
if err != nil {
return err
}
body = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequest(method, c.endpoint+path, body)
if err != nil {
return err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Auth-Token", c.token)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
return fmt.Errorf("HTTP request failed with status code %d: %s", resp.StatusCode, string(respBody))
}
if result != nil {
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
return json.Unmarshal(respBody, result)
}
return nil
}

View file

@ -0,0 +1,249 @@
// Package designate implements a DNS provider for solving the DNS-01 challenge using the Designate DNSaaS for Openstack.
package designate
import (
"errors"
"fmt"
"log"
"os"
"sync"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
opts gophercloud.AuthOptions
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DESIGNATE_TTL", 10),
PropagationTimeout: env.GetOrDefaultSecond("DESIGNATE_PROPAGATION_TIMEOUT", 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond("DESIGNATE_POLLING_INTERVAL", 10*time.Second),
}
}
// DNSProvider describes a provider for Designate
type DNSProvider struct {
config *Config
client *gophercloud.ServiceClient
dnsEntriesMu sync.Mutex
}
// NewDNSProvider returns a DNSProvider instance configured for Designate.
// Credentials must be passed in the environment variables:
// OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, OS_REGION_NAME.
func NewDNSProvider() (*DNSProvider, error) {
_, err := env.Get("OS_AUTH_URL", "OS_USERNAME", "OS_PASSWORD", "OS_TENANT_NAME", "OS_REGION_NAME")
if err != nil {
return nil, fmt.Errorf("designate: %v", err)
}
opts, err := openstack.AuthOptionsFromEnv()
if err != nil {
return nil, fmt.Errorf("designate: %v", err)
}
config := NewDefaultConfig()
config.opts = opts
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Designate.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("designate: the configuration of the DNS provider is nil")
}
provider, err := openstack.AuthenticatedClient(config.opts)
if err != nil {
return nil, fmt.Errorf("designate: failed to authenticate: %v", err)
}
dnsClient, err := openstack.NewDNSV2(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
return nil, fmt.Errorf("designate: failed to get DNS provider: %v", err)
}
return &DNSProvider{client: dnsClient, config: config}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("designate: couldn't get zone ID in Present: %v", err)
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("designate: %v", err)
}
// use mutex to prevent race condition between creating the record and verifying it
d.dnsEntriesMu.Lock()
defer d.dnsEntriesMu.Unlock()
existingRecord, err := d.getRecord(zoneID, fqdn)
if err != nil {
return fmt.Errorf("designate: %v", err)
}
if existingRecord != nil {
if contains(existingRecord.Records, value) {
log.Printf("designate: the record already exists: %s", value)
return nil
}
return d.updateRecord(existingRecord, value)
}
err = d.createRecord(zoneID, fqdn, value)
if err != nil {
return fmt.Errorf("designate: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return err
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("designate: couldn't get zone ID in CleanUp: %v", err)
}
// use mutex to prevent race condition between getting the record and deleting it
d.dnsEntriesMu.Lock()
defer d.dnsEntriesMu.Unlock()
record, err := d.getRecord(zoneID, fqdn)
if err != nil {
return fmt.Errorf("designate: couldn't get Record ID in CleanUp: %v", err)
}
if record == nil {
// Record is already deleted
return nil
}
err = recordsets.Delete(d.client, zoneID, record.ID).ExtractErr()
if err != nil {
return fmt.Errorf("designate: error for %s in CleanUp: %v", fqdn, err)
}
return nil
}
func contains(values []string, value string) bool {
for _, v := range values {
if v == value {
return true
}
}
return false
}
func (d *DNSProvider) createRecord(zoneID, fqdn, value string) error {
createOpts := recordsets.CreateOpts{
Name: fqdn,
Type: "TXT",
TTL: d.config.TTL,
Description: "ACME verification record",
Records: []string{value},
}
actual, err := recordsets.Create(d.client, zoneID, createOpts).Extract()
if err != nil {
return fmt.Errorf("error for %s in Present while creating record: %v", fqdn, err)
}
if actual.Name != fqdn || actual.TTL != d.config.TTL {
return fmt.Errorf("the created record doesn't match what we wanted to create")
}
return nil
}
func (d *DNSProvider) updateRecord(record *recordsets.RecordSet, value string) error {
if contains(record.Records, value) {
log.Printf("skip: the record already exists: %s", value)
return nil
}
values := append([]string{value}, record.Records...)
updateOpts := recordsets.UpdateOpts{
Description: &record.Description,
TTL: record.TTL,
Records: values,
}
result := recordsets.Update(d.client, record.ZoneID, record.ID, updateOpts)
return result.Err
}
func (d *DNSProvider) getZoneID(wanted string) (string, error) {
allPages, err := zones.List(d.client, nil).AllPages()
if err != nil {
return "", err
}
allZones, err := zones.ExtractZones(allPages)
if err != nil {
return "", err
}
for _, zone := range allZones {
if zone.Name == wanted {
return zone.ID, nil
}
}
return "", fmt.Errorf("zone id not found for %s", wanted)
}
func (d *DNSProvider) getRecord(zoneID string, wanted string) (*recordsets.RecordSet, error) {
allPages, err := recordsets.ListByZone(d.client, zoneID, nil).AllPages()
if err != nil {
return nil, err
}
allRecords, err := recordsets.ExtractRecordSets(allPages)
if err != nil {
return nil, err
}
for _, record := range allRecords {
if record.Name == wanted {
return &record, nil
}
}
return nil, nil
}

View file

@ -0,0 +1,132 @@
package digitalocean
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/go-acme/lego/challenge/dns01"
)
const defaultBaseURL = "https://api.digitalocean.com"
// txtRecordResponse represents a response from DO's API after making a TXT record
type txtRecordResponse struct {
DomainRecord record `json:"domain_record"`
}
type record struct {
ID int `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Data string `json:"data,omitempty"`
TTL int `json:"ttl,omitempty"`
}
type apiError struct {
ID string `json:"id"`
Message string `json:"message"`
}
func (d *DNSProvider) removeTxtRecord(domain string, recordID int) error {
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
}
reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", d.config.BaseURL, dns01.UnFqdn(authZone), recordID)
req, err := d.newRequest(http.MethodDelete, reqURL, nil)
if err != nil {
return err
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return readError(req, resp)
}
return nil
}
func (d *DNSProvider) addTxtRecord(domain, fqdn, value string) (*txtRecordResponse, error) {
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return nil, fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
}
reqData := record{Type: "TXT", Name: fqdn, Data: value, TTL: d.config.TTL}
body, err := json.Marshal(reqData)
if err != nil {
return nil, err
}
reqURL := fmt.Sprintf("%s/v2/domains/%s/records", d.config.BaseURL, dns01.UnFqdn(authZone))
req, err := d.newRequest(http.MethodPost, reqURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, readError(req, resp)
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.New(toUnreadableBodyMessage(req, content))
}
// Everything looks good; but we'll need the ID later to delete the record
respData := &txtRecordResponse{}
err = json.Unmarshal(content, respData)
if err != nil {
return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(req, content))
}
return respData, nil
}
func (d *DNSProvider) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, reqURL, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.config.AuthToken))
return req, nil
}
func readError(req *http.Request, resp *http.Response) error {
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.New(toUnreadableBodyMessage(req, content))
}
var errInfo apiError
err = json.Unmarshal(content, &errInfo)
if err != nil {
return fmt.Errorf("apiError unmarshaling error: %v: %s", err, toUnreadableBodyMessage(req, content))
}
return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
}
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
}

View file

@ -0,0 +1,126 @@
// Package digitalocean implements a DNS provider for solving the DNS-01 challenge using digitalocean DNS.
package digitalocean
import (
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
AuthToken string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
BaseURL: defaultBaseURL,
TTL: env.GetOrDefaultInt("DO_TTL", 30),
PropagationTimeout: env.GetOrDefaultSecond("DO_PROPAGATION_TIMEOUT", 60*time.Second),
PollingInterval: env.GetOrDefaultSecond("DO_POLLING_INTERVAL", 5*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DO_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
// that uses DigitalOcean's REST API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
recordIDs map[string]int
recordIDsMu sync.Mutex
}
// NewDNSProvider returns a DNSProvider instance configured for Digital
// Ocean. Credentials must be passed in the environment variable:
// DO_AUTH_TOKEN.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DO_AUTH_TOKEN")
if err != nil {
return nil, fmt.Errorf("digitalocean: %v", err)
}
config := NewDefaultConfig()
config.AuthToken = values["DO_AUTH_TOKEN"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Digital Ocean.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("digitalocean: the configuration of the DNS provider is nil")
}
if config.AuthToken == "" {
return nil, fmt.Errorf("digitalocean: credentials missing")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
return &DNSProvider{
config: config,
recordIDs: make(map[string]int),
}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
respData, err := d.addTxtRecord(domain, fqdn, value)
if err != nil {
return fmt.Errorf("digitalocean: %v", err)
}
d.recordIDsMu.Lock()
d.recordIDs[fqdn] = respData.DomainRecord.ID
d.recordIDsMu.Unlock()
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
// get the record's unique ID from when we created it
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[fqdn]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("digitalocean: unknown record ID for '%s'", fqdn)
}
err := d.removeTxtRecord(domain, recordID)
if err != nil {
return fmt.Errorf("digitalocean: %v", err)
}
// Delete record ID from map
d.recordIDsMu.Lock()
delete(d.recordIDs, fqdn)
d.recordIDsMu.Unlock()
return nil
}

View file

@ -0,0 +1,177 @@
package dns
import (
"fmt"
"github.com/go-acme/lego/challenge"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/providers/dns/acmedns"
"github.com/go-acme/lego/providers/dns/alidns"
"github.com/go-acme/lego/providers/dns/auroradns"
"github.com/go-acme/lego/providers/dns/azure"
"github.com/go-acme/lego/providers/dns/bluecat"
"github.com/go-acme/lego/providers/dns/cloudflare"
"github.com/go-acme/lego/providers/dns/cloudns"
"github.com/go-acme/lego/providers/dns/cloudxns"
"github.com/go-acme/lego/providers/dns/conoha"
"github.com/go-acme/lego/providers/dns/designate"
"github.com/go-acme/lego/providers/dns/digitalocean"
"github.com/go-acme/lego/providers/dns/dnsimple"
"github.com/go-acme/lego/providers/dns/dnsmadeeasy"
"github.com/go-acme/lego/providers/dns/dnspod"
"github.com/go-acme/lego/providers/dns/dreamhost"
"github.com/go-acme/lego/providers/dns/duckdns"
"github.com/go-acme/lego/providers/dns/dyn"
"github.com/go-acme/lego/providers/dns/exec"
"github.com/go-acme/lego/providers/dns/exoscale"
"github.com/go-acme/lego/providers/dns/fastdns"
"github.com/go-acme/lego/providers/dns/gandi"
"github.com/go-acme/lego/providers/dns/gandiv5"
"github.com/go-acme/lego/providers/dns/gcloud"
"github.com/go-acme/lego/providers/dns/glesys"
"github.com/go-acme/lego/providers/dns/godaddy"
"github.com/go-acme/lego/providers/dns/hostingde"
"github.com/go-acme/lego/providers/dns/httpreq"
"github.com/go-acme/lego/providers/dns/iij"
"github.com/go-acme/lego/providers/dns/inwx"
"github.com/go-acme/lego/providers/dns/lightsail"
"github.com/go-acme/lego/providers/dns/linode"
"github.com/go-acme/lego/providers/dns/linodev4"
"github.com/go-acme/lego/providers/dns/mydnsjp"
"github.com/go-acme/lego/providers/dns/namecheap"
"github.com/go-acme/lego/providers/dns/namedotcom"
"github.com/go-acme/lego/providers/dns/netcup"
"github.com/go-acme/lego/providers/dns/nifcloud"
"github.com/go-acme/lego/providers/dns/ns1"
"github.com/go-acme/lego/providers/dns/oraclecloud"
"github.com/go-acme/lego/providers/dns/otc"
"github.com/go-acme/lego/providers/dns/ovh"
"github.com/go-acme/lego/providers/dns/pdns"
"github.com/go-acme/lego/providers/dns/rackspace"
"github.com/go-acme/lego/providers/dns/rfc2136"
"github.com/go-acme/lego/providers/dns/route53"
"github.com/go-acme/lego/providers/dns/sakuracloud"
"github.com/go-acme/lego/providers/dns/selectel"
"github.com/go-acme/lego/providers/dns/stackpath"
"github.com/go-acme/lego/providers/dns/transip"
"github.com/go-acme/lego/providers/dns/vegadns"
"github.com/go-acme/lego/providers/dns/vscale"
"github.com/go-acme/lego/providers/dns/vultr"
"github.com/go-acme/lego/providers/dns/zoneee"
)
// NewDNSChallengeProviderByName Factory for DNS providers
func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
switch name {
case "acme-dns":
return acmedns.NewDNSProvider()
case "alidns":
return alidns.NewDNSProvider()
case "azure":
return azure.NewDNSProvider()
case "auroradns":
return auroradns.NewDNSProvider()
case "bluecat":
return bluecat.NewDNSProvider()
case "cloudflare":
return cloudflare.NewDNSProvider()
case "cloudns":
return cloudns.NewDNSProvider()
case "cloudxns":
return cloudxns.NewDNSProvider()
case "conoha":
return conoha.NewDNSProvider()
case "designate":
return designate.NewDNSProvider()
case "digitalocean":
return digitalocean.NewDNSProvider()
case "dnsimple":
return dnsimple.NewDNSProvider()
case "dnsmadeeasy":
return dnsmadeeasy.NewDNSProvider()
case "dnspod":
return dnspod.NewDNSProvider()
case "dreamhost":
return dreamhost.NewDNSProvider()
case "duckdns":
return duckdns.NewDNSProvider()
case "dyn":
return dyn.NewDNSProvider()
case "fastdns":
return fastdns.NewDNSProvider()
case "exec":
return exec.NewDNSProvider()
case "exoscale":
return exoscale.NewDNSProvider()
case "gandi":
return gandi.NewDNSProvider()
case "gandiv5":
return gandiv5.NewDNSProvider()
case "glesys":
return glesys.NewDNSProvider()
case "gcloud":
return gcloud.NewDNSProvider()
case "godaddy":
return godaddy.NewDNSProvider()
case "hostingde":
return hostingde.NewDNSProvider()
case "httpreq":
return httpreq.NewDNSProvider()
case "iij":
return iij.NewDNSProvider()
case "inwx":
return inwx.NewDNSProvider()
case "lightsail":
return lightsail.NewDNSProvider()
case "linode":
return linode.NewDNSProvider()
case "linodev4":
return linodev4.NewDNSProvider()
case "manual":
return dns01.NewDNSProviderManual()
case "mydnsjp":
return mydnsjp.NewDNSProvider()
case "namecheap":
return namecheap.NewDNSProvider()
case "namedotcom":
return namedotcom.NewDNSProvider()
case "netcup":
return netcup.NewDNSProvider()
case "nifcloud":
return nifcloud.NewDNSProvider()
case "ns1":
return ns1.NewDNSProvider()
case "oraclecloud":
return oraclecloud.NewDNSProvider()
case "otc":
return otc.NewDNSProvider()
case "ovh":
return ovh.NewDNSProvider()
case "pdns":
return pdns.NewDNSProvider()
case "rackspace":
return rackspace.NewDNSProvider()
case "route53":
return route53.NewDNSProvider()
case "rfc2136":
return rfc2136.NewDNSProvider()
case "sakuracloud":
return sakuracloud.NewDNSProvider()
case "stackpath":
return stackpath.NewDNSProvider()
case "selectel":
return selectel.NewDNSProvider()
case "transip":
return transip.NewDNSProvider()
case "vegadns":
return vegadns.NewDNSProvider()
case "vultr":
return vultr.NewDNSProvider()
case "vscale":
return vscale.NewDNSProvider()
case "zoneee":
return zoneee.NewDNSProvider()
default:
return nil, fmt.Errorf("unrecognised DNS provider: %s", name)
}
}

View file

@ -0,0 +1,211 @@
// Package dnsimple implements a DNS provider for solving the DNS-01 challenge using dnsimple DNS.
package dnsimple
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/dnsimple/dnsimple-go/dnsimple"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"golang.org/x/oauth2"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
AccessToken string
BaseURL string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DNSIMPLE_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("DNSIMPLE_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DNSIMPLE_POLLING_INTERVAL", dns01.DefaultPollingInterval),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *dnsimple.Client
}
// NewDNSProvider returns a DNSProvider instance configured for dnsimple.
// Credentials must be passed in the environment variables: DNSIMPLE_OAUTH_TOKEN.
//
// See: https://developer.dnsimple.com/v2/#authentication
func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.AccessToken = env.GetOrFile("DNSIMPLE_OAUTH_TOKEN")
config.BaseURL = env.GetOrFile("DNSIMPLE_BASE_URL")
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for DNSimple.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dnsimple: the configuration of the DNS provider is nil")
}
if config.AccessToken == "" {
return nil, fmt.Errorf("dnsimple: OAuth token is missing")
}
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.AccessToken})
client := dnsimple.NewClient(oauth2.NewClient(context.Background(), ts))
if config.BaseURL != "" {
client.BaseURL = config.BaseURL
}
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zoneName, err := d.getHostedZone(domain)
if err != nil {
return fmt.Errorf("dnsimple: %v", err)
}
accountID, err := d.getAccountID()
if err != nil {
return fmt.Errorf("dnsimple: %v", err)
}
recordAttributes := newTxtRecord(zoneName, fqdn, value, d.config.TTL)
_, err = d.client.Zones.CreateRecord(accountID, zoneName, recordAttributes)
if err != nil {
return fmt.Errorf("dnsimple: API call failed: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
records, err := d.findTxtRecords(domain, fqdn)
if err != nil {
return fmt.Errorf("dnsimple: %v", err)
}
accountID, err := d.getAccountID()
if err != nil {
return fmt.Errorf("dnsimple: %v", err)
}
var lastErr error
for _, rec := range records {
_, err := d.client.Zones.DeleteRecord(accountID, rec.ZoneID, rec.ID)
if err != nil {
lastErr = fmt.Errorf("dnsimple: %v", err)
}
}
return lastErr
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(domain string) (string, error) {
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return "", err
}
accountID, err := d.getAccountID()
if err != nil {
return "", err
}
zoneName := dns01.UnFqdn(authZone)
zones, err := d.client.Zones.ListZones(accountID, &dnsimple.ZoneListOptions{NameLike: zoneName})
if err != nil {
return "", fmt.Errorf("API call failed: %v", err)
}
var hostedZone dnsimple.Zone
for _, zone := range zones.Data {
if zone.Name == zoneName {
hostedZone = zone
}
}
if hostedZone.ID == 0 {
return "", fmt.Errorf("zone %s not found in DNSimple for domain %s", authZone, domain)
}
return hostedZone.Name, nil
}
func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnsimple.ZoneRecord, error) {
zoneName, err := d.getHostedZone(domain)
if err != nil {
return nil, err
}
accountID, err := d.getAccountID()
if err != nil {
return nil, err
}
recordName := extractRecordName(fqdn, zoneName)
result, err := d.client.Zones.ListRecords(accountID, zoneName, &dnsimple.ZoneRecordListOptions{Name: recordName, Type: "TXT", ListOptions: dnsimple.ListOptions{}})
if err != nil {
return nil, fmt.Errorf("API call has failed: %v", err)
}
return result.Data, nil
}
func newTxtRecord(zoneName, fqdn, value string, ttl int) dnsimple.ZoneRecord {
name := extractRecordName(fqdn, zoneName)
return dnsimple.ZoneRecord{
Type: "TXT",
Name: name,
Content: value,
TTL: ttl,
}
}
func extractRecordName(fqdn, domain string) string {
name := dns01.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}
func (d *DNSProvider) getAccountID() (string, error) {
whoamiResponse, err := d.client.Identity.Whoami()
if err != nil {
return "", err
}
if whoamiResponse.Data.Account == nil {
return "", fmt.Errorf("user tokens are not supported, please use an account token")
}
return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil
}

View file

@ -0,0 +1,163 @@
// Package dnsmadeeasy implements a DNS provider for solving the DNS-01 challenge using DNS Made Easy.
package dnsmadeeasy
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/go-acme/lego/providers/dns/dnsmadeeasy/internal"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
APISecret string
Sandbox bool
HTTPClient *http.Client
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DNSMADEEASY_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("DNSMADEEASY_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DNSMADEEASY_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DNSMADEEASY_HTTP_TIMEOUT", 10*time.Second),
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// DNSMadeEasy's DNS API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
client *internal.Client
}
// NewDNSProvider returns a DNSProvider instance configured for DNSMadeEasy DNS.
// Credentials must be passed in the environment variables:
// DNSMADEEASY_API_KEY and DNSMADEEASY_API_SECRET.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DNSMADEEASY_API_KEY", "DNSMADEEASY_API_SECRET")
if err != nil {
return nil, fmt.Errorf("dnsmadeeasy: %v", err)
}
config := NewDefaultConfig()
config.Sandbox = env.GetOrDefaultBool("DNSMADEEASY_SANDBOX", false)
config.APIKey = values["DNSMADEEASY_API_KEY"]
config.APISecret = values["DNSMADEEASY_API_SECRET"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for DNS Made Easy.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dnsmadeeasy: the configuration of the DNS provider is nil")
}
var baseURL string
if config.Sandbox {
baseURL = "https://api.sandbox.dnsmadeeasy.com/V2.0"
} else {
if len(config.BaseURL) > 0 {
baseURL = config.BaseURL
} else {
baseURL = "https://api.dnsmadeeasy.com/V2.0"
}
}
client, err := internal.NewClient(config.APIKey, config.APISecret)
if err != nil {
return nil, fmt.Errorf("dnsmadeeasy: %v", err)
}
client.HTTPClient = config.HTTPClient
client.BaseURL = baseURL
return &DNSProvider{
client: client,
config: config,
}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domainName, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %v", fqdn, err)
}
// fetch the domain details
domain, err := d.client.GetDomain(authZone)
if err != nil {
return fmt.Errorf("dnsmadeeasy: unable to get domain for zone %s: %v", authZone, err)
}
// create the TXT record
name := strings.Replace(fqdn, "."+authZone, "", 1)
record := &internal.Record{Type: "TXT", Name: name, Value: value, TTL: d.config.TTL}
err = d.client.CreateRecord(domain, record)
if err != nil {
return fmt.Errorf("dnsmadeeasy: unable to create record for %s: %v", name, err)
}
return nil
}
// CleanUp removes the TXT records matching the specified parameters
func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domainName, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %v", fqdn, err)
}
// fetch the domain details
domain, err := d.client.GetDomain(authZone)
if err != nil {
return fmt.Errorf("dnsmadeeasy: unable to get domain for zone %s: %v", authZone, err)
}
// find matching records
name := strings.Replace(fqdn, "."+authZone, "", 1)
records, err := d.client.GetRecords(domain, name, "TXT")
if err != nil {
return fmt.Errorf("dnsmadeeasy: unable to get records for domain %s: %v", domain.Name, err)
}
// delete records
var lastError error
for _, record := range *records {
err = d.client.DeleteRecord(record)
if err != nil {
lastError = fmt.Errorf("dnsmadeeasy: unable to delete record [id=%d, name=%s]: %v", record.ID, record.Name, err)
}
}
return lastError
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,173 @@
package internal
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// Domain holds the DNSMadeEasy API representation of a Domain
type Domain struct {
ID int `json:"id"`
Name string `json:"name"`
}
// Record holds the DNSMadeEasy API representation of a Domain Record
type Record struct {
ID int `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL int `json:"ttl"`
SourceID int `json:"sourceId"`
}
type recordsResponse struct {
Records *[]Record `json:"data"`
}
// Client DNSMadeEasy client
type Client struct {
apiKey string
apiSecret string
BaseURL string
HTTPClient *http.Client
}
// NewClient creates a DNSMadeEasy client
func NewClient(apiKey string, apiSecret string) (*Client, error) {
if apiKey == "" {
return nil, fmt.Errorf("credentials missing: API key")
}
if apiSecret == "" {
return nil, fmt.Errorf("credentials missing: API secret")
}
return &Client{
apiKey: apiKey,
apiSecret: apiSecret,
HTTPClient: &http.Client{},
}, nil
}
// GetDomain gets a domain
func (c *Client) GetDomain(authZone string) (*Domain, error) {
domainName := authZone[0 : len(authZone)-1]
resource := fmt.Sprintf("%s%s", "/dns/managed/name?domainname=", domainName)
resp, err := c.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
domain := &Domain{}
err = json.NewDecoder(resp.Body).Decode(&domain)
if err != nil {
return nil, err
}
return domain, nil
}
// GetRecords gets all TXT records
func (c *Client) GetRecords(domain *Domain, recordName, recordType string) (*[]Record, error) {
resource := fmt.Sprintf("%s/%d/%s%s%s%s", "/dns/managed", domain.ID, "records?recordName=", recordName, "&type=", recordType)
resp, err := c.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
records := &recordsResponse{}
err = json.NewDecoder(resp.Body).Decode(&records)
if err != nil {
return nil, err
}
return records.Records, nil
}
// CreateRecord creates a TXT records
func (c *Client) CreateRecord(domain *Domain, record *Record) error {
url := fmt.Sprintf("%s/%d/%s", "/dns/managed", domain.ID, "records")
resp, err := c.sendRequest(http.MethodPost, url, record)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// DeleteRecord deletes a TXT records
func (c *Client) DeleteRecord(record Record) error {
resource := fmt.Sprintf("%s/%d/%s/%d", "/dns/managed", record.SourceID, "records", record.ID)
resp, err := c.sendRequest(http.MethodDelete, resource, nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (c *Client) sendRequest(method, resource string, payload interface{}) (*http.Response, error) {
url := fmt.Sprintf("%s%s", c.BaseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
timestamp := time.Now().UTC().Format(time.RFC1123)
signature, err := computeHMAC(timestamp, c.apiSecret)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("x-dnsme-apiKey", c.apiKey)
req.Header.Set("x-dnsme-requestDate", timestamp)
req.Header.Set("x-dnsme-hmac", signature)
req.Header.Set("accept", "application/json")
req.Header.Set("content-type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode)
}
return nil, fmt.Errorf("request failed with HTTP status code %d: %s", resp.StatusCode, string(body))
}
return resp, nil
}
func computeHMAC(message string, secret string) (string, error) {
key := []byte(secret)
h := hmac.New(sha1.New, key)
_, err := h.Write([]byte(message))
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}

View file

@ -0,0 +1,189 @@
// Package dnspod implements a DNS provider for solving the DNS-01 challenge using dnspod DNS.
package dnspod
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
dnspod "github.com/decker502/dnspod-go"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
LoginToken string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DNSPOD_TTL", 600),
PropagationTimeout: env.GetOrDefaultSecond("DNSPOD_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DNSPOD_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DNSPOD_HTTP_TIMEOUT", 0),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *dnspod.Client
}
// NewDNSProvider returns a DNSProvider instance configured for dnspod.
// Credentials must be passed in the environment variables: DNSPOD_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DNSPOD_API_KEY")
if err != nil {
return nil, fmt.Errorf("dnspod: %v", err)
}
config := NewDefaultConfig()
config.LoginToken = values["DNSPOD_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for dnspod.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dnspod: the configuration of the DNS provider is nil")
}
if config.LoginToken == "" {
return nil, fmt.Errorf("dnspod: credentials missing")
}
params := dnspod.CommonParams{LoginToken: config.LoginToken, Format: "json"}
client := dnspod.NewClient(params)
client.HttpClient = config.HTTPClient
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zoneID, zoneName, err := d.getHostedZone(domain)
if err != nil {
return err
}
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, d.config.TTL)
_, _, err = d.client.Domains.CreateRecord(zoneID, *recordAttributes)
if err != nil {
return fmt.Errorf("API call failed: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
records, err := d.findTxtRecords(domain, fqdn)
if err != nil {
return err
}
zoneID, _, err := d.getHostedZone(domain)
if err != nil {
return err
}
for _, rec := range records {
_, err := d.client.Domains.DeleteRecord(zoneID, rec.ID)
if err != nil {
return err
}
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(domain string) (string, string, error) {
zones, _, err := d.client.Domains.List()
if err != nil {
return "", "", fmt.Errorf("API call failed: %v", err)
}
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return "", "", err
}
var hostedZone dnspod.Domain
for _, zone := range zones {
if zone.Name == dns01.UnFqdn(authZone) {
hostedZone = zone
}
}
if hostedZone.ID == 0 {
return "", "", fmt.Errorf("zone %s not found in dnspod for domain %s", authZone, domain)
}
return fmt.Sprintf("%v", hostedZone.ID), hostedZone.Name, nil
}
func (d *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnspod.Record {
name := d.extractRecordName(fqdn, zone)
return &dnspod.Record{
Type: "TXT",
Name: name,
Value: value,
Line: "默认",
TTL: strconv.Itoa(ttl),
}
}
func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnspod.Record, error) {
zoneID, zoneName, err := d.getHostedZone(domain)
if err != nil {
return nil, err
}
var records []dnspod.Record
result, _, err := d.client.Domains.ListRecords(zoneID, "")
if err != nil {
return records, fmt.Errorf("API call has failed: %v", err)
}
recordName := d.extractRecordName(fqdn, zoneName)
for _, record := range result {
if record.Name == recordName {
records = append(records, record)
}
}
return records, nil
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
name := dns01.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}

View file

@ -0,0 +1,73 @@
package dreamhost
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"github.com/go-acme/lego/log"
)
const (
defaultBaseURL = "https://api.dreamhost.com"
cmdAddRecord = "dns-add_record"
cmdRemoveRecord = "dns-remove_record"
)
type apiResponse struct {
Data string `json:"data"`
Result string `json:"result"`
}
func (d *DNSProvider) buildQuery(action, domain, txt string) (*url.URL, error) {
u, err := url.Parse(d.config.BaseURL)
if err != nil {
return nil, err
}
query := u.Query()
query.Set("key", d.config.APIKey)
query.Set("cmd", action)
query.Set("format", "json")
query.Set("record", domain)
query.Set("type", "TXT")
query.Set("value", txt)
query.Set("comment", url.QueryEscape("Managed By lego"))
u.RawQuery = query.Encode()
return u, nil
}
// updateTxtRecord will either add or remove a TXT record.
// action is either cmdAddRecord or cmdRemoveRecord
func (d *DNSProvider) updateTxtRecord(u fmt.Stringer) error {
resp, err := d.config.HTTPClient.Get(u.String())
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode)
}
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read body: %v", err)
}
var response apiResponse
err = json.Unmarshal(raw, &response)
if err != nil {
return fmt.Errorf("unable to decode API server response: %v: %s", err, string(raw))
}
if response.Result == "error" {
return fmt.Errorf("add TXT record failed: %s", response.Data)
}
log.Infof("dreamhost: %s", response.Data)
return nil
}

View file

@ -0,0 +1,111 @@
// Package dreamhost implements a DNS provider for solving the DNS-01 challenge using DreamHost.
// See https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview
// and https://help.dreamhost.com/hc/en-us/articles/217555707-DNS-API-commands for the API spec.
package dreamhost
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
BaseURL: defaultBaseURL,
PropagationTimeout: env.GetOrDefaultSecond("DREAMHOST_PROPAGATION_TIMEOUT", 60*time.Minute),
PollingInterval: env.GetOrDefaultSecond("DREAMHOST_POLLING_INTERVAL", 1*time.Minute),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DREAMHOST_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider adds and removes the record for the DNS challenge
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a new DNS provider using
// environment variable DREAMHOST_TOKEN for adding and removing the DNS record.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DREAMHOST_API_KEY")
if err != nil {
return nil, fmt.Errorf("dreamhost: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["DREAMHOST_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for DreamHost.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dreamhost: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, errors.New("dreamhost: credentials missing")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
record := dns01.UnFqdn(fqdn)
u, err := d.buildQuery(cmdAddRecord, record, value)
if err != nil {
return fmt.Errorf("dreamhost: %v", err)
}
err = d.updateTxtRecord(u)
if err != nil {
return fmt.Errorf("dreamhost: %v", err)
}
return nil
}
// CleanUp clears DreamHost TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
record := dns01.UnFqdn(fqdn)
u, err := d.buildQuery(cmdRemoveRecord, record, value)
if err != nil {
return fmt.Errorf("dreamhost: %v", err)
}
err = d.updateTxtRecord(u)
if err != nil {
return fmt.Errorf("dreamhost: %v", err)
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,68 @@
package duckdns
import (
"fmt"
"io/ioutil"
"net/url"
"strconv"
"strings"
"github.com/go-acme/lego/challenge/dns01"
"github.com/miekg/dns"
)
// updateTxtRecord Update the domains TXT record
// To update the TXT record we just need to make one simple get request.
// In DuckDNS you only have one TXT record shared with the domain and all sub domains.
func (d *DNSProvider) updateTxtRecord(domain, token, txt string, clear bool) error {
u, _ := url.Parse("https://www.duckdns.org/update")
mainDomain := getMainDomain(domain)
if len(mainDomain) == 0 {
return fmt.Errorf("unable to find the main domain for: %s", domain)
}
query := u.Query()
query.Set("domains", mainDomain)
query.Set("token", token)
query.Set("clear", strconv.FormatBool(clear))
query.Set("txt", txt)
u.RawQuery = query.Encode()
response, err := d.config.HTTPClient.Get(u.String())
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, u)
}
return nil
}
// DuckDNS only lets you write to your subdomain
// so it must be in format subdomain.duckdns.org
// not in format subsubdomain.subdomain.duckdns.org
// so strip off everything that is not top 3 levels
func getMainDomain(domain string) string {
domain = dns01.UnFqdn(domain)
split := dns.Split(domain)
if strings.HasSuffix(strings.ToLower(domain), "duckdns.org") {
if len(split) < 3 {
return ""
}
firstSubDomainIndex := split[len(split)-3]
return domain[firstSubDomainIndex:]
}
return domain[split[len(split)-1]:]
}

View file

@ -0,0 +1,89 @@
// Package duckdns implements a DNS provider for solving the DNS-01 challenge using DuckDNS.
// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.
package duckdns
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Token string
PropagationTimeout time.Duration
PollingInterval time.Duration
SequenceInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("DUCKDNS_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DUCKDNS_POLLING_INTERVAL", dns01.DefaultPollingInterval),
SequenceInterval: env.GetOrDefaultSecond("DUCKDNS_SEQUENCE_INTERVAL", dns01.DefaultPropagationTimeout),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DUCKDNS_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider adds and removes the record for the DNS challenge
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a new DNS provider using
// environment variable DUCKDNS_TOKEN for adding and removing the DNS record.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DUCKDNS_TOKEN")
if err != nil {
return nil, fmt.Errorf("duckdns: %v", err)
}
config := NewDefaultConfig()
config.Token = values["DUCKDNS_TOKEN"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for DuckDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("duckdns: the configuration of the DNS provider is nil")
}
if config.Token == "" {
return nil, errors.New("duckdns: credentials missing")
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord := dns01.GetRecord(domain, keyAuth)
return d.updateTxtRecord(domain, d.config.Token, txtRecord, false)
}
// CleanUp clears DuckDNS TXT record
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return d.updateTxtRecord(domain, d.config.Token, "", true)
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Sequential All DNS challenges for this provider will be resolved sequentially.
// Returns the interval between each iteration.
func (d *DNSProvider) Sequential() time.Duration {
return d.config.SequenceInterval
}

View file

@ -0,0 +1,146 @@
package dyn
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const defaultBaseURL = "https://api.dynect.net/REST"
type dynResponse struct {
// One of 'success', 'failure', or 'incomplete'
Status string `json:"status"`
// The structure containing the actual results of the request
Data json.RawMessage `json:"data"`
// The ID of the job that was created in response to a request.
JobID int `json:"job_id"`
// A list of zero or more messages
Messages json.RawMessage `json:"msgs"`
}
type credentials struct {
Customer string `json:"customer_name"`
User string `json:"user_name"`
Pass string `json:"password"`
}
type session struct {
Token string `json:"token"`
Version string `json:"version"`
}
type publish struct {
Publish bool `json:"publish"`
Notes string `json:"notes"`
}
// Starts a new Dyn API Session. Authenticates using customerName, userName,
// password and receives a token to be used in for subsequent requests.
func (d *DNSProvider) login() error {
payload := &credentials{Customer: d.config.CustomerName, User: d.config.UserName, Pass: d.config.Password}
dynRes, err := d.sendRequest(http.MethodPost, "Session", payload)
if err != nil {
return err
}
var s session
err = json.Unmarshal(dynRes.Data, &s)
if err != nil {
return err
}
d.token = s.Token
return nil
}
// Destroys Dyn Session
func (d *DNSProvider) logout() error {
if len(d.token) == 0 {
// nothing to do
return nil
}
url := fmt.Sprintf("%s/Session", defaultBaseURL)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Auth-Token", d.token)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("API request failed to delete session with HTTP status code %d", resp.StatusCode)
}
d.token = ""
return nil
}
func (d *DNSProvider) publish(zone, notes string) error {
pub := &publish{Publish: true, Notes: notes}
resource := fmt.Sprintf("Zone/%s/", zone)
_, err := d.sendRequest(http.MethodPut, resource, pub)
return err
}
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) {
url := fmt.Sprintf("%s/%s", defaultBaseURL, resource)
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if len(d.token) > 0 {
req.Header.Set("Auth-Token", d.token)
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("API request failed with HTTP status code %d", resp.StatusCode)
}
var dynRes dynResponse
err = json.NewDecoder(resp.Body).Decode(&dynRes)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("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("API request returned HTTP 307. This is currently unsupported")
}
if dynRes.Status == "failure" {
// TODO add better error handling
return nil, fmt.Errorf("API request failed: %s", dynRes.Messages)
}
return &dynRes, nil
}

157
vendor/github.com/go-acme/lego/providers/dns/dyn/dyn.go generated vendored Normal file
View file

@ -0,0 +1,157 @@
// Package dyn implements a DNS provider for solving the DNS-01 challenge using Dyn Managed DNS.
package dyn
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
CustomerName string
UserName string
Password string
HTTPClient *http.Client
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DYN_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("DYN_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DYN_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DYN_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// Dyn's Managed DNS API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
token string
}
// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
// Credentials must be passed in the environment variables:
// DYN_CUSTOMER_NAME, DYN_USER_NAME and DYN_PASSWORD.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("DYN_CUSTOMER_NAME", "DYN_USER_NAME", "DYN_PASSWORD")
if err != nil {
return nil, fmt.Errorf("dyn: %v", err)
}
config := NewDefaultConfig()
config.CustomerName = values["DYN_CUSTOMER_NAME"]
config.UserName = values["DYN_USER_NAME"]
config.Password = values["DYN_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("dyn: the configuration of the DNS provider is nil")
}
if config.CustomerName == "" || config.UserName == "" || config.Password == "" {
return nil, fmt.Errorf("dyn: credentials missing")
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
err = d.login()
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
data := map[string]interface{}{
"rdata": map[string]string{
"txtdata": value,
},
"ttl": strconv.Itoa(d.config.TTL),
}
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
_, err = d.sendRequest(http.MethodPost, resource, data)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client")
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
return d.logout()
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
err = d.login()
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
url := fmt.Sprintf("%s/%s", defaultBaseURL, resource)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Auth-Token", d.token)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("dyn: API request failed to delete TXT record HTTP status code %d", resp.StatusCode)
}
err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client")
if err != nil {
return fmt.Errorf("dyn: %v", err)
}
return d.logout()
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,107 @@
// Package exec implements a DNS provider which runs a program for adding/removing the DNS record.
package exec
import (
"errors"
"fmt"
"os"
"os/exec"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
)
// Config Provider configuration.
type Config struct {
Program string
Mode string
PropagationTimeout time.Duration
PollingInterval time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("EXEC_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("EXEC_POLLING_INTERVAL", dns01.DefaultPollingInterval),
}
}
// DNSProvider adds and removes the record for the DNS challenge by calling a
// program with command-line parameters.
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a new DNS provider which runs the program in the
// environment variable EXEC_PATH for adding and removing the DNS record.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("EXEC_PATH")
if err != nil {
return nil, fmt.Errorf("exec: %v", err)
}
config := NewDefaultConfig()
config.Program = values["EXEC_PATH"]
config.Mode = os.Getenv("EXEC_MODE")
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig returns a new DNS provider which runs the given configuration
// for adding and removing the DNS record.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration is nil")
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
var args []string
if d.config.Mode == "RAW" {
args = []string{"present", "--", domain, token, keyAuth}
} else {
fqdn, value := dns01.GetRecord(domain, keyAuth)
args = []string{"present", fqdn, value}
}
cmd := exec.Command(d.config.Program, args...)
output, err := cmd.CombinedOutput()
if len(output) > 0 {
log.Println(string(output))
}
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
var args []string
if d.config.Mode == "RAW" {
args = []string{"cleanup", "--", domain, token, keyAuth}
} else {
fqdn, value := dns01.GetRecord(domain, keyAuth)
args = []string{"cleanup", fqdn, value}
}
cmd := exec.Command(d.config.Program, args...)
output, err := cmd.CombinedOutput()
if len(output) > 0 {
log.Println(string(output))
}
return err
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,184 @@
// Package exoscale implements a DNS provider for solving the DNS-01 challenge using exoscale DNS.
package exoscale
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/exoscale/egoscale"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const defaultBaseURL = "https://api.exoscale.com/dns"
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
APISecret string
Endpoint string
HTTPClient *http.Client
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("EXOSCALE_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("EXOSCALE_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("EXOSCALE_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("EXOSCALE_HTTP_TIMEOUT", 0),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *egoscale.Client
}
// NewDNSProvider Credentials must be passed in the environment variables:
// EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("EXOSCALE_API_KEY", "EXOSCALE_API_SECRET")
if err != nil {
return nil, fmt.Errorf("exoscale: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["EXOSCALE_API_KEY"]
config.APISecret = values["EXOSCALE_API_SECRET"]
config.Endpoint = env.GetOrFile("EXOSCALE_ENDPOINT")
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Exoscale.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the DNS provider is nil")
}
if config.APIKey == "" || config.APISecret == "" {
return nil, fmt.Errorf("exoscale: credentials missing")
}
if config.Endpoint == "" {
config.Endpoint = defaultBaseURL
}
client := egoscale.NewClient(config.Endpoint, config.APIKey, config.APISecret)
client.HTTPClient = config.HTTPClient
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
ctx := context.Background()
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, recordName, err := d.FindZoneAndRecordName(fqdn, domain)
if err != nil {
return err
}
recordID, err := d.FindExistingRecordID(zone, recordName)
if err != nil {
return err
}
if recordID == 0 {
record := egoscale.DNSRecord{
Name: recordName,
TTL: d.config.TTL,
Content: value,
RecordType: "TXT",
}
_, err := d.client.CreateRecord(ctx, zone, record)
if err != nil {
return errors.New("Error while creating DNS record: " + err.Error())
}
} else {
record := egoscale.UpdateDNSRecord{
ID: recordID,
Name: recordName,
TTL: d.config.TTL,
Content: value,
RecordType: "TXT",
}
_, err := d.client.UpdateRecord(ctx, zone, record)
if err != nil {
return errors.New("Error while updating DNS record: " + err.Error())
}
}
return nil
}
// CleanUp removes the record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
ctx := context.Background()
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, recordName, err := d.FindZoneAndRecordName(fqdn, domain)
if err != nil {
return err
}
recordID, err := d.FindExistingRecordID(zone, recordName)
if err != nil {
return err
}
if recordID != 0 {
err = d.client.DeleteRecord(ctx, zone, recordID)
if err != nil {
return errors.New("Error while deleting DNS record: " + err.Error())
}
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// FindExistingRecordID Query Exoscale to find an existing record for this name.
// Returns nil if no record could be found
func (d *DNSProvider) FindExistingRecordID(zone, recordName string) (int64, error) {
ctx := context.Background()
records, err := d.client.GetRecords(ctx, zone)
if err != nil {
return -1, errors.New("Error while retrievening DNS records: " + err.Error())
}
for _, record := range records {
if record.Name == recordName {
return record.ID, nil
}
}
return 0, nil
}
// FindZoneAndRecordName Extract DNS zone and DNS entry name
func (d *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) {
zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return "", "", err
}
zone = dns01.UnFqdn(zone)
name := dns01.UnFqdn(fqdn)
name = name[:len(name)-len("."+zone)]
return zone, name, nil
}

View file

@ -0,0 +1,158 @@
// Package fastdns implements a DNS provider for solving the DNS-01 challenge using FastDNS.
package fastdns
import (
"errors"
"fmt"
"reflect"
"time"
configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v1"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
edgegrid.Config
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("AKAMAI_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("AKAMAI_POLLING_INTERVAL", dns01.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("AKAMAI_TTL", dns01.DefaultTTL),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
config *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) {
values, err := env.Get("AKAMAI_HOST", "AKAMAI_CLIENT_TOKEN", "AKAMAI_CLIENT_SECRET", "AKAMAI_ACCESS_TOKEN")
if err != nil {
return nil, fmt.Errorf("fastdns: %v", err)
}
config := NewDefaultConfig()
config.Config = edgegrid.Config{
Host: values["AKAMAI_HOST"],
ClientToken: values["AKAMAI_CLIENT_TOKEN"],
ClientSecret: values["AKAMAI_CLIENT_SECRET"],
AccessToken: values["AKAMAI_ACCESS_TOKEN"],
MaxBody: 131072,
}
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for FastDNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("fastdns: the configuration of the DNS provider is nil")
}
if config.ClientToken == "" || config.ClientSecret == "" || config.AccessToken == "" || config.Host == "" {
return nil, fmt.Errorf("fastdns: credentials are missing")
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record to fullfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zoneName, recordName, err := d.findZoneAndRecordName(fqdn, domain)
if err != nil {
return fmt.Errorf("fastdns: %v", err)
}
configdns.Init(d.config.Config)
zone, err := configdns.GetZone(zoneName)
if err != nil {
return fmt.Errorf("fastdns: %v", err)
}
record := configdns.NewTxtRecord()
_ = record.SetField("name", recordName)
_ = record.SetField("ttl", d.config.TTL)
_ = record.SetField("target", value)
_ = record.SetField("active", true)
for _, r := range zone.Zone.Txt {
if r != nil && reflect.DeepEqual(r.ToMap(), record.ToMap()) {
return nil
}
}
return d.createRecord(zone, record)
}
// CleanUp removes the record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zoneName, recordName, err := d.findZoneAndRecordName(fqdn, domain)
if err != nil {
return fmt.Errorf("fastdns: %v", err)
}
configdns.Init(d.config.Config)
zone, err := configdns.GetZone(zoneName)
if err != nil {
return fmt.Errorf("fastdns: %v", err)
}
var removed bool
for _, r := range zone.Zone.Txt {
if r != nil && r.Name == recordName {
if zone.RemoveRecord(r) != nil {
return fmt.Errorf("fastdns: %v", err)
}
removed = true
}
}
if removed {
return zone.Save()
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) findZoneAndRecordName(fqdn, domain string) (string, string, error) {
zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return "", "", err
}
zone = dns01.UnFqdn(zone)
name := dns01.UnFqdn(fqdn)
name = name[:len(name)-len("."+zone)]
return zone, name, nil
}
func (d *DNSProvider) createRecord(zone *configdns.Zone, record *configdns.TxtRecord) error {
err := zone.AddRecord(record)
if err != nil {
return err
}
return zone.Save()
}

View file

@ -0,0 +1,316 @@
package gandi
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
)
// types for XML-RPC method calls and parameters
type param interface {
param()
}
type paramString struct {
XMLName xml.Name `xml:"param"`
Value string `xml:"value>string"`
}
type paramInt struct {
XMLName xml.Name `xml:"param"`
Value int `xml:"value>int"`
}
type structMember interface {
structMember()
}
type structMemberString struct {
Name string `xml:"name"`
Value string `xml:"value>string"`
}
type structMemberInt struct {
Name string `xml:"name"`
Value int `xml:"value>int"`
}
type paramStruct struct {
XMLName xml.Name `xml:"param"`
StructMembers []structMember `xml:"value>struct>member"`
}
func (p paramString) param() {}
func (p paramInt) param() {}
func (m structMemberString) structMember() {}
func (m structMemberInt) structMember() {}
func (p paramStruct) param() {}
type methodCall struct {
XMLName xml.Name `xml:"methodCall"`
MethodName string `xml:"methodName"`
Params []param `xml:"params"`
}
// types for XML-RPC responses
type response interface {
faultCode() int
faultString() string
}
type responseFault struct {
FaultCode int `xml:"fault>value>struct>member>value>int"`
FaultString string `xml:"fault>value>struct>member>value>string"`
}
func (r responseFault) faultCode() int { return r.FaultCode }
func (r responseFault) faultString() string { return r.FaultString }
type responseStruct struct {
responseFault
StructMembers []struct {
Name string `xml:"name"`
ValueInt int `xml:"value>int"`
} `xml:"params>param>value>struct>member"`
}
type responseInt struct {
responseFault
Value int `xml:"params>param>value>int"`
}
type responseBool struct {
responseFault
Value bool `xml:"params>param>value>boolean"`
}
type rpcError struct {
faultCode int
faultString string
}
func (e rpcError) Error() string {
return fmt.Sprintf("Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString)
}
// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by
// marshaling the data given in the call argument to XML and sending
// that via HTTP Post to Gandi.
// The response is then unmarshalled into the resp argument.
func (d *DNSProvider) rpcCall(call *methodCall, resp response) error {
// marshal
b, err := xml.MarshalIndent(call, "", " ")
if err != nil {
return fmt.Errorf("marshal error: %v", err)
}
// post
b = append([]byte(`<?xml version="1.0"?>`+"\n"), b...)
respBody, err := d.httpPost(d.config.BaseURL, "text/xml", bytes.NewReader(b))
if err != nil {
return err
}
// unmarshal
err = xml.Unmarshal(respBody, resp)
if err != nil {
return fmt.Errorf("unmarshal error: %v", err)
}
if resp.faultCode() != 0 {
return rpcError{
faultCode: resp.faultCode(), faultString: resp.faultString()}
}
return nil
}
// functions to perform API actions
func (d *DNSProvider) getZoneID(domain string) (int, error) {
resp := &responseStruct{}
err := d.rpcCall(&methodCall{
MethodName: "domain.info",
Params: []param{
paramString{Value: d.config.APIKey},
paramString{Value: domain},
},
}, resp)
if err != nil {
return 0, err
}
var zoneID int
for _, member := range resp.StructMembers {
if member.Name == "zone_id" {
zoneID = member.ValueInt
}
}
if zoneID == 0 {
return 0, fmt.Errorf("could not determine zone_id for %s", domain)
}
return zoneID, nil
}
func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) {
resp := &responseStruct{}
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.clone",
Params: []param{
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
paramInt{Value: 0},
paramStruct{
StructMembers: []structMember{
structMemberString{
Name: "name",
Value: name,
}},
},
},
}, resp)
if err != nil {
return 0, err
}
var newZoneID int
for _, member := range resp.StructMembers {
if member.Name == "id" {
newZoneID = member.ValueInt
}
}
if newZoneID == 0 {
return 0, fmt.Errorf("could not determine cloned zone_id")
}
return newZoneID, nil
}
func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) {
resp := &responseInt{}
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.version.new",
Params: []param{
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
},
}, resp)
if err != nil {
return 0, err
}
if resp.Value == 0 {
return 0, fmt.Errorf("could not create new zone version")
}
return resp.Value, nil
}
func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value string, ttl int) error {
resp := &responseStruct{}
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.record.add",
Params: []param{
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
paramInt{Value: version},
paramStruct{
StructMembers: []structMember{
structMemberString{
Name: "type",
Value: "TXT",
}, structMemberString{
Name: "name",
Value: name,
}, structMemberString{
Name: "value",
Value: value,
}, structMemberInt{
Name: "ttl",
Value: ttl,
}},
},
},
}, resp)
return err
}
func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
resp := &responseBool{}
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.version.set",
Params: []param{
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
paramInt{Value: version},
},
}, resp)
if err != nil {
return err
}
if !resp.Value {
return fmt.Errorf("could not set zone version")
}
return nil
}
func (d *DNSProvider) setZone(domain string, zoneID int) error {
resp := &responseStruct{}
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.set",
Params: []param{
paramString{Value: d.config.APIKey},
paramString{Value: domain},
paramInt{Value: zoneID},
},
}, resp)
if err != nil {
return err
}
var respZoneID int
for _, member := range resp.StructMembers {
if member.Name == "zone_id" {
respZoneID = member.ValueInt
}
}
if respZoneID != zoneID {
return fmt.Errorf("could not set new zone_id for %s", domain)
}
return nil
}
func (d *DNSProvider) deleteZone(zoneID int) error {
resp := &responseBool{}
err := d.rpcCall(&methodCall{
MethodName: "domain.zone.delete",
Params: []param{
paramString{Value: d.config.APIKey},
paramInt{Value: zoneID},
},
}, resp)
if err != nil {
return err
}
if !resp.Value {
return fmt.Errorf("could not delete zone_id")
}
return nil
}
func (d *DNSProvider) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
resp, err := d.config.HTTPClient.Post(url, bodyType, body)
if err != nil {
return nil, fmt.Errorf("HTTP Post Error: %v", err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("HTTP Post Error: %v", err)
}
return b, nil
}

View file

@ -0,0 +1,214 @@
// Package gandi implements a DNS provider for solving the DNS-01 challenge using Gandi DNS.
package gandi
import (
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Gandi API reference: http://doc.rpc.gandi.net/index.html
// Gandi API domain examples: http://doc.rpc.gandi.net/domain/faq.html
const (
// defaultBaseURL Gandi XML-RPC endpoint used by Present and CleanUp
defaultBaseURL = "https://rpc.gandi.net/xmlrpc/"
minTTL = 300
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GANDI_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GANDI_PROPAGATION_TIMEOUT", 40*time.Minute),
PollingInterval: env.GetOrDefaultSecond("GANDI_POLLING_INTERVAL", 60*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GANDI_HTTP_TIMEOUT", 60*time.Second),
},
}
}
// inProgressInfo contains information about an in-progress challenge
type inProgressInfo struct {
zoneID int // zoneID of gandi zone to restore in CleanUp
newZoneID int // zoneID of temporary gandi zone containing TXT record
authZone string // the domain name registered at gandi with trailing "."
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses Gandi's XML-RPC
// API to manage TXT records for a domain.
type DNSProvider struct {
inProgressFQDNs map[string]inProgressInfo
inProgressAuthZones map[string]struct{}
inProgressMu sync.Mutex
config *Config
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden during tests.
findZoneByFqdn func(fqdn string) (string, error)
}
// NewDNSProvider returns a DNSProvider instance configured for Gandi.
// Credentials must be passed in the environment variable: GANDI_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("GANDI_API_KEY")
if err != nil {
return nil, fmt.Errorf("gandi: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["GANDI_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Gandi.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("gandi: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, fmt.Errorf("gandi: no API Key given")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
return &DNSProvider{
config: config,
inProgressFQDNs: make(map[string]inProgressInfo),
inProgressAuthZones: make(map[string]struct{}),
findZoneByFqdn: dns01.FindZoneByFqdn,
}, nil
}
// Present creates a TXT record using the specified parameters. It
// does this by creating and activating a new temporary Gandi DNS
// zone. This new zone contains the TXT record.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
if d.config.TTL < minTTL {
d.config.TTL = minTTL // 300 is gandi minimum value for ttl
}
// find authZone and Gandi zone_id for fqdn
authZone, err := d.findZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("gandi: findZoneByFqdn failure: %v", err)
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("gandi: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf("gandi: unexpected authZone %s for fqdn %s", authZone, fqdn)
}
name := fqdn[:len(fqdn)-len("."+authZone)]
// acquire lock and check there is not a challenge already in
// progress for this value of authZone
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()
if _, ok := d.inProgressAuthZones[authZone]; ok {
return fmt.Errorf("gandi: challenge already in progress for authZone %s", authZone)
}
// perform API actions to create and activate new gandi zone
// containing the required TXT record
newZoneName := fmt.Sprintf("%s [ACME Challenge %s]", dns01.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
newZoneID, err := d.cloneZone(zoneID, newZoneName)
if err != nil {
return err
}
newZoneVersion, err := d.newZoneVersion(newZoneID)
if err != nil {
return fmt.Errorf("gandi: %v", err)
}
err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, d.config.TTL)
if err != nil {
return fmt.Errorf("gandi: %v", err)
}
err = d.setZoneVersion(newZoneID, newZoneVersion)
if err != nil {
return fmt.Errorf("gandi: %v", err)
}
err = d.setZone(authZone, newZoneID)
if err != nil {
return fmt.Errorf("gandi: %v", err)
}
// save data necessary for CleanUp
d.inProgressFQDNs[fqdn] = inProgressInfo{
zoneID: zoneID,
newZoneID: newZoneID,
authZone: authZone,
}
d.inProgressAuthZones[authZone] = struct{}{}
return nil
}
// CleanUp removes the TXT record matching the specified
// parameters. It does this by restoring the old Gandi DNS zone and
// removing the temporary one created by Present.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
// acquire lock and retrieve zoneID, newZoneID and authZone
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()
if _, ok := d.inProgressFQDNs[fqdn]; !ok {
// if there is no cleanup information then just return
return nil
}
zoneID := d.inProgressFQDNs[fqdn].zoneID
newZoneID := d.inProgressFQDNs[fqdn].newZoneID
authZone := d.inProgressFQDNs[fqdn].authZone
delete(d.inProgressFQDNs, fqdn)
delete(d.inProgressAuthZones, authZone)
// perform API actions to restore old gandi zone for authZone
err := d.setZone(authZone, zoneID)
if err != nil {
return fmt.Errorf("gandi: %v", err)
}
return d.deleteZone(newZoneID)
}
// Timeout returns the values (40*time.Minute, 60*time.Second) which
// are used by the acme package as timeout and check interval values
// when checking for DNS record propagation with Gandi.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,199 @@
package gandiv5
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/go-acme/lego/log"
)
const apiKeyHeader = "X-Api-Key"
// types for JSON responses with only a message
type apiResponse struct {
Message string `json:"message"`
UUID string `json:"uuid,omitempty"`
}
// Record TXT record representation
type Record struct {
RRSetTTL int `json:"rrset_ttl"`
RRSetValues []string `json:"rrset_values"`
RRSetName string `json:"rrset_name,omitempty"`
RRSetType string `json:"rrset_type,omitempty"`
}
func (d *DNSProvider) addTXTRecord(domain string, name string, value string, ttl int) error {
// Get exiting values for the TXT records
// Needed to create challenges for both wildcard and base name domains
txtRecord, err := d.getTXTRecord(domain, name)
if err != nil {
return err
}
values := []string{value}
if len(txtRecord.RRSetValues) > 0 {
values = append(values, txtRecord.RRSetValues...)
}
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
newRecord := &Record{RRSetTTL: ttl, RRSetValues: values}
req, err := d.newRequest(http.MethodPut, target, newRecord)
if err != nil {
return err
}
message := &apiResponse{}
err = d.do(req, message)
if err != nil {
return fmt.Errorf("unable to create TXT record for domain %s and name %s: %v", domain, name, err)
}
if message != nil && len(message.Message) > 0 {
log.Infof("API response: %s", message.Message)
}
return nil
}
func (d *DNSProvider) getTXTRecord(domain, name string) (*Record, error) {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
// Get exiting values for the TXT records
// Needed to create challenges for both wildcard and base name domains
req, err := d.newRequest(http.MethodGet, target, nil)
if err != nil {
return nil, err
}
txtRecord := &Record{}
err = d.do(req, txtRecord)
if err != nil {
return nil, fmt.Errorf("unable to get TXT records for domain %s and name %s: %v", domain, name, err)
}
return txtRecord, nil
}
func (d *DNSProvider) deleteTXTRecord(domain string, name string) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
req, err := d.newRequest(http.MethodDelete, target, nil)
if err != nil {
return err
}
message := &apiResponse{}
err = d.do(req, message)
if err != nil {
return fmt.Errorf("unable to delete TXT record for domain %s and name %s: %v", domain, name, err)
}
if message != nil && len(message.Message) > 0 {
log.Infof("API response: %s", message.Message)
}
return nil
}
func (d *DNSProvider) newRequest(method, resource string, body interface{}) (*http.Request, error) {
u := fmt.Sprintf("%s/%s", d.config.BaseURL, resource)
if body == nil {
req, err := http.NewRequest(method, u, nil)
if err != nil {
return nil, err
}
return req, nil
}
reqBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, u, bytes.NewBuffer(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return req, nil
}
func (d *DNSProvider) do(req *http.Request, v interface{}) error {
if len(d.config.APIKey) > 0 {
req.Header.Set(apiKeyHeader, d.config.APIKey)
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
}
err = checkResponse(resp)
if err != nil {
return err
}
if v == nil {
return nil
}
raw, err := readBody(resp)
if err != nil {
return fmt.Errorf("failed to read body: %v", err)
}
if len(raw) > 0 {
err = json.Unmarshal(raw, v)
if err != nil {
return fmt.Errorf("unmarshaling error: %v: %s", err, string(raw))
}
}
return nil
}
func checkResponse(resp *http.Response) error {
if resp.StatusCode == 404 && resp.Request.Method == http.MethodGet {
return nil
}
if resp.StatusCode >= 400 {
data, err := readBody(resp)
if err != nil {
return fmt.Errorf("%d [%s] request failed: %v", resp.StatusCode, http.StatusText(resp.StatusCode), err)
}
message := &apiResponse{}
err = json.Unmarshal(data, message)
if err != nil {
return fmt.Errorf("%d [%s] request failed: %v: %s", resp.StatusCode, http.StatusText(resp.StatusCode), err, data)
}
return fmt.Errorf("%d [%s] request failed: %s", resp.StatusCode, http.StatusText(resp.StatusCode), message.Message)
}
return nil
}
func readBody(resp *http.Response) ([]byte, error) {
if resp.Body == nil {
return nil, fmt.Errorf("response body is nil")
}
defer resp.Body.Close()
rawBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return rawBody, nil
}

View file

@ -0,0 +1,167 @@
// Package gandiv5 implements a DNS provider for solving the DNS-01 challenge using Gandi LiveDNS api.
package gandiv5
import (
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Gandi API reference: http://doc.livedns.gandi.net/
const (
// defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp.
defaultBaseURL = "https://dns.api.gandi.net/api/v5"
minTTL = 300
)
// inProgressInfo contains information about an in-progress challenge
type inProgressInfo struct {
fieldName string
authZone string
}
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GANDIV5_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GANDIV5_PROPAGATION_TIMEOUT", 20*time.Minute),
PollingInterval: env.GetOrDefaultSecond("GANDIV5_POLLING_INTERVAL", 20*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GANDIV5_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses Gandi's LiveDNS
// API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
inProgressFQDNs map[string]inProgressInfo
inProgressMu sync.Mutex
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden during tests.
findZoneByFqdn func(fqdn string) (string, error)
}
// NewDNSProvider returns a DNSProvider instance configured for Gandi.
// Credentials must be passed in the environment variable: GANDIV5_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("GANDIV5_API_KEY")
if err != nil {
return nil, fmt.Errorf("gandi: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["GANDIV5_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Gandi.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("gandiv5: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, fmt.Errorf("gandiv5: no API Key given")
}
if config.BaseURL == "" {
config.BaseURL = defaultBaseURL
}
if config.TTL < minTTL {
return nil, fmt.Errorf("gandiv5: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
return &DNSProvider{
config: config,
inProgressFQDNs: make(map[string]inProgressInfo),
findZoneByFqdn: dns01.FindZoneByFqdn,
}, nil
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
// find authZone
authZone, err := d.findZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("gandiv5: findZoneByFqdn failure: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf("gandiv5: 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
err = d.addTXTRecord(dns01.UnFqdn(authZone), name, value, d.config.TTL)
if err != nil {
return err
}
// save data necessary for CleanUp
d.inProgressFQDNs[fqdn] = inProgressInfo{
authZone: authZone,
fieldName: name,
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
// acquire lock and retrieve authZone
d.inProgressMu.Lock()
defer d.inProgressMu.Unlock()
if _, ok := d.inProgressFQDNs[fqdn]; !ok {
// if there is no cleanup information then just return
return nil
}
fieldName := d.inProgressFQDNs[fqdn].fieldName
authZone := d.inProgressFQDNs[fqdn].authZone
delete(d.inProgressFQDNs, fqdn)
// delete TXT record from authZone
err := d.deleteTXTRecord(dns01.UnFqdn(authZone), fieldName)
if err != nil {
return fmt.Errorf("gandiv5: %v", 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 Gandi.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,327 @@
// Package gcloud implements a DNS provider for solving the DNS-01 challenge using Google Cloud DNS.
package gcloud
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
"github.com/go-acme/lego/platform/wait"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
)
const (
changeStatusDone = "done"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Debug bool
Project string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
Debug: env.GetOrDefaultBool("GCE_DEBUG", false),
TTL: env.GetOrDefaultInt("GCE_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("GCE_PROPAGATION_TIMEOUT", 180*time.Second),
PollingInterval: env.GetOrDefaultSecond("GCE_POLLING_INTERVAL", 5*time.Second),
}
}
// DNSProvider is an implementation of the DNSProvider interface.
type DNSProvider struct {
config *Config
client *dns.Service
}
// NewDNSProvider returns a DNSProvider instance configured for Google Cloud DNS.
// Project name must be passed in the environment variable: GCE_PROJECT.
// A Service Account can be passed in the environment variable: GCE_SERVICE_ACCOUNT
// or by specifying the keyfile location: GCE_SERVICE_ACCOUNT_FILE
func NewDNSProvider() (*DNSProvider, error) {
// Use a service account file if specified via environment variable.
if saKey := env.GetOrFile("GCE_SERVICE_ACCOUNT"); len(saKey) > 0 {
return NewDNSProviderServiceAccountKey([]byte(saKey))
}
// Use default credentials.
project := env.GetOrDefaultString("GCE_PROJECT", "")
return NewDNSProviderCredentials(project)
}
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderCredentials(project string) (*DNSProvider, error) {
if project == "" {
return nil, fmt.Errorf("googlecloud: project name missing")
}
client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
if err != nil {
return nil, fmt.Errorf("googlecloud: unable to get Google Cloud client: %v", err)
}
config := NewDefaultConfig()
config.Project = project
config.HTTPClient = client
return NewDNSProviderConfig(config)
}
// NewDNSProviderServiceAccountKey uses the supplied service account JSON
// to return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderServiceAccountKey(saKey []byte) (*DNSProvider, error) {
if len(saKey) == 0 {
return nil, fmt.Errorf("googlecloud: Service Account is missing")
}
// If GCE_PROJECT is non-empty it overrides the project in the service
// account file.
project := env.GetOrDefaultString("GCE_PROJECT", "")
if project == "" {
// read project id from service account file
var datJSON struct {
ProjectID string `json:"project_id"`
}
err := json.Unmarshal(saKey, &datJSON)
if err != nil || datJSON.ProjectID == "" {
return nil, fmt.Errorf("googlecloud: project ID not found in Google Cloud Service Account file")
}
project = datJSON.ProjectID
}
conf, err := google.JWTConfigFromJSON(saKey, dns.NdevClouddnsReadwriteScope)
if err != nil {
return nil, fmt.Errorf("googlecloud: unable to acquire config: %v", err)
}
client := conf.Client(context.Background())
config := NewDefaultConfig()
config.Project = project
config.HTTPClient = client
return NewDNSProviderConfig(config)
}
// NewDNSProviderServiceAccount uses the supplied service account JSON file
// to return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderServiceAccount(saFile string) (*DNSProvider, error) {
if saFile == "" {
return nil, fmt.Errorf("googlecloud: Service Account file missing")
}
saKey, err := ioutil.ReadFile(saFile)
if err != nil {
return nil, fmt.Errorf("googlecloud: unable to read Service Account file: %v", err)
}
return NewDNSProviderServiceAccountKey(saKey)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Google Cloud DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("googlecloud: the configuration of the DNS provider is nil")
}
svc, err := dns.New(config.HTTPClient)
if err != nil {
return nil, fmt.Errorf("googlecloud: unable to create Google Cloud DNS service: %v", err)
}
return &DNSProvider{config: config, client: svc}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZone(domain)
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
// Look for existing records.
existingRrSet, err := d.findTxtRecords(zone, fqdn)
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
for _, rrSet := range existingRrSet {
var rrd []string
for _, rr := range rrSet.Rrdatas {
data := mustUnquote(rr)
rrd = append(rrd, data)
if data == value {
log.Printf("skip: the record already exists: %s", value)
return nil
}
}
rrSet.Rrdatas = rrd
}
// Attempt to delete the existing records before adding the new one.
if len(existingRrSet) > 0 {
if err = d.applyChanges(zone, &dns.Change{Deletions: existingRrSet}); err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
}
rec := &dns.ResourceRecordSet{
Name: fqdn,
Rrdatas: []string{value},
Ttl: int64(d.config.TTL),
Type: "TXT",
}
// Append existing TXT record data to the new TXT record data
for _, rrSet := range existingRrSet {
for _, rr := range rrSet.Rrdatas {
if rr != value {
rec.Rrdatas = append(rec.Rrdatas, rr)
}
}
}
change := &dns.Change{
Additions: []*dns.ResourceRecordSet{rec},
}
if err = d.applyChanges(zone, change); err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
return nil
}
func (d *DNSProvider) applyChanges(zone string, change *dns.Change) error {
if d.config.Debug {
data, _ := json.Marshal(change)
log.Printf("change (Create): %s", string(data))
}
chg, err := d.client.Changes.Create(d.config.Project, zone, change).Do()
if err != nil {
if v, ok := err.(*googleapi.Error); ok {
if v.Code == http.StatusNotFound {
return nil
}
}
data, _ := json.Marshal(change)
return fmt.Errorf("failed to perform changes [zone %s, change %s]: %v", zone, string(data), err)
}
if chg.Status == changeStatusDone {
return nil
}
chgID := chg.Id
// wait for change to be acknowledged
return wait.For("apply change", 30*time.Second, 3*time.Second, func() (bool, error) {
if d.config.Debug {
data, _ := json.Marshal(change)
log.Printf("change (Get): %s", string(data))
}
chg, err = d.client.Changes.Get(d.config.Project, zone, chgID).Do()
if err != nil {
data, _ := json.Marshal(change)
return false, fmt.Errorf("failed to get changes [zone %s, change %s]: %v", zone, string(data), err)
}
if chg.Status == changeStatusDone {
return true, nil
}
return false, fmt.Errorf("status: %s", chg.Status)
})
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZone(domain)
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
records, err := d.findTxtRecords(zone, fqdn)
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
if len(records) == 0 {
return nil
}
_, err = d.client.Changes.Create(d.config.Project, zone, &dns.Change{Deletions: records}).Do()
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
return nil
}
// Timeout customizes the timeout values used by the ACME package for checking
// DNS record validity.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// getHostedZone returns the managed-zone
func (d *DNSProvider) getHostedZone(domain string) (string, error) {
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
if err != nil {
return "", err
}
zones, err := d.client.ManagedZones.
List(d.config.Project).
DnsName(authZone).
Do()
if err != nil {
return "", fmt.Errorf("API call failed: %v", err)
}
if len(zones.ManagedZones) == 0 {
return "", fmt.Errorf("no matching domain found for domain %s", authZone)
}
return zones.ManagedZones[0].Name, nil
}
func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
recs, err := d.client.ResourceRecordSets.List(d.config.Project, zone).Name(fqdn).Type("TXT").Do()
if err != nil {
return nil, err
}
return recs.Rrsets, nil
}
func mustUnquote(raw string) string {
clean, err := strconv.Unquote(raw)
if err != nil {
return raw
}
return clean
}

View file

@ -0,0 +1,91 @@
package glesys
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/go-acme/lego/log"
)
// 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"`
}
func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) {
response, err := d.sendRequest(http.MethodPost, "addrecord", addRecordRequest{
DomainName: domain,
Host: name,
Type: "TXT",
Data: value,
TTL: ttl,
})
if response != nil && response.Response.Status.Code == http.StatusOK {
log.Infof("[%s]: Successfully created record id %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(http.MethodPost, "deleterecord", deleteRecordRequest{
RecordID: recordid,
})
if response != nil && response.Response.Status.Code == 200 {
log.Infof("[%s]: Successfully deleted record id %d", fqdn, recordid)
}
return err
}
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
url := fmt.Sprintf("%s/%s", defaultBaseURL, 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.config.APIUser, d.config.APIKey)
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode)
}
var response responseStruct
err = json.NewDecoder(resp.Body).Decode(&response)
return &response, err
}

View file

@ -0,0 +1,146 @@
// Package glesys implements a DNS provider for solving the DNS-01 challenge using GleSYS api.
package glesys
import (
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const (
// defaultBaseURL is the GleSYS API endpoint used by Present and CleanUp.
defaultBaseURL = "https://api.glesys.com/domain"
minTTL = 60
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIUser string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GLESYS_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GLESYS_PROPAGATION_TIMEOUT", 20*time.Minute),
PollingInterval: env.GetOrDefaultSecond("GLESYS_POLLING_INTERVAL", 20*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GLESYS_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses GleSYS
// API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
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) {
values, err := env.Get("GLESYS_API_USER", "GLESYS_API_KEY")
if err != nil {
return nil, fmt.Errorf("glesys: %v", err)
}
config := NewDefaultConfig()
config.APIUser = values["GLESYS_API_USER"]
config.APIKey = values["GLESYS_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for GleSYS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("glesys: the configuration of the DNS provider is nil")
}
if config.APIUser == "" || config.APIKey == "" {
return nil, fmt.Errorf("glesys: incomplete credentials provided")
}
if config.TTL < minTTL {
return nil, fmt.Errorf("glesys: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
return &DNSProvider{
config: config,
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 := dns01.GetRecord(domain, keyAuth)
// find authZone
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("glesys: findZoneByFqdn failure: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf("glesys: 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, dns01.UnFqdn(authZone), name, value, d.config.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, _ := dns01.GetRecord(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
return d.deleteTXTRecord(domain, recordID)
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,53 @@
package godaddy
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
)
// DNSRecord a DNS record
type DNSRecord struct {
Type string `json:"type"`
Name string `json:"name"`
Data string `json:"data"`
Priority int `json:"priority,omitempty"`
TTL int `json:"ttl,omitempty"`
}
func (d *DNSProvider) updateRecords(records []DNSRecord, domainZone string, recordName string) error {
body, err := json.Marshal(records)
if err != nil {
return err
}
var resp *http.Response
resp, err = d.makeRequest(http.MethodPut, fmt.Sprintf("/v1/domains/%s/records/TXT/%s", domainZone, recordName), bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("could not create record %v; Status: %v; Body: %s", string(body), resp.StatusCode, string(bodyBytes))
}
return nil
}
func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", defaultBaseURL, uri), body)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.config.APIKey, d.config.APISecret))
return d.config.HTTPClient.Do(req)
}

View file

@ -0,0 +1,151 @@
// Package godaddy implements a DNS provider for solving the DNS-01 challenge using godaddy DNS.
package godaddy
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const (
// defaultBaseURL represents the API endpoint to call.
defaultBaseURL = "https://api.godaddy.com"
minTTL = 600
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
APISecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
SequenceInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("GODADDY_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("GODADDY_PROPAGATION_TIMEOUT", 120*time.Second),
PollingInterval: env.GetOrDefaultSecond("GODADDY_POLLING_INTERVAL", 2*time.Second),
SequenceInterval: env.GetOrDefaultSecond("GODADDY_SEQUENCE_INTERVAL", dns01.DefaultPropagationTimeout),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("GODADDY_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for godaddy.
// Credentials must be passed in the environment variables:
// GODADDY_API_KEY and GODADDY_API_SECRET.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("GODADDY_API_KEY", "GODADDY_API_SECRET")
if err != nil {
return nil, fmt.Errorf("godaddy: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["GODADDY_API_KEY"]
config.APISecret = values["GODADDY_API_SECRET"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for godaddy.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("godaddy: the configuration of the DNS provider is nil")
}
if config.APIKey == "" || config.APISecret == "" {
return nil, fmt.Errorf("godaddy: credentials missing")
}
if config.TTL < minTTL {
return nil, fmt.Errorf("godaddy: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
return &DNSProvider{config: config}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
domainZone, err := d.getZone(fqdn)
if err != nil {
return err
}
recordName := d.extractRecordName(fqdn, domainZone)
rec := []DNSRecord{
{
Type: "TXT",
Name: recordName,
Data: value,
TTL: d.config.TTL,
},
}
return d.updateRecords(rec, domainZone, recordName)
}
// CleanUp sets null value in the TXT DNS record as GoDaddy has no proper DELETE record method
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
domainZone, err := d.getZone(fqdn)
if err != nil {
return err
}
recordName := d.extractRecordName(fqdn, domainZone)
rec := []DNSRecord{
{
Type: "TXT",
Name: recordName,
Data: "null",
},
}
return d.updateRecords(rec, domainZone, recordName)
}
// Sequential All DNS challenges for this provider will be resolved sequentially.
// Returns the interval between each iteration.
func (d *DNSProvider) Sequential() time.Duration {
return d.config.SequenceInterval
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
name := dns01.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}
func (d *DNSProvider) getZone(fqdn string) (string, error) {
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return "", err
}
return dns01.UnFqdn(authZone), nil
}

View file

@ -0,0 +1,127 @@
package hostingde
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/cenkalti/backoff"
)
const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json"
// https://www.hosting.de/api/?json#list-zoneconfigs
func (d *DNSProvider) listZoneConfigs(findRequest ZoneConfigsFindRequest) (*ZoneConfigsFindResponse, error) {
uri := defaultBaseURL + "/zoneConfigsFind"
findResponse := &ZoneConfigsFindResponse{}
rawResp, err := d.post(uri, findRequest, findResponse)
if err != nil {
return nil, err
}
if len(findResponse.Response.Data) == 0 {
return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(uri, rawResp))
}
if findResponse.Status != "success" && findResponse.Status != "pending" {
return findResponse, errors.New(toUnreadableBodyMessage(uri, rawResp))
}
return findResponse, nil
}
// https://www.hosting.de/api/?json#updating-zones
func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateResponse, error) {
uri := defaultBaseURL + "/zoneUpdate"
// but we'll need the ID later to delete the record
updateResponse := &ZoneUpdateResponse{}
rawResp, err := d.post(uri, updateRequest, updateResponse)
if err != nil {
return nil, err
}
if updateResponse.Status != "success" && updateResponse.Status != "pending" {
return nil, errors.New(toUnreadableBodyMessage(uri, rawResp))
}
return updateResponse, nil
}
func (d *DNSProvider) getZone(findRequest ZoneConfigsFindRequest) (*ZoneConfig, error) {
ctx, cancel := context.WithCancel(context.Background())
var zoneConfig *ZoneConfig
operation := func() error {
findResponse, err := d.listZoneConfigs(findRequest)
if err != nil {
cancel()
return err
}
if findResponse.Response.Data[0].Status != "active" {
return fmt.Errorf("unexpected status: %q", findResponse.Response.Data[0].Status)
}
zoneConfig = &findResponse.Response.Data[0]
return nil
}
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = 3 * time.Second
bo.MaxInterval = 10 * bo.InitialInterval
bo.MaxElapsedTime = 100 * bo.InitialInterval
// retry in case the zone was edited recently and is not yet active
err := backoff.Retry(operation, backoff.WithContext(bo, ctx))
if err != nil {
return nil, err
}
return zoneConfig, nil
}
func (d *DNSProvider) post(uri string, request interface{}, response interface{}) ([]byte, error) {
body, err := json.Marshal(request)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(body))
if err != nil {
return nil, err
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error querying API: %v", err)
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.New(toUnreadableBodyMessage(uri, content))
}
err = json.Unmarshal(content, response)
if err != nil {
return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(uri, content))
}
return content, nil
}
func toUnreadableBodyMessage(uri string, rawBody []byte) string {
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", uri, string(rawBody))
}

View file

@ -0,0 +1,183 @@
// Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de.
package hostingde
import (
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
ZoneName string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("HOSTINGDE_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("HOSTINGDE_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("HOSTINGDE_POLLING_INTERVAL", 2*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("HOSTINGDE_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
recordIDs map[string]string
recordIDsMu sync.Mutex
}
// NewDNSProvider returns a DNSProvider instance configured for hosting.de.
// Credentials must be passed in the environment variables:
// HOSTINGDE_ZONE_NAME and HOSTINGDE_API_KEY
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("HOSTINGDE_API_KEY", "HOSTINGDE_ZONE_NAME")
if err != nil {
return nil, fmt.Errorf("hostingde: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["HOSTINGDE_API_KEY"]
config.ZoneName = values["HOSTINGDE_ZONE_NAME"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for hosting.de.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("hostingde: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, errors.New("hostingde: API key missing")
}
if config.ZoneName == "" {
return nil, errors.New("hostingde: Zone Name missing")
}
return &DNSProvider{
config: config,
recordIDs: make(map[string]string),
}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
// get the ZoneConfig for that domain
zonesFind := ZoneConfigsFindRequest{
Filter: Filter{
Field: "zoneName",
Value: domain,
},
Limit: 1,
Page: 1,
}
zonesFind.AuthToken = d.config.APIKey
zoneConfig, err := d.getZone(zonesFind)
if err != nil {
return fmt.Errorf("hostingde: %v", err)
}
zoneConfig.Name = d.config.ZoneName
rec := []DNSRecord{{
Type: "TXT",
Name: dns01.UnFqdn(fqdn),
Content: value,
TTL: d.config.TTL,
}}
req := ZoneUpdateRequest{
ZoneConfig: *zoneConfig,
RecordsToAdd: rec,
}
req.AuthToken = d.config.APIKey
resp, err := d.updateZone(req)
if err != nil {
return fmt.Errorf("hostingde: %v", err)
}
for _, record := range resp.Response.Records {
if record.Name == dns01.UnFqdn(fqdn) && record.Content == fmt.Sprintf(`"%s"`, value) {
d.recordIDsMu.Lock()
d.recordIDs[fqdn] = record.ID
d.recordIDsMu.Unlock()
}
}
if d.recordIDs[fqdn] == "" {
return fmt.Errorf("hostingde: error getting ID of just created record, for domain %s", domain)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
rec := []DNSRecord{{
Type: "TXT",
Name: dns01.UnFqdn(fqdn),
Content: `"` + value + `"`,
}}
// get the ZoneConfig for that domain
zonesFind := ZoneConfigsFindRequest{
Filter: Filter{
Field: "zoneName",
Value: domain,
},
Limit: 1,
Page: 1,
}
zonesFind.AuthToken = d.config.APIKey
zoneConfig, err := d.getZone(zonesFind)
if err != nil {
return fmt.Errorf("hostingde: %v", err)
}
zoneConfig.Name = d.config.ZoneName
req := ZoneUpdateRequest{
ZoneConfig: *zoneConfig,
RecordsToDelete: rec,
}
req.AuthToken = d.config.APIKey
// Delete record ID from map
d.recordIDsMu.Lock()
delete(d.recordIDs, fqdn)
d.recordIDsMu.Unlock()
_, err = d.updateZone(req)
if err != nil {
return fmt.Errorf("hostingde: %v", err)
}
return nil
}

View file

@ -0,0 +1,139 @@
package hostingde
import "encoding/json"
// APIError represents an error in an API response.
// https://www.hosting.de/api/?json#warnings-and-errors
type APIError struct {
Code int `json:"code"`
ContextObject string `json:"contextObject"`
ContextPath string `json:"contextPath"`
Details []string `json:"details"`
Text string `json:"text"`
Value string `json:"value"`
}
// Filter is used to filter FindRequests to the API.
// https://www.hosting.de/api/?json#filter-object
type Filter struct {
Field string `json:"field"`
Value string `json:"value"`
}
// Sort is used to sort FindRequests from the API.
// https://www.hosting.de/api/?json#filtering-and-sorting
type Sort struct {
Field string `json:"zoneName"`
Order string `json:"order"`
}
// Metadata represents the metadata in an API response.
// https://www.hosting.de/api/?json#metadata-object
type Metadata struct {
ClientTransactionID string `json:"clientTransactionId"`
ServerTransactionID string `json:"serverTransactionId"`
}
// ZoneConfig The ZoneConfig object defines a zone.
// https://www.hosting.de/api/?json#the-zoneconfig-object
type ZoneConfig struct {
ID string `json:"id"`
AccountID string `json:"accountId"`
Status string `json:"status"`
Name string `json:"name"`
NameUnicode string `json:"nameUnicode"`
MasterIP string `json:"masterIp"`
Type string `json:"type"`
EMailAddress string `json:"emailAddress"`
ZoneTransferWhitelist []string `json:"zoneTransferWhitelist"`
LastChangeDate string `json:"lastChangeDate"`
DNSServerGroupID string `json:"dnsServerGroupId"`
DNSSecMode string `json:"dnsSecMode"`
SOAValues *SOAValues `json:"soaValues,omitempty"`
TemplateValues json.RawMessage `json:"templateValues,omitempty"`
}
// SOAValues The SOA values object contains the time (seconds) used in a zones SOA record.
// https://www.hosting.de/api/?json#the-soa-values-object
type SOAValues struct {
Refresh int `json:"refresh"`
Retry int `json:"retry"`
Expire int `json:"expire"`
TTL int `json:"ttl"`
NegativeTTL int `json:"negativeTtl"`
}
// DNSRecord The DNS Record object is part of a zone. It is used to manage DNS resource records.
// https://www.hosting.de/api/?json#the-record-object
type DNSRecord struct {
ID string `json:"id,omitempty"`
ZoneID string `json:"zoneId,omitempty"`
RecordTemplateID string `json:"recordTemplateId,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Content string `json:"content,omitempty"`
TTL int `json:"ttl,omitempty"`
Priority int `json:"priority,omitempty"`
LastChangeDate string `json:"lastChangeDate,omitempty"`
}
// Zone The Zone Object.
// https://www.hosting.de/api/?json#the-zone-object
type Zone struct {
Records []DNSRecord `json:"records"`
ZoneConfig ZoneConfig `json:"zoneConfig"`
}
// ZoneUpdateRequest represents a API ZoneUpdate request.
// https://www.hosting.de/api/?json#updating-zones
type ZoneUpdateRequest struct {
BaseRequest
ZoneConfig `json:"zoneConfig"`
RecordsToAdd []DNSRecord `json:"recordsToAdd"`
RecordsToDelete []DNSRecord `json:"recordsToDelete"`
}
// ZoneUpdateResponse represents a response from the API.
// https://www.hosting.de/api/?json#updating-zones
type ZoneUpdateResponse struct {
BaseResponse
Response Zone `json:"response"`
}
// ZoneConfigsFindRequest represents a API ZonesFind request.
// https://www.hosting.de/api/?json#list-zoneconfigs
type ZoneConfigsFindRequest struct {
BaseRequest
Filter Filter `json:"filter"`
Limit int `json:"limit"`
Page int `json:"page"`
Sort *Sort `json:"sort,omitempty"`
}
// ZoneConfigsFindResponse represents the API response for ZoneConfigsFind.
// https://www.hosting.de/api/?json#list-zoneconfigs
type ZoneConfigsFindResponse struct {
BaseResponse
Response struct {
Limit int `json:"limit"`
Page int `json:"page"`
TotalEntries int `json:"totalEntries"`
TotalPages int `json:"totalPages"`
Type string `json:"type"`
Data []ZoneConfig `json:"data"`
} `json:"response"`
}
// BaseResponse Common response struct.
// https://www.hosting.de/api/?json#responses
type BaseResponse struct {
Errors []APIError `json:"errors"`
Metadata Metadata `json:"metadata"`
Warnings []string `json:"warnings"`
Status string `json:"status"`
}
// BaseRequest Common request struct.
type BaseRequest struct {
AuthToken string `json:"authToken"`
}

View file

@ -0,0 +1,195 @@
// Package httpreq implements a DNS provider for solving the DNS-01 challenge through a HTTP server.
package httpreq
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
type message struct {
FQDN string `json:"fqdn"`
Value string `json:"value"`
}
type messageRaw struct {
Domain string `json:"domain"`
Token string `json:"token"`
KeyAuth string `json:"keyAuth"`
}
// Config is used to configure the creation of the DNSProvider
type Config struct {
Endpoint *url.URL
Mode string
Username string
Password string
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("HTTPREQ_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("HTTPREQ_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("HTTPREQ_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider describes a provider for acme-proxy
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a DNSProvider instance.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("HTTPREQ_ENDPOINT")
if err != nil {
return nil, fmt.Errorf("httpreq: %v", err)
}
endpoint, err := url.Parse(values["HTTPREQ_ENDPOINT"])
if err != nil {
return nil, fmt.Errorf("httpreq: %v", err)
}
config := NewDefaultConfig()
config.Mode = os.Getenv("HTTPREQ_MODE")
config.Username = os.Getenv("HTTPREQ_USERNAME")
config.Password = os.Getenv("HTTPREQ_PASSWORD")
config.Endpoint = endpoint
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider .
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("httpreq: the configuration of the DNS provider is nil")
}
if config.Endpoint == nil {
return nil, errors.New("httpreq: the endpoint is missing")
}
return &DNSProvider{config: config}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
if d.config.Mode == "RAW" {
msg := &messageRaw{
Domain: domain,
Token: token,
KeyAuth: keyAuth,
}
err := d.doPost("/present", msg)
if err != nil {
return fmt.Errorf("httpreq: %v", err)
}
return nil
}
fqdn, value := dns01.GetRecord(domain, keyAuth)
msg := &message{
FQDN: fqdn,
Value: value,
}
err := d.doPost("/present", msg)
if err != nil {
return fmt.Errorf("httpreq: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
if d.config.Mode == "RAW" {
msg := &messageRaw{
Domain: domain,
Token: token,
KeyAuth: keyAuth,
}
err := d.doPost("/cleanup", msg)
if err != nil {
return fmt.Errorf("httpreq: %v", err)
}
return nil
}
fqdn, value := dns01.GetRecord(domain, keyAuth)
msg := &message{
FQDN: fqdn,
Value: value,
}
err := d.doPost("/cleanup", msg)
if err != nil {
return fmt.Errorf("httpreq: %v", err)
}
return nil
}
func (d *DNSProvider) doPost(uri string, msg interface{}) error {
reqBody := &bytes.Buffer{}
err := json.NewEncoder(reqBody).Encode(msg)
if err != nil {
return err
}
newURI := path.Join(d.config.Endpoint.EscapedPath(), uri)
endpoint, err := d.config.Endpoint.Parse(newURI)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, endpoint.String(), reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if len(d.config.Username) > 0 && len(d.config.Password) > 0 {
req.SetBasicAuth(d.config.Username, d.config.Password)
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%d: failed to read response body: %v", resp.StatusCode, err)
}
return fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
}
return nil
}

239
vendor/github.com/go-acme/lego/providers/dns/iij/iij.go generated vendored Normal file
View file

@ -0,0 +1,239 @@
// Package iij implements a DNS provider for solving the DNS-01 challenge using IIJ DNS.
package iij
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/iij/doapi"
"github.com/iij/doapi/protocol"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
AccessKey string
SecretKey string
DoServiceCode string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("IIJ_TTL", 300),
PropagationTimeout: env.GetOrDefaultSecond("IIJ_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("IIJ_POLLING_INTERVAL", 4*time.Second),
}
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
api *doapi.API
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for IIJ DO
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("IIJ_API_ACCESS_KEY", "IIJ_API_SECRET_KEY", "IIJ_DO_SERVICE_CODE")
if err != nil {
return nil, fmt.Errorf("iij: %v", err)
}
config := NewDefaultConfig()
config.AccessKey = values["IIJ_API_ACCESS_KEY"]
config.SecretKey = values["IIJ_API_SECRET_KEY"]
config.DoServiceCode = values["IIJ_DO_SERVICE_CODE"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig takes a given config
// and returns a custom configured DNSProvider instance
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config.SecretKey == "" || config.AccessKey == "" || config.DoServiceCode == "" {
return nil, fmt.Errorf("iij: credentials missing")
}
return &DNSProvider{
api: doapi.NewAPI(config.AccessKey, config.SecretKey),
config: config,
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, value := dns01.GetRecord(domain, keyAuth)
err := d.addTxtRecord(domain, value)
if err != nil {
return fmt.Errorf("iij: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
_, value := dns01.GetRecord(domain, keyAuth)
err := d.deleteTxtRecord(domain, value)
if err != nil {
return fmt.Errorf("iij: %v", err)
}
return nil
}
func (d *DNSProvider) addTxtRecord(domain, value string) error {
zones, err := d.listZones()
if err != nil {
return err
}
owner, zone, err := splitDomain(domain, zones)
if err != nil {
return err
}
request := protocol.RecordAdd{
DoServiceCode: d.config.DoServiceCode,
ZoneName: zone,
Owner: owner,
TTL: strconv.Itoa(d.config.TTL),
RecordType: "TXT",
RData: value,
}
response := &protocol.RecordAddResponse{}
if err := doapi.Call(*d.api, request, response); err != nil {
return err
}
return d.commit()
}
func (d *DNSProvider) deleteTxtRecord(domain, value string) error {
zones, err := d.listZones()
if err != nil {
return err
}
owner, zone, err := splitDomain(domain, zones)
if err != nil {
return err
}
id, err := d.findTxtRecord(owner, zone, value)
if err != nil {
return err
}
request := protocol.RecordDelete{
DoServiceCode: d.config.DoServiceCode,
ZoneName: zone,
RecordID: id,
}
response := &protocol.RecordDeleteResponse{}
if err := doapi.Call(*d.api, request, response); err != nil {
return err
}
return d.commit()
}
func (d *DNSProvider) commit() error {
request := protocol.Commit{
DoServiceCode: d.config.DoServiceCode,
}
response := &protocol.CommitResponse{}
return doapi.Call(*d.api, request, response)
}
func (d *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) {
request := protocol.RecordListGet{
DoServiceCode: d.config.DoServiceCode,
ZoneName: zone,
}
response := &protocol.RecordListGetResponse{}
if err := doapi.Call(*d.api, request, response); err != nil {
return "", err
}
var id string
for _, record := range response.RecordList {
if record.Owner == owner && record.RecordType == "TXT" && record.RData == "\""+value+"\"" {
id = record.Id
}
}
if id == "" {
return "", fmt.Errorf("%s record in %s not found", owner, zone)
}
return id, nil
}
func (d *DNSProvider) listZones() ([]string, error) {
request := protocol.ZoneListGet{
DoServiceCode: d.config.DoServiceCode,
}
response := &protocol.ZoneListGetResponse{}
if err := doapi.Call(*d.api, request, response); err != nil {
return nil, err
}
return response.ZoneList, nil
}
func splitDomain(domain string, zones []string) (string, string, error) {
parts := strings.Split(strings.Trim(domain, "."), ".")
var owner string
var zone string
for i := 0; i < len(parts)-1; i++ {
zone = strings.Join(parts[i:], ".")
if zoneContains(zone, zones) {
baseOwner := strings.Join(parts[0:i], ".")
if len(baseOwner) > 0 {
baseOwner = "." + baseOwner
}
owner = "_acme-challenge" + baseOwner
break
}
}
if len(owner) == 0 {
return "", "", fmt.Errorf("%s not found", domain)
}
return owner, zone, nil
}
func zoneContains(zone string, zones []string) bool {
for _, z := range zones {
if zone == z {
return true
}
}
return false
}

View file

@ -0,0 +1,166 @@
// Package inwx implements a DNS provider for solving the DNS-01 challenge using inwx dom robot
package inwx
import (
"errors"
"fmt"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
"github.com/nrdcg/goinwx"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Username string
Password string
Sandbox bool
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("INWX_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("INWX_POLLING_INTERVAL", dns01.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("INWX_TTL", 300),
Sandbox: env.GetOrDefaultBool("INWX_SANDBOX", false),
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
client *goinwx.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
// Credentials must be passed in the environment variables:
// INWX_USERNAME and INWX_PASSWORD.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("INWX_USERNAME", "INWX_PASSWORD")
if err != nil {
return nil, fmt.Errorf("inwx: %v", err)
}
config := NewDefaultConfig()
config.Username = values["INWX_USERNAME"]
config.Password = values["INWX_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("inwx: the configuration of the DNS provider is nil")
}
if config.Username == "" || config.Password == "" {
return nil, fmt.Errorf("inwx: credentials missing")
}
if config.Sandbox {
log.Infof("inwx: sandbox mode is enabled")
}
client := goinwx.NewClient(config.Username, config.Password, &goinwx.ClientOptions{Sandbox: config.Sandbox})
return &DNSProvider{config: config, client: client}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("inwx: %v", err)
}
err = d.client.Account.Login()
if err != nil {
return fmt.Errorf("inwx: %v", err)
}
defer func() {
errL := d.client.Account.Logout()
if errL != nil {
log.Infof("inwx: failed to logout: %v", errL)
}
}()
var request = &goinwx.NameserverRecordRequest{
Domain: dns01.UnFqdn(authZone),
Name: dns01.UnFqdn(fqdn),
Type: "TXT",
Content: value,
TTL: d.config.TTL,
}
_, err = d.client.Nameservers.CreateRecord(request)
if err != nil {
switch er := err.(type) {
case *goinwx.ErrorResponse:
if er.Message == "Object exists" {
return nil
}
return fmt.Errorf("inwx: %v", err)
default:
return fmt.Errorf("inwx: %v", err)
}
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("inwx: %v", err)
}
err = d.client.Account.Login()
if err != nil {
return fmt.Errorf("inwx: %v", err)
}
defer func() {
errL := d.client.Account.Logout()
if errL != nil {
log.Infof("inwx: failed to logout: %v", errL)
}
}()
response, err := d.client.Nameservers.Info(&goinwx.NameserverInfoRequest{
Domain: dns01.UnFqdn(authZone),
Name: dns01.UnFqdn(fqdn),
Type: "TXT",
})
if err != nil {
return fmt.Errorf("inwx: %v", err)
}
var lastErr error
for _, record := range response.Records {
err = d.client.Nameservers.DeleteRecord(record.ID)
if err != nil {
lastErr = fmt.Errorf("inwx: %v", err)
}
}
return lastErr
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,153 @@
// Package lightsail implements a DNS provider for solving the DNS-01 challenge using AWS Lightsail DNS.
package lightsail
import (
"errors"
"fmt"
"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/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const (
maxRetries = 5
)
// 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 (c 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
}
// Config is used to configure the creation of the DNSProvider
type Config struct {
DNSZone string
Region string
PropagationTimeout time.Duration
PollingInterval time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
DNSZone: env.GetOrFile("DNS_ZONE"),
PropagationTimeout: env.GetOrDefaultSecond("LIGHTSAIL_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("LIGHTSAIL_POLLING_INTERVAL", dns01.DefaultPollingInterval),
Region: env.GetOrDefaultString("LIGHTSAIL_REGION", "us-east-1"),
}
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *lightsail.Lightsail
config *Config
}
// 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], [LIGHTSAIL_REGION]
// 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) {
return NewDNSProviderConfig(NewDefaultConfig())
}
// NewDNSProviderConfig return a DNSProvider instance configured for AWS Lightsail.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("lightsail: the configuration of the DNS provider is nil")
}
retryer := customRetryer{}
retryer.NumMaxRetries = maxRetries
conf := aws.NewConfig().WithRegion(config.Region)
sess, err := session.NewSession(request.WithRetryer(conf, retryer))
if err != nil {
return nil, err
}
return &DNSProvider{
config: config,
client: lightsail.New(sess),
}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
err := d.newTxtRecord(fqdn, `"`+value+`"`)
if err != nil {
return fmt.Errorf("lightsail: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
params := &lightsail.DeleteDomainEntryInput{
DomainName: aws.String(d.config.DNSZone),
DomainEntry: &lightsail.DomainEntry{
Name: aws.String(fqdn),
Type: aws.String("TXT"),
Target: aws.String(`"` + value + `"`),
},
}
_, err := d.client.DeleteDomainEntry(params)
if err != nil {
return fmt.Errorf("lightsail: %v", err)
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) newTxtRecord(fqdn string, value string) error {
params := &lightsail.CreateDomainEntryInput{
DomainName: aws.String(d.config.DNSZone),
DomainEntry: &lightsail.DomainEntry{
Name: aws.String(fqdn),
Target: aws.String(value),
Type: aws.String("TXT"),
},
}
_, err := d.client.CreateDomainEntry(params)
return err
}

View file

@ -0,0 +1,164 @@
// Package linode implements a DNS provider for solving the DNS-01 challenge using Linode DNS.
package linode
import (
"errors"
"fmt"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/timewasted/linode/dns"
)
const (
minTTL = 300
dnsUpdateFreqMins = 15
dnsUpdateFudgeSecs = 120
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
PollingInterval time.Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PollingInterval: env.GetOrDefaultSecond("LINODE_POLLING_INTERVAL", 15*time.Second),
TTL: env.GetOrDefaultInt("LINODE_TTL", minTTL),
}
}
type hostedZoneInfo struct {
domainID int
resourceName string
}
// DNSProvider implements the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *dns.DNS
}
// NewDNSProvider returns a DNSProvider instance configured for Linode.
// Credentials must be passed in the environment variable: LINODE_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("LINODE_API_KEY")
if err != nil {
return nil, fmt.Errorf("linode: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["LINODE_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Linode.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("linode: the configuration of the DNS provider is nil")
}
if len(config.APIKey) == 0 {
return nil, errors.New("linode: credentials missing")
}
if config.TTL < minTTL {
return nil, fmt.Errorf("linode: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
return &DNSProvider{
config: config,
client: dns.New(config.APIKey),
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
// Since Linode only updates their zone files every X minutes, we need
// to figure out how many minutes we have to wait until we hit the next
// interval of X. We then wait another couple of minutes, just to be
// safe. Hopefully at some point during all of this, the record will
// have propagated throughout Linode's network.
minsRemaining := dnsUpdateFreqMins - (time.Now().Minute() % dnsUpdateFreqMins)
timeout = (time.Duration(minsRemaining) * time.Minute) +
(minTTL * time.Second) +
(dnsUpdateFudgeSecs * time.Second)
interval = d.config.PollingInterval
return
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZoneInfo(fqdn)
if err != nil {
return err
}
if _, err = d.client.CreateDomainResourceTXT(zone.domainID, dns01.UnFqdn(fqdn), value, d.config.TTL); err != nil {
return err
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZoneInfo(fqdn)
if err != nil {
return err
}
// Get all TXT records for the specified domain.
resources, err := d.client.GetResourcesByType(zone.domainID, "TXT")
if err != nil {
return err
}
// Remove the specified resource, if it exists.
for _, resource := range resources {
if resource.Name == zone.resourceName && resource.Target == value {
resp, err := d.client.DeleteDomainResource(resource.DomainID, resource.ResourceID)
if err != nil {
return err
}
if resp.ResourceID != resource.ResourceID {
return errors.New("error deleting resource: resource IDs do not match")
}
break
}
}
return nil
}
func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
// Lookup the zone that handles the specified FQDN.
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return nil, err
}
resourceName := strings.TrimSuffix(fqdn, "."+authZone)
// Query the authority zone.
domain, err := d.client.GetDomain(dns01.UnFqdn(authZone))
if err != nil {
return nil, err
}
return &hostedZoneInfo{
domainID: domain.DomainID,
resourceName: resourceName,
}, nil
}

View file

@ -0,0 +1,190 @@
// Package linodev4 implements a DNS provider for solving the DNS-01 challenge using Linode DNS and Linode's APIv4
package linodev4
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/linode/linodego"
"golang.org/x/oauth2"
)
const (
minTTL = 300
dnsUpdateFreqMins = 15
dnsUpdateFudgeSecs = 120
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Token string
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PollingInterval: env.GetOrDefaultSecond("LINODE_POLLING_INTERVAL", 15*time.Second),
TTL: env.GetOrDefaultInt("LINODE_TTL", minTTL),
HTTPTimeout: env.GetOrDefaultSecond("LINODE_HTTP_TIMEOUT", 0),
}
}
type hostedZoneInfo struct {
domainID int
resourceName string
}
// DNSProvider implements the acme.ChallengeProvider interface.
type DNSProvider struct {
config *Config
client *linodego.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Linode.
// Credentials must be passed in the environment variable: LINODE_TOKEN.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("LINODE_TOKEN")
if err != nil {
return nil, fmt.Errorf("linodev4: %v", err)
}
config := NewDefaultConfig()
config.Token = values["LINODE_TOKEN"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Linode.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("linodev4: the configuration of the DNS provider is nil")
}
if len(config.Token) == 0 {
return nil, errors.New("linodev4: Linode Access Token missing")
}
if config.TTL < minTTL {
return nil, fmt.Errorf("linodev4: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.Token})
oauth2Client := &http.Client{
Timeout: config.HTTPTimeout,
Transport: &oauth2.Transport{
Source: tokenSource,
},
}
client := linodego.NewClient(oauth2Client)
client.SetUserAgent(fmt.Sprintf("lego-dns linodego/%s", linodego.Version))
return &DNSProvider{
config: config,
client: &client,
}, nil
}
// 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) {
// Since Linode only updates their zone files every X minutes, we need
// to figure out how many minutes we have to wait until we hit the next
// interval of X. We then wait another couple of minutes, just to be
// safe. Hopefully at some point during all of this, the record will
// have propagated throughout Linode's network.
minsRemaining := dnsUpdateFreqMins - (time.Now().Minute() % dnsUpdateFreqMins)
timeout = (time.Duration(minsRemaining) * time.Minute) +
(minTTL * time.Second) +
(dnsUpdateFudgeSecs * time.Second)
interval = d.config.PollingInterval
return
}
// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZoneInfo(fqdn)
if err != nil {
return err
}
createOpts := linodego.DomainRecordCreateOptions{
Name: dns01.UnFqdn(fqdn),
Target: value,
TTLSec: d.config.TTL,
Type: linodego.RecordTypeTXT,
}
_, err = d.client.CreateDomainRecord(context.Background(), zone.domainID, createOpts)
return err
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZoneInfo(fqdn)
if err != nil {
return err
}
// Get all TXT records for the specified domain.
listOpts := linodego.NewListOptions(0, "{\"type\":\"TXT\"}")
resources, err := d.client.ListDomainRecords(context.Background(), zone.domainID, listOpts)
if err != nil {
return err
}
// Remove the specified resource, if it exists.
for _, resource := range resources {
if (resource.Name == strings.TrimSuffix(fqdn, ".") || resource.Name == zone.resourceName) &&
resource.Target == value {
if err := d.client.DeleteDomainRecord(context.Background(), zone.domainID, resource.ID); err != nil {
return err
}
}
}
return nil
}
func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
// Lookup the zone that handles the specified FQDN.
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return nil, err
}
// Query the authority zone.
data, err := json.Marshal(map[string]string{"domain": dns01.UnFqdn(authZone)})
if err != nil {
return nil, err
}
listOpts := linodego.NewListOptions(0, string(data))
domains, err := d.client.ListDomains(context.Background(), listOpts)
if err != nil {
return nil, err
}
if len(domains) == 0 {
return nil, fmt.Errorf("domain not found")
}
return &hostedZoneInfo{
domainID: domains[0].ID,
resourceName: strings.TrimSuffix(fqdn, "."+authZone),
}, nil
}

View file

@ -0,0 +1,52 @@
package mydnsjp
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
func (d *DNSProvider) doRequest(domain, value string, cmd string) error {
req, err := d.buildRequest(domain, value, cmd)
if err != nil {
return err
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("error querying API: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
var content []byte
content, err = ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("request %s failed [status code %d]: %s", req.URL, resp.StatusCode, string(content))
}
return nil
}
func (d *DNSProvider) buildRequest(domain, value string, cmd string) (*http.Request, error) {
params := url.Values{}
params.Set("CERTBOT_DOMAIN", domain)
params.Set("CERTBOT_VALIDATION", value)
params.Set("EDIT_CMD", cmd)
req, err := http.NewRequest(http.MethodPost, defaultBaseURL, strings.NewReader(params.Encode()))
if err != nil {
return nil, fmt.Errorf("invalid request: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(d.config.MasterID, d.config.Password)
return req, nil
}

View file

@ -0,0 +1,93 @@
// Package mydnsjp implements a DNS provider for solving the DNS-01 challenge using MyDNS.jp.
package mydnsjp
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const defaultBaseURL = "https://www.mydns.jp/directedit.html"
// Config is used to configure the creation of the DNSProvider
type Config struct {
MasterID string
Password string
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("MYDNSJP_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("MYDNSJP_POLLING_INTERVAL", 2*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("MYDNSJP_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for MyDNS.jp.
// Credentials must be passed in the environment variables: MYDNSJP_MASTER_ID and MYDNSJP_PASSWORD.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("MYDNSJP_MASTER_ID", "MYDNSJP_PASSWORD")
if err != nil {
return nil, fmt.Errorf("mydnsjp: %v", err)
}
config := NewDefaultConfig()
config.MasterID = values["MYDNSJP_MASTER_ID"]
config.Password = values["MYDNSJP_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for MyDNS.jp.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("mydnsjp: the configuration of the DNS provider is nil")
}
if config.MasterID == "" || config.Password == "" {
return nil, errors.New("mydnsjp: some credentials information are missing")
}
return &DNSProvider{config: config}, nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, value := dns01.GetRecord(domain, keyAuth)
err := d.doRequest(domain, value, "REGIST")
if err != nil {
return fmt.Errorf("mydnsjp: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
_, value := dns01.GetRecord(domain, keyAuth)
err := d.doRequest(domain, value, "DELETE")
if err != nil {
return fmt.Errorf("mydnsjp: %v", err)
}
return nil
}

View file

@ -0,0 +1,225 @@
package namecheap
import (
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
// Record describes a DNS record returned by the Namecheap DNS gethosts API.
// Namecheap uses the term "host" to refer to all DNS records that include
// a host field (A, AAAA, CNAME, NS, TXT, URL).
type Record struct {
Type string `xml:",attr"`
Name string `xml:",attr"`
Address string `xml:",attr"`
MXPref string `xml:",attr"`
TTL string `xml:",attr"`
}
// apiError describes an error record in a namecheap API response.
type apiError struct {
Number int `xml:",attr"`
Description string `xml:",innerxml"`
}
type setHostsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apiError `xml:"Errors>Error"`
Result struct {
IsSuccess string `xml:",attr"`
} `xml:"CommandResponse>DomainDNSSetHostsResult"`
}
type getHostsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apiError `xml:"Errors>Error"`
Hosts []Record `xml:"CommandResponse>DomainDNSGetHostsResult>host"`
}
type getTldsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Errors []apiError `xml:"Errors>Error"`
Result []struct {
Name string `xml:",attr"`
} `xml:"CommandResponse>Tlds>Tld"`
}
// getTLDs requests the list of available TLDs.
// https://www.namecheap.com/support/api/methods/domains/get-tld-list.aspx
func (d *DNSProvider) getTLDs() (map[string]string, error) {
request, err := d.newRequestGet("namecheap.domains.getTldList")
if err != nil {
return nil, err
}
var gtr getTldsResponse
err = d.do(request, &gtr)
if err != nil {
return nil, err
}
if len(gtr.Errors) > 0 {
return nil, fmt.Errorf("%s [%d]", gtr.Errors[0].Description, gtr.Errors[0].Number)
}
tlds := make(map[string]string)
for _, t := range gtr.Result {
tlds[t.Name] = t.Name
}
return tlds, nil
}
// getHosts reads the full list of DNS host records.
// https://www.namecheap.com/support/api/methods/domains-dns/get-hosts.aspx
func (d *DNSProvider) getHosts(sld, tld string) ([]Record, error) {
request, err := d.newRequestGet("namecheap.domains.dns.getHosts",
addParam("SLD", sld),
addParam("TLD", tld),
)
if err != nil {
return nil, err
}
var ghr getHostsResponse
err = d.do(request, &ghr)
if err != nil {
return nil, err
}
if len(ghr.Errors) > 0 {
return nil, fmt.Errorf("%s [%d]", ghr.Errors[0].Description, ghr.Errors[0].Number)
}
return ghr.Hosts, nil
}
// setHosts writes the full list of DNS host records .
// https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
func (d *DNSProvider) setHosts(sld, tld string, hosts []Record) error {
req, err := d.newRequestPost("namecheap.domains.dns.setHosts",
addParam("SLD", sld),
addParam("TLD", tld),
func(values url.Values) {
for i, h := range hosts {
ind := fmt.Sprintf("%d", i+1)
values.Add("HostName"+ind, h.Name)
values.Add("RecordType"+ind, h.Type)
values.Add("Address"+ind, h.Address)
values.Add("MXPref"+ind, h.MXPref)
values.Add("TTL"+ind, h.TTL)
}
},
)
if err != nil {
return err
}
var shr setHostsResponse
err = d.do(req, &shr)
if err != nil {
return err
}
if len(shr.Errors) > 0 {
return fmt.Errorf("%s [%d]", shr.Errors[0].Description, shr.Errors[0].Number)
}
if shr.Result.IsSuccess != "true" {
return fmt.Errorf("setHosts failed")
}
return nil
}
func (d *DNSProvider) do(req *http.Request, out interface{}) error {
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
var body []byte
body, err = readBody(resp)
if err != nil {
return fmt.Errorf("HTTP error %d [%s]: %v", resp.StatusCode, http.StatusText(resp.StatusCode), err)
}
return fmt.Errorf("HTTP error %d [%s]: %s", resp.StatusCode, http.StatusText(resp.StatusCode), string(body))
}
body, err := readBody(resp)
if err != nil {
return err
}
if err := xml.Unmarshal(body, out); err != nil {
return err
}
return nil
}
func (d *DNSProvider) newRequestGet(cmd string, params ...func(url.Values)) (*http.Request, error) {
query := d.makeQuery(cmd, params...)
reqURL, err := url.Parse(d.config.BaseURL)
if err != nil {
return nil, err
}
reqURL.RawQuery = query.Encode()
return http.NewRequest(http.MethodGet, reqURL.String(), nil)
}
func (d *DNSProvider) newRequestPost(cmd string, params ...func(url.Values)) (*http.Request, error) {
query := d.makeQuery(cmd, params...)
req, err := http.NewRequest(http.MethodPost, d.config.BaseURL, strings.NewReader(query.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}
func (d *DNSProvider) makeQuery(cmd string, params ...func(url.Values)) url.Values {
queryParams := make(url.Values)
queryParams.Set("ApiUser", d.config.APIUser)
queryParams.Set("ApiKey", d.config.APIKey)
queryParams.Set("UserName", d.config.APIUser)
queryParams.Set("Command", cmd)
queryParams.Set("ClientIp", d.config.ClientIP)
for _, param := range params {
param(queryParams)
}
return queryParams
}
func addParam(key, value string) func(url.Values) {
return func(values url.Values) {
values.Set(key, value)
}
}
func readBody(resp *http.Response) ([]byte, error) {
if resp.Body == nil {
return nil, fmt.Errorf("response body is nil")
}
defer resp.Body.Close()
rawBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return rawBody, nil
}

View file

@ -0,0 +1,260 @@
// Package namecheap implements a DNS provider for solving the DNS-01 challenge using namecheap DNS.
package namecheap
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
)
// Notes about namecheap's tool API:
// 1. Using the API requires registration. Once registered, use your account
// name and API key to access the API.
// 2. There is no API to add or modify a single DNS record. Instead you must
// read the entire list of records, make modifications, and then write the
// entire updated list of records. (Yuck.)
// 3. Namecheap's DNS updates can be slow to propagate. I've seen them take
// as long as an hour.
// 4. Namecheap requires you to whitelist the IP address from which you call
// its APIs. It also requires all API calls to include the whitelisted IP
// address as a form or query string value. This code uses a namecheap
// service to query the client's IP address.
const (
defaultBaseURL = "https://api.namecheap.com/xml.response"
getIPURL = "https://dynamicdns.park-your-domain.com/getip"
)
// A challenge represents all the data needed to specify a dns-01 challenge
// to lets-encrypt.
type challenge struct {
domain string
key string
keyFqdn string
keyValue string
tld string
sld string
host string
}
// Config is used to configure the creation of the DNSProvider
type Config struct {
Debug bool
BaseURL string
APIUser string
APIKey string
ClientIP string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
BaseURL: defaultBaseURL,
Debug: env.GetOrDefaultBool("NAMECHEAP_DEBUG", false),
TTL: env.GetOrDefaultInt("NAMECHEAP_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("NAMECHEAP_PROPAGATION_TIMEOUT", 60*time.Minute),
PollingInterval: env.GetOrDefaultSecond("NAMECHEAP_POLLING_INTERVAL", 15*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NAMECHEAP_HTTP_TIMEOUT", 60*time.Second),
},
}
}
// DNSProvider is an implementation of the ChallengeProviderTimeout interface
// that uses Namecheap's tool API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for namecheap.
// Credentials must be passed in the environment variables:
// NAMECHEAP_API_USER and NAMECHEAP_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NAMECHEAP_API_USER", "NAMECHEAP_API_KEY")
if err != nil {
return nil, fmt.Errorf("namecheap: %v", err)
}
config := NewDefaultConfig()
config.APIUser = values["NAMECHEAP_API_USER"]
config.APIKey = values["NAMECHEAP_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for namecheap.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("namecheap: the configuration of the DNS provider is nil")
}
if config.APIUser == "" || config.APIKey == "" {
return nil, fmt.Errorf("namecheap: credentials missing")
}
if len(config.ClientIP) == 0 {
clientIP, err := getClientIP(config.HTTPClient, config.Debug)
if err != nil {
return nil, fmt.Errorf("namecheap: %v", err)
}
config.ClientIP = clientIP
}
return &DNSProvider{config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Namecheap can sometimes take a long time to complete an update, so wait up to 60 minutes for the update to propagate.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present installs a TXT record for the DNS challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
tlds, err := d.getTLDs()
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
ch, err := newChallenge(domain, keyAuth, tlds)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
records, err := d.getHosts(ch.sld, ch.tld)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
record := Record{
Name: ch.key,
Type: "TXT",
Address: ch.keyValue,
MXPref: "10",
TTL: strconv.Itoa(d.config.TTL),
}
records = append(records, record)
if d.config.Debug {
for _, h := range records {
log.Printf("%-5.5s %-30.30s %-6s %-70.70s", h.Type, h.Name, h.TTL, h.Address)
}
}
err = d.setHosts(ch.sld, ch.tld, records)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
return nil
}
// CleanUp removes a TXT record used for a previous DNS challenge.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
tlds, err := d.getTLDs()
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
ch, err := newChallenge(domain, keyAuth, tlds)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
records, err := d.getHosts(ch.sld, ch.tld)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
// Find the challenge TXT record and remove it if found.
var found bool
var newRecords []Record
for _, h := range records {
if h.Name == ch.key && h.Type == "TXT" {
found = true
} else {
newRecords = append(newRecords, h)
}
}
if !found {
return nil
}
err = d.setHosts(ch.sld, ch.tld, newRecords)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
return nil
}
// getClientIP returns the client's public IP address.
// It uses namecheap's IP discovery service to perform the lookup.
func getClientIP(client *http.Client, debug bool) (addr string, err error) {
resp, err := client.Get(getIPURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
clientIP, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if debug {
log.Println("Client IP:", string(clientIP))
}
return string(clientIP), nil
}
// 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 = dns01.UnFqdn(domain)
parts := strings.Split(domain, ".")
// Find the longest matching TLD.
longest := -1
for i := len(parts); i > 0; i-- {
t := strings.Join(parts[i-1:], ".")
if _, found := tlds[t]; found {
longest = i - 1
}
}
if longest < 1 {
return nil, fmt.Errorf("invalid domain name %q", domain)
}
tld := strings.Join(parts[longest:], ".")
sld := parts[longest-1]
var host string
if longest >= 1 {
host = strings.Join(parts[:longest-1], ".")
}
fqdn, value := dns01.GetRecord(domain, keyAuth)
return &challenge{
domain: domain,
key: "_acme-challenge." + host,
keyFqdn: fqdn,
keyValue: value,
tld: tld,
sld: sld,
host: host,
}, nil
}

View file

@ -0,0 +1,170 @@
// Package namedotcom implements a DNS provider for solving the DNS-01 challenge using Name.com's DNS service.
package namedotcom
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/namedotcom/go/namecom"
)
// according to https://www.name.com/api-docs/DNS#CreateRecord
const minTTL = 300
// Config is used to configure the creation of the DNSProvider
type Config struct {
Username string
APIToken string
Server string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NAMECOM_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("NAMECOM_PROPAGATION_TIMEOUT", 15*time.Minute),
PollingInterval: env.GetOrDefaultSecond("NAMECOM_POLLING_INTERVAL", 20*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NAMECOM_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *namecom.NameCom
config *Config
}
// 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) {
values, err := env.Get("NAMECOM_USERNAME", "NAMECOM_API_TOKEN")
if err != nil {
return nil, fmt.Errorf("namedotcom: %v", err)
}
config := NewDefaultConfig()
config.Username = values["NAMECOM_USERNAME"]
config.APIToken = values["NAMECOM_API_TOKEN"]
config.Server = env.GetOrFile("NAMECOM_SERVER")
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for namedotcom.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("namedotcom: the configuration of the DNS provider is nil")
}
if config.Username == "" {
return nil, fmt.Errorf("namedotcom: username is required")
}
if config.APIToken == "" {
return nil, fmt.Errorf("namedotcom: API token is required")
}
if config.TTL < minTTL {
return nil, fmt.Errorf("namedotcom: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
client := namecom.New(config.Username, config.APIToken)
client.Client = config.HTTPClient
if config.Server != "" {
client.Server = config.Server
}
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
request := &namecom.Record{
DomainName: domain,
Host: d.extractRecordName(fqdn, domain),
Type: "TXT",
TTL: uint32(d.config.TTL),
Answer: value,
}
_, err := d.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 (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
records, err := d.getRecords(domain)
if err != nil {
return fmt.Errorf("namedotcom: %v", err)
}
for _, rec := range records {
if rec.Fqdn == fqdn && rec.Type == "TXT" {
request := &namecom.DeleteRecordRequest{
DomainName: domain,
ID: rec.ID,
}
_, err := d.client.DeleteRecord(request)
if err != nil {
return fmt.Errorf("namedotcom: %v", err)
}
}
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) {
request := &namecom.ListRecordsRequest{
DomainName: domain,
Page: 1,
}
var records []*namecom.Record
for request.Page > 0 {
response, err := d.client.ListRecords(request)
if err != nil {
return nil, err
}
records = append(records, response.Records...)
request.Page = response.NextPage
}
return records, nil
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
name := dns01.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}

View file

@ -0,0 +1,327 @@
package internal
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// defaultBaseURL for reaching the jSON-based API-Endpoint of netcup
const defaultBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
// success response status
const success = "success"
// Request wrapper as specified in netcup wiki
// needed for every request to netcup API around *Msg
// https://www.netcup-wiki.de/wiki/CCP_API#Anmerkungen_zu_JSON-Requests
type Request struct {
Action string `json:"action"`
Param interface{} `json:"param"`
}
// LoginRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#login
type LoginRequest struct {
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APIPassword string `json:"apipassword"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// LogoutRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#logout
type LogoutRequest struct {
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APISessionID string `json:"apisessionid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// UpdateDNSRecordsRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#updateDnsRecords
type UpdateDNSRecordsRequest struct {
DomainName string `json:"domainname"`
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APISessionID string `json:"apisessionid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
DNSRecordSet DNSRecordSet `json:"dnsrecordset"`
}
// DNSRecordSet as specified in netcup WSDL
// needed in UpdateDNSRecordsRequest
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecordset
type DNSRecordSet struct {
DNSRecords []DNSRecord `json:"dnsrecords"`
}
// InfoDNSRecordsRequest as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#infoDnsRecords
type InfoDNSRecordsRequest struct {
DomainName string `json:"domainname"`
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APISessionID string `json:"apisessionid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// DNSRecord as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecord
type DNSRecord struct {
ID int `json:"id,string,omitempty"`
Hostname string `json:"hostname"`
RecordType string `json:"type"`
Priority string `json:"priority,omitempty"`
Destination string `json:"destination"`
DeleteRecord bool `json:"deleterecord,omitempty"`
State string `json:"state,omitempty"`
TTL int `json:"ttl,omitempty"`
}
// ResponseMsg as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Responsemessage
type ResponseMsg struct {
ServerRequestID string `json:"serverrequestid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
Action string `json:"action"`
Status string `json:"status"`
StatusCode int `json:"statuscode"`
ShortMessage string `json:"shortmessage"`
LongMessage string `json:"longmessage"`
ResponseData json.RawMessage `json:"responsedata,omitempty"`
}
func (r *ResponseMsg) Error() string {
return fmt.Sprintf("an error occurred during the action %s: [Status=%s, StatusCode=%d, ShortMessage=%s, LongMessage=%s]",
r.Action, r.Status, r.StatusCode, r.ShortMessage, r.LongMessage)
}
// LoginResponse response to login action.
type LoginResponse struct {
APISessionID string `json:"apisessionid"`
}
// InfoDNSRecordsResponse response to infoDnsRecords action.
type InfoDNSRecordsResponse struct {
APISessionID string `json:"apisessionid"`
DNSRecords []DNSRecord `json:"dnsrecords,omitempty"`
}
// Client netcup DNS client
type Client struct {
customerNumber string
apiKey string
apiPassword string
HTTPClient *http.Client
BaseURL string
}
// NewClient creates a netcup DNS client
func NewClient(customerNumber string, apiKey string, apiPassword string) (*Client, error) {
if customerNumber == "" || apiKey == "" || apiPassword == "" {
return nil, fmt.Errorf("credentials missing")
}
return &Client{
customerNumber: customerNumber,
apiKey: apiKey,
apiPassword: apiPassword,
BaseURL: defaultBaseURL,
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
},
}, nil
}
// Login performs the login as specified by the netcup WSDL
// returns sessionID needed to perform remaining actions
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) Login() (string, error) {
payload := &Request{
Action: "login",
Param: &LoginRequest{
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APIPassword: c.apiPassword,
ClientRequestID: "",
},
}
var responseData LoginResponse
err := c.doRequest(payload, &responseData)
if err != nil {
return "", fmt.Errorf("loging error: %v", err)
}
return responseData.APISessionID, nil
}
// Logout performs the logout with the supplied sessionID as specified by the netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) Logout(sessionID string) error {
payload := &Request{
Action: "logout",
Param: &LogoutRequest{
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: sessionID,
ClientRequestID: "",
},
}
err := c.doRequest(payload, nil)
if err != nil {
return fmt.Errorf("logout error: %v", err)
}
return nil
}
// UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) UpdateDNSRecord(sessionID, domainName string, records []DNSRecord) error {
payload := &Request{
Action: "updateDnsRecords",
Param: UpdateDNSRecordsRequest{
DomainName: domainName,
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: sessionID,
ClientRequestID: "",
DNSRecordSet: DNSRecordSet{DNSRecords: records},
},
}
err := c.doRequest(payload, nil)
if err != nil {
return fmt.Errorf("error when sending the request: %v", err)
}
return nil
}
// GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL
// returns an array of DNSRecords
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, error) {
payload := &Request{
Action: "infoDnsRecords",
Param: InfoDNSRecordsRequest{
DomainName: hostname,
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: apiSessionID,
ClientRequestID: "",
},
}
var responseData InfoDNSRecordsResponse
err := c.doRequest(payload, &responseData)
if err != nil {
return nil, fmt.Errorf("error when sending the request: %v", err)
}
return responseData.DNSRecords, nil
}
// doRequest marshals given body to JSON, send the request to netcup API
// and returns body of response
func (c *Client) doRequest(payload interface{}, responseData interface{}) error {
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, c.BaseURL, bytes.NewReader(body))
if err != nil {
return err
}
req.Close = true
req.Header.Set("content-type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
if err = checkResponse(resp); err != nil {
return err
}
respMsg, err := decodeResponseMsg(resp)
if err != nil {
return err
}
if respMsg.Status != success {
return respMsg
}
if responseData != nil {
err = json.Unmarshal(respMsg.ResponseData, responseData)
if err != nil {
return fmt.Errorf("%v: unmarshaling %T error: %v: %s",
respMsg, responseData, err, string(respMsg.ResponseData))
}
}
return nil
}
func checkResponse(resp *http.Response) error {
if resp.StatusCode > 299 {
if resp.Body == nil {
return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode)
}
defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("unable to read body: status code=%d, error=%v", resp.StatusCode, err)
}
return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw))
}
return nil
}
func decodeResponseMsg(resp *http.Response) (*ResponseMsg, error) {
if resp.Body == nil {
return nil, fmt.Errorf("response body is nil, status code=%d", resp.StatusCode)
}
defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read body: status code=%d, error=%v", resp.StatusCode, err)
}
var respMsg ResponseMsg
err = json.Unmarshal(raw, &respMsg)
if err != nil {
return nil, fmt.Errorf("unmarshaling %T error [status code=%d]: %v: %s", respMsg, resp.StatusCode, err, string(raw))
}
return &respMsg, nil
}
// GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord
// equivalence is determined by Destination and RecortType attributes
// returns index of given DNSRecord in given array of DNSRecords
func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
for index, element := range records {
if record.Destination == element.Destination && record.RecordType == element.RecordType {
return index, nil
}
}
return -1, fmt.Errorf("no DNS Record found")
}

View file

@ -0,0 +1,182 @@
// Package netcup implements a DNS Provider for solving the DNS-01 challenge using the netcup DNS API.
package netcup
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/providers/dns/netcup/internal"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
Key string
Password string
Customer string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NETCUP_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("NETCUP_PROPAGATION_TIMEOUT", 120*time.Second),
PollingInterval: env.GetOrDefaultSecond("NETCUP_POLLING_INTERVAL", 5*time.Second),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NETCUP_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
client *internal.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for netcup.
// Credentials must be passed in the environment variables:
// NETCUP_CUSTOMER_NUMBER, NETCUP_API_KEY, NETCUP_API_PASSWORD
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NETCUP_CUSTOMER_NUMBER", "NETCUP_API_KEY", "NETCUP_API_PASSWORD")
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
config := NewDefaultConfig()
config.Customer = values["NETCUP_CUSTOMER_NUMBER"]
config.Key = values["NETCUP_API_KEY"]
config.Password = values["NETCUP_API_PASSWORD"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for netcup.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("netcup: the configuration of the DNS provider is nil")
}
client, err := internal.NewClient(config.Customer, config.Key, config.Password)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
client.HTTPClient = config.HTTPClient
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domainName, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("netcup: failed to find DNSZone, %v", err)
}
sessionID, err := d.client.Login()
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
defer func() {
err = d.client.Logout(sessionID)
if err != nil {
log.Print("netcup: %v", err)
}
}()
hostname := strings.Replace(fqdn, "."+zone, "", 1)
record := internal.DNSRecord{
Hostname: hostname,
RecordType: "TXT",
Destination: value,
TTL: d.config.TTL,
}
zone = dns01.UnFqdn(zone)
records, err := d.client.GetDNSRecords(zone, sessionID)
if err != nil {
// skip no existing records
log.Infof("no existing records, error ignored: %v", err)
}
records = append(records, record)
err = d.client.UpdateDNSRecord(sessionID, zone, records)
if err != nil {
return fmt.Errorf("netcup: failed to add TXT-Record: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domainName, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("netcup: failed to find DNSZone, %v", err)
}
sessionID, err := d.client.Login()
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
defer func() {
err = d.client.Logout(sessionID)
if err != nil {
log.Print("netcup: %v", err)
}
}()
hostname := strings.Replace(fqdn, "."+zone, "", 1)
zone = dns01.UnFqdn(zone)
records, err := d.client.GetDNSRecords(zone, sessionID)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
record := internal.DNSRecord{
Hostname: hostname,
RecordType: "TXT",
Destination: value,
}
idx, err := internal.GetDNSRecordIdx(records, record)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
records[idx].DeleteRecord = true
err = d.client.UpdateDNSRecord(sessionID, zone, []internal.DNSRecord{records[idx]})
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,234 @@
package internal
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"net/http"
"time"
)
const (
defaultBaseURL = "https://dns.api.nifcloud.com"
apiVersion = "2012-12-12N2013-12-16"
// XMLNs XML NS of Route53
XMLNs = "https://route53.amazonaws.com/doc/2012-12-12/"
)
// ChangeResourceRecordSetsRequest is a complex type that contains change information for the resource record set.
type ChangeResourceRecordSetsRequest struct {
XMLNs string `xml:"xmlns,attr"`
ChangeBatch ChangeBatch `xml:"ChangeBatch"`
}
// ChangeResourceRecordSetsResponse is a complex type containing the response for the request.
type ChangeResourceRecordSetsResponse struct {
ChangeInfo ChangeInfo `xml:"ChangeInfo"`
}
// GetChangeResponse is a complex type that contains the ChangeInfo element.
type GetChangeResponse struct {
ChangeInfo ChangeInfo `xml:"ChangeInfo"`
}
// ErrorResponse is the information for any errors.
type ErrorResponse struct {
Error struct {
Type string `xml:"Type"`
Message string `xml:"Message"`
Code string `xml:"Code"`
} `xml:"Error"`
RequestID string `xml:"RequestId"`
}
// ChangeBatch is the information for a change request.
type ChangeBatch struct {
Changes Changes `xml:"Changes"`
Comment string `xml:"Comment"`
}
// Changes is array of Change.
type Changes struct {
Change []Change `xml:"Change"`
}
// Change is the information for each resource record set that you want to change.
type Change struct {
Action string `xml:"Action"`
ResourceRecordSet ResourceRecordSet `xml:"ResourceRecordSet"`
}
// ResourceRecordSet is the information about the resource record set to create or delete.
type ResourceRecordSet struct {
Name string `xml:"Name"`
Type string `xml:"Type"`
TTL int `xml:"TTL"`
ResourceRecords ResourceRecords `xml:"ResourceRecords"`
}
// ResourceRecords is array of ResourceRecord.
type ResourceRecords struct {
ResourceRecord []ResourceRecord `xml:"ResourceRecord"`
}
// ResourceRecord is the information specific to the resource record.
type ResourceRecord struct {
Value string `xml:"Value"`
}
// ChangeInfo is A complex type that describes change information about changes made to your hosted zone.
type ChangeInfo struct {
ID string `xml:"Id"`
Status string `xml:"Status"`
SubmittedAt string `xml:"SubmittedAt"`
}
// NewClient Creates a new client of NIFCLOUD DNS
func NewClient(accessKey string, secretKey string) (*Client, error) {
if len(accessKey) == 0 || len(secretKey) == 0 {
return nil, errors.New("credentials missing")
}
return &Client{
accessKey: accessKey,
secretKey: secretKey,
BaseURL: defaultBaseURL,
HTTPClient: &http.Client{},
}, nil
}
// Client client of NIFCLOUD DNS
type Client struct {
accessKey string
secretKey string
BaseURL string
HTTPClient *http.Client
}
// ChangeResourceRecordSets Call ChangeResourceRecordSets API and return response.
func (c *Client) ChangeResourceRecordSets(hostedZoneID string, input ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) {
requestURL := fmt.Sprintf("%s/%s/hostedzone/%s/rrset", c.BaseURL, apiVersion, hostedZoneID)
body := &bytes.Buffer{}
body.Write([]byte(xml.Header))
err := xml.NewEncoder(body).Encode(input)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, requestURL, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "text/xml; charset=utf-8")
err = c.sign(req)
if err != nil {
return nil, fmt.Errorf("an error occurred during the creation of the signature: %v", err)
}
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if res.Body == nil {
return nil, errors.New("the response body is nil")
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
errResp := &ErrorResponse{}
err = xml.NewDecoder(res.Body).Decode(errResp)
if err != nil {
return nil, fmt.Errorf("an error occurred while unmarshaling the error body to XML: %v", err)
}
return nil, fmt.Errorf("an error occurred: %s", errResp.Error.Message)
}
output := &ChangeResourceRecordSetsResponse{}
err = xml.NewDecoder(res.Body).Decode(output)
if err != nil {
return nil, fmt.Errorf("an error occurred while unmarshaling the response body to XML: %v", err)
}
return output, err
}
// GetChange Call GetChange API and return response.
func (c *Client) GetChange(statusID string) (*GetChangeResponse, error) {
requestURL := fmt.Sprintf("%s/%s/change/%s", c.BaseURL, apiVersion, statusID)
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return nil, err
}
err = c.sign(req)
if err != nil {
return nil, fmt.Errorf("an error occurred during the creation of the signature: %v", err)
}
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if res.Body == nil {
return nil, errors.New("the response body is nil")
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
errResp := &ErrorResponse{}
err = xml.NewDecoder(res.Body).Decode(errResp)
if err != nil {
return nil, fmt.Errorf("an error occurred while unmarshaling the error body to XML: %v", err)
}
return nil, fmt.Errorf("an error occurred: %s", errResp.Error.Message)
}
output := &GetChangeResponse{}
err = xml.NewDecoder(res.Body).Decode(output)
if err != nil {
return nil, fmt.Errorf("an error occurred while unmarshaling the response body to XML: %v", err)
}
return output, nil
}
func (c *Client) sign(req *http.Request) error {
if req.Header.Get("Date") == "" {
location, err := time.LoadLocation("GMT")
if err != nil {
return err
}
req.Header.Set("Date", time.Now().In(location).Format(time.RFC1123))
}
if req.URL.Path == "" {
req.URL.Path += "/"
}
mac := hmac.New(sha1.New, []byte(c.secretKey))
_, err := mac.Write([]byte(req.Header.Get("Date")))
if err != nil {
return err
}
hashed := mac.Sum(nil)
signature := base64.StdEncoding.EncodeToString(hashed)
auth := fmt.Sprintf("NIFTY3-HTTPS NiftyAccessKeyId=%s,Algorithm=HmacSHA1,Signature=%s", c.accessKey, signature)
req.Header.Set("X-Nifty-Authorization", auth)
return nil
}

View file

@ -0,0 +1,156 @@
// Package nifcloud implements a DNS provider for solving the DNS-01 challenge using NIFCLOUD DNS.
package nifcloud
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/providers/dns/nifcloud/internal"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/go-acme/lego/platform/wait"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
BaseURL string
AccessKey string
SecretKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NIFCLOUD_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("NIFCLOUD_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NIFCLOUD_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NIFCLOUD_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
client *internal.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for the NIFCLOUD DNS service.
// Credentials must be passed in the environment variables:
// NIFCLOUD_ACCESS_KEY_ID and NIFCLOUD_SECRET_ACCESS_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NIFCLOUD_ACCESS_KEY_ID", "NIFCLOUD_SECRET_ACCESS_KEY")
if err != nil {
return nil, fmt.Errorf("nifcloud: %v", err)
}
config := NewDefaultConfig()
config.BaseURL = env.GetOrFile("NIFCLOUD_DNS_ENDPOINT")
config.AccessKey = values["NIFCLOUD_ACCESS_KEY_ID"]
config.SecretKey = values["NIFCLOUD_SECRET_ACCESS_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for NIFCLOUD.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("nifcloud: the configuration of the DNS provider is nil")
}
client, err := internal.NewClient(config.AccessKey, config.SecretKey)
if err != nil {
return nil, fmt.Errorf("nifcloud: %v", err)
}
if config.HTTPClient != nil {
client.HTTPClient = config.HTTPClient
}
if len(config.BaseURL) > 0 {
client.BaseURL = config.BaseURL
}
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
err := d.changeRecord("CREATE", fqdn, value, domain, d.config.TTL)
if err != nil {
return fmt.Errorf("nifcloud: %v", err)
}
return err
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
err := d.changeRecord("DELETE", fqdn, value, domain, d.config.TTL)
if err != nil {
return fmt.Errorf("nifcloud: %v", err)
}
return err
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) changeRecord(action, fqdn, value, domain string, ttl int) error {
name := dns01.UnFqdn(fqdn)
reqParams := internal.ChangeResourceRecordSetsRequest{
XMLNs: internal.XMLNs,
ChangeBatch: internal.ChangeBatch{
Comment: "Managed by Lego",
Changes: internal.Changes{
Change: []internal.Change{
{
Action: action,
ResourceRecordSet: internal.ResourceRecordSet{
Name: name,
Type: "TXT",
TTL: ttl,
ResourceRecords: internal.ResourceRecords{
ResourceRecord: []internal.ResourceRecord{
{
Value: value,
},
},
},
},
},
},
},
},
}
resp, err := d.client.ChangeResourceRecordSets(domain, reqParams)
if err != nil {
return fmt.Errorf("failed to change NIFCLOUD record set: %v", err)
}
statusID := resp.ChangeInfo.ID
return wait.For("nifcloud", 120*time.Second, 4*time.Second, func() (bool, error) {
resp, err := d.client.GetChange(statusID)
if err != nil {
return false, fmt.Errorf("failed to query NIFCLOUD DNS change status: %v", err)
}
return resp.ChangeInfo.Status == "INSYNC", nil
})
}

162
vendor/github.com/go-acme/lego/providers/dns/ns1/ns1.go generated vendored Normal file
View file

@ -0,0 +1,162 @@
// Package ns1 implements a DNS provider for solving the DNS-01 challenge using NS1 DNS.
package ns1
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
"github.com/go-acme/lego/platform/config/env"
"gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NS1_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("NS1_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NS1_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("NS1_HTTP_TIMEOUT", 10*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *rest.Client
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for NS1.
// Credentials must be passed in the environment variables: NS1_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NS1_API_KEY")
if err != nil {
return nil, fmt.Errorf("ns1: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["NS1_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for NS1.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ns1: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, fmt.Errorf("ns1: credentials missing")
}
client := rest.NewClient(config.HTTPClient, rest.SetAPIKey(config.APIKey))
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZone(fqdn)
if err != nil {
return fmt.Errorf("ns1: %v", err)
}
record, _, err := d.client.Records.Get(zone.Zone, dns01.UnFqdn(fqdn), "TXT")
// Create a new record
if err == rest.ErrRecordMissing || record == nil {
log.Infof("Create a new record for [zone: %s, fqdn: %s, domain: %s]", zone.Zone, fqdn)
record = dns.NewRecord(zone.Zone, dns01.UnFqdn(fqdn), "TXT")
record.TTL = d.config.TTL
record.Answers = []*dns.Answer{{Rdata: []string{value}}}
_, err = d.client.Records.Create(record)
if err != nil {
return fmt.Errorf("ns1: failed to create record [zone: %q, fqdn: %q]: %v", zone.Zone, fqdn, err)
}
return nil
}
if err != nil {
return fmt.Errorf("ns1: failed to get the existing record: %v", err)
}
// Update the existing records
record.Answers = append(record.Answers, &dns.Answer{Rdata: []string{value}})
log.Infof("Update an existing record for [zone: %s, fqdn: %s, domain: %s]", zone.Zone, fqdn, domain)
_, err = d.client.Records.Update(record)
if err != nil {
return fmt.Errorf("ns1: failed to update record [zone: %q, fqdn: %q]: %v", zone.Zone, fqdn, err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZone(fqdn)
if err != nil {
return fmt.Errorf("ns1: %v", err)
}
name := dns01.UnFqdn(fqdn)
_, err = d.client.Records.Delete(zone.Zone, name, "TXT")
if err != nil {
return fmt.Errorf("ns1: failed to delete record [zone: %q, domain: %q]: %v", zone.Zone, name, err)
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getHostedZone(fqdn string) (*dns.Zone, error) {
authZone, err := getAuthZone(fqdn)
if err != nil {
return nil, fmt.Errorf("failed to extract auth zone from fqdn %q: %v", fqdn, err)
}
zone, _, err := d.client.Zones.Get(authZone)
if err != nil {
return nil, fmt.Errorf("failed to get zone [authZone: %q, fqdn: %q]: %v", authZone, fqdn, err)
}
return zone, nil
}
func getAuthZone(fqdn string) (string, error) {
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return "", err
}
return strings.TrimSuffix(authZone, "."), nil
}

View file

@ -0,0 +1,101 @@
package oraclecloud
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"github.com/go-acme/lego/platform/config/env"
"github.com/oracle/oci-go-sdk/common"
)
const (
ociPrivkey = "OCI_PRIVKEY"
ociPrivkeyPass = "OCI_PRIVKEY_PASS"
ociTenancyOCID = "OCI_TENANCY_OCID"
ociUserOCID = "OCI_USER_OCID"
ociPubkeyFingerprint = "OCI_PUBKEY_FINGERPRINT"
ociRegion = "OCI_REGION"
)
type configProvider struct {
values map[string]string
privateKeyPassphrase string
}
func newConfigProvider(values map[string]string) *configProvider {
return &configProvider{
values: values,
privateKeyPassphrase: env.GetOrFile(ociPrivkeyPass),
}
}
func (p *configProvider) PrivateRSAKey() (*rsa.PrivateKey, error) {
privateKey, err := getPrivateKey(ociPrivkey)
if err != nil {
return nil, err
}
return common.PrivateKeyFromBytes(privateKey, common.String(p.privateKeyPassphrase))
}
func (p *configProvider) KeyID() (string, error) {
tenancy, err := p.TenancyOCID()
if err != nil {
return "", err
}
user, err := p.UserOCID()
if err != nil {
return "", err
}
fingerprint, err := p.KeyFingerprint()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil
}
func (p *configProvider) TenancyOCID() (value string, err error) {
return p.values[ociTenancyOCID], nil
}
func (p *configProvider) UserOCID() (string, error) {
return p.values[ociUserOCID], nil
}
func (p *configProvider) KeyFingerprint() (string, error) {
return p.values[ociPubkeyFingerprint], nil
}
func (p *configProvider) Region() (string, error) {
return p.values[ociRegion], nil
}
func getPrivateKey(envVar string) ([]byte, error) {
envVarValue := os.Getenv(envVar)
if envVarValue != "" {
bytes, err := base64.StdEncoding.DecodeString(envVarValue)
if err != nil {
return nil, fmt.Errorf("failed to read base64 value %s (defined by env var %s): %s", envVarValue, envVar, err)
}
return bytes, nil
}
fileVar := envVar + "_FILE"
fileVarValue := os.Getenv(fileVar)
if fileVarValue == "" {
return nil, fmt.Errorf("no value provided for: %s or %s", envVar, fileVar)
}
fileContents, err := ioutil.ReadFile(fileVarValue)
if err != nil {
return nil, fmt.Errorf("failed to read the file %s (defined by env var %s): %s", fileVarValue, fileVar, err)
}
return fileContents, nil
}

View file

@ -0,0 +1,175 @@
package oraclecloud
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/oracle/oci-go-sdk/common"
"github.com/oracle/oci-go-sdk/dns"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
CompartmentID string
OCIConfigProvider common.ConfigurationProvider
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("OCI_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("OCI_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("OCI_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("OCI_HTTP_TIMEOUT", 60*time.Second),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
type DNSProvider struct {
client *dns.DnsClient
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for OracleCloud.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(ociPrivkey, ociTenancyOCID, ociUserOCID, ociPubkeyFingerprint, ociRegion, "OCI_COMPARTMENT_OCID")
if err != nil {
return nil, fmt.Errorf("oraclecloud: %v", err)
}
config := NewDefaultConfig()
config.CompartmentID = values["OCI_COMPARTMENT_OCID"]
config.OCIConfigProvider = newConfigProvider(values)
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for OracleCloud.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("oraclecloud: the configuration of the DNS provider is nil")
}
if config.CompartmentID == "" {
return nil, errors.New("oraclecloud: CompartmentID is missing")
}
if config.OCIConfigProvider == nil {
return nil, errors.New("oraclecloud: OCIConfigProvider is missing")
}
client, err := dns.NewDnsClientWithConfigurationProvider(config.OCIConfigProvider)
if err != nil {
return nil, fmt.Errorf("oraclecloud: %v", err)
}
if config.HTTPClient != nil {
client.HTTPClient = config.HTTPClient
}
return &DNSProvider{client: &client, config: config}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
// generate request to dns.PatchDomainRecordsRequest
recordOperation := dns.RecordOperation{
Domain: common.String(dns01.UnFqdn(fqdn)),
Rdata: common.String(value),
Rtype: common.String("TXT"),
Ttl: common.Int(d.config.TTL),
IsProtected: common.Bool(false),
}
request := dns.PatchDomainRecordsRequest{
CompartmentId: common.String(d.config.CompartmentID),
ZoneNameOrId: common.String(domain),
Domain: common.String(dns01.UnFqdn(fqdn)),
PatchDomainRecordsDetails: dns.PatchDomainRecordsDetails{
Items: []dns.RecordOperation{recordOperation},
},
}
_, err := d.client.PatchDomainRecords(context.Background(), request)
if err != nil {
return fmt.Errorf("oraclecloud: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
// search to TXT record's hash to delete
getRequest := dns.GetDomainRecordsRequest{
ZoneNameOrId: common.String(domain),
Domain: common.String(dns01.UnFqdn(fqdn)),
CompartmentId: common.String(d.config.CompartmentID),
Rtype: common.String("TXT"),
}
ctx := context.Background()
domainRecords, err := d.client.GetDomainRecords(ctx, getRequest)
if err != nil {
return fmt.Errorf("oraclecloud: %v", err)
}
if *domainRecords.OpcTotalItems == 0 {
return fmt.Errorf("oraclecloud: no record to CleanUp")
}
var deleteHash *string
for _, record := range domainRecords.RecordCollection.Items {
if record.Rdata != nil && *record.Rdata == `"`+value+`"` {
deleteHash = record.RecordHash
break
}
}
if deleteHash == nil {
return fmt.Errorf("oraclecloud: no record to CleanUp")
}
recordOperation := dns.RecordOperation{
RecordHash: deleteHash,
Operation: dns.RecordOperationOperationRemove,
}
patchRequest := dns.PatchDomainRecordsRequest{
ZoneNameOrId: common.String(domain),
Domain: common.String(dns01.UnFqdn(fqdn)),
PatchDomainRecordsDetails: dns.PatchDomainRecordsDetails{
Items: []dns.RecordOperation{recordOperation},
},
CompartmentId: common.String(d.config.CompartmentID),
}
_, err = d.client.PatchDomainRecords(ctx, patchRequest)
if err != nil {
return fmt.Errorf("oraclecloud: %v", err)
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

View file

@ -0,0 +1,263 @@
package otc
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
)
type recordset struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
TTL int `json:"ttl"`
Records []string `json:"records"`
}
type nameResponse struct {
Name string `json:"name"`
}
type userResponse struct {
Name string `json:"name"`
Password string `json:"password"`
Domain nameResponse `json:"domain"`
}
type passwordResponse struct {
User userResponse `json:"user"`
}
type identityResponse struct {
Methods []string `json:"methods"`
Password passwordResponse `json:"password"`
}
type scopeResponse struct {
Project nameResponse `json:"project"`
}
type authResponse struct {
Identity identityResponse `json:"identity"`
Scope scopeResponse `json:"scope"`
}
type loginResponse struct {
Auth authResponse `json:"auth"`
}
type endpointResponse struct {
Token token `json:"token"`
}
type token struct {
Catalog []catalog `json:"catalog"`
}
type catalog struct {
Type string `json:"type"`
Endpoints []endpoint `json:"endpoints"`
}
type endpoint struct {
URL string `json:"url"`
}
type zoneItem struct {
ID string `json:"id"`
}
type zonesResponse struct {
Zones []zoneItem `json:"zones"`
}
type recordSet struct {
ID string `json:"id"`
}
type recordSetsResponse struct {
RecordSets []recordSet `json:"recordsets"`
}
// Starts a new OTC API Session. Authenticates using userName, password
// and receives a token to be used in for subsequent requests.
func (d *DNSProvider) login() error {
return d.loginRequest()
}
func (d *DNSProvider) loginRequest() error {
userResp := userResponse{
Name: d.config.UserName,
Password: d.config.Password,
Domain: nameResponse{
Name: d.config.DomainName,
},
}
loginResp := loginResponse{
Auth: authResponse{
Identity: identityResponse{
Methods: []string{"password"},
Password: passwordResponse{
User: userResp,
},
},
Scope: scopeResponse{
Project: nameResponse{
Name: d.config.ProjectName,
},
},
},
}
body, err := json.Marshal(loginResp)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, d.config.IdentityEndpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: d.config.HTTPClient.Timeout}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("OTC API request failed with HTTP status code %d", resp.StatusCode)
}
d.token = resp.Header.Get("X-Subject-Token")
if d.token == "" {
return fmt.Errorf("unable to get auth token")
}
var endpointResp endpointResponse
err = json.NewDecoder(resp.Body).Decode(&endpointResp)
if err != nil {
return err
}
var endpoints []endpoint
for _, v := range endpointResp.Token.Catalog {
if v.Type == "dns" {
endpoints = append(endpoints, v.Endpoints...)
}
}
if len(endpoints) > 0 {
d.baseURL = fmt.Sprintf("%s/v2", endpoints[0].URL)
} else {
return fmt.Errorf("unable to get dns endpoint")
}
return nil
}
func (d *DNSProvider) getZoneID(zone string) (string, error) {
resource := fmt.Sprintf("zones?name=%s", zone)
resp, err := d.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return "", err
}
var zonesRes zonesResponse
err = json.NewDecoder(resp).Decode(&zonesRes)
if err != nil {
return "", err
}
if len(zonesRes.Zones) < 1 {
return "", fmt.Errorf("zone %s not found", zone)
}
if len(zonesRes.Zones) > 1 {
return "", fmt.Errorf("to many zones found")
}
if zonesRes.Zones[0].ID == "" {
return "", fmt.Errorf("id not found")
}
return zonesRes.Zones[0].ID, nil
}
func (d *DNSProvider) getRecordSetID(zoneID string, fqdn string) (string, error) {
resource := fmt.Sprintf("zones/%s/recordsets?type=TXT&name=%s", zoneID, fqdn)
resp, err := d.sendRequest(http.MethodGet, resource, nil)
if err != nil {
return "", err
}
var recordSetsRes recordSetsResponse
err = json.NewDecoder(resp).Decode(&recordSetsRes)
if err != nil {
return "", err
}
if len(recordSetsRes.RecordSets) < 1 {
return "", fmt.Errorf("record not found")
}
if len(recordSetsRes.RecordSets) > 1 {
return "", fmt.Errorf("to many records found")
}
if recordSetsRes.RecordSets[0].ID == "" {
return "", fmt.Errorf("id not found")
}
return recordSetsRes.RecordSets[0].ID, nil
}
func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error {
resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID)
_, err := d.sendRequest(http.MethodDelete, resource, nil)
return err
}
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (io.Reader, 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("X-Auth-Token", d.token)
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("OTC API request %s failed with HTTP status code %d", url, resp.StatusCode)
}
body1, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return bytes.NewReader(body1), nil
}

179
vendor/github.com/go-acme/lego/providers/dns/otc/otc.go generated vendored Normal file
View file

@ -0,0 +1,179 @@
// Package otc implements a DNS provider for solving the DNS-01 challenge using Open Telekom Cloud Managed DNS.
package otc
import (
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
)
const defaultIdentityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens"
// minTTL 300 is otc minimum value for ttl
const minTTL = 300
// Config is used to configure the creation of the DNSProvider
type Config struct {
IdentityEndpoint string
DomainName string
ProjectName string
UserName string
Password string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
IdentityEndpoint: env.GetOrDefaultString("OTC_IDENTITY_ENDPOINT", defaultIdentityEndpoint),
PropagationTimeout: env.GetOrDefaultSecond("OTC_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("OTC_POLLING_INTERVAL", dns01.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("OTC_TTL", minTTL),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("OTC_HTTP_TIMEOUT", 10*time.Second),
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// Workaround for keep alive bug in otc api
DisableKeepAlives: true,
},
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// OTC's Managed DNS API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
baseURL string
token string
}
// NewDNSProvider returns a DNSProvider instance configured for OTC DNS.
// Credentials must be passed in the environment variables: OTC_USER_NAME,
// OTC_DOMAIN_NAME, OTC_PASSWORD OTC_PROJECT_NAME and OTC_IDENTITY_ENDPOINT.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("OTC_DOMAIN_NAME", "OTC_USER_NAME", "OTC_PASSWORD", "OTC_PROJECT_NAME")
if err != nil {
return nil, fmt.Errorf("otc: %v", err)
}
config := NewDefaultConfig()
config.DomainName = values["OTC_DOMAIN_NAME"]
config.UserName = values["OTC_USER_NAME"]
config.Password = values["OTC_PASSWORD"]
config.ProjectName = values["OTC_PROJECT_NAME"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for OTC DNS.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("otc: the configuration of the DNS provider is nil")
}
if config.DomainName == "" || config.UserName == "" || config.Password == "" || config.ProjectName == "" {
return nil, fmt.Errorf("otc: credentials missing")
}
if config.TTL < minTTL {
return nil, fmt.Errorf("otc: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
}
if config.IdentityEndpoint == "" {
config.IdentityEndpoint = defaultIdentityEndpoint
}
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
err = d.login()
if err != nil {
return fmt.Errorf("otc: %v", err)
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("otc: unable to get zone: %s", err)
}
resource := fmt.Sprintf("zones/%s/recordsets", zoneID)
r1 := &recordset{
Name: fqdn,
Description: "Added TXT record for ACME dns-01 challenge using lego client",
Type: "TXT",
TTL: d.config.TTL,
Records: []string{fmt.Sprintf("\"%s\"", value)},
}
_, err = d.sendRequest(http.MethodPost, resource, r1)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
err = d.login()
if err != nil {
return fmt.Errorf("otc: %v", err)
}
zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
recordID, err := d.getRecordSetID(zoneID, fqdn)
if err != nil {
return fmt.Errorf("otc: unable go get record %s for zone %s: %s", fqdn, domain, err)
}
err = d.deleteRecordSet(zoneID, recordID)
if err != nil {
return fmt.Errorf("otc: %v", err)
}
return nil
}
// 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 d.config.PropagationTimeout, d.config.PollingInterval
}

Some files were not shown because too many files have changed in this diff Show more