ACME DNS challenges

This commit is contained in:
Ludovic Fernandez 2018-10-10 16:28:04 +02:00 committed by Traefiker Bot
parent 7a2592b2fa
commit 5bdf8a5ea3
127 changed files with 24386 additions and 739 deletions

View file

@ -26,6 +26,9 @@ const (
// “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
statusValid = "valid"
statusInvalid = "invalid"
)
// User interface is to be implemented by users of this library.
@ -43,7 +46,7 @@ type solver interface {
// 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 {
type preSolver interface {
PreSolve(challenge challenge, domain string) error
}
@ -502,9 +505,9 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
// Start by checking to see if the certificate was based off a CSR, and
// use that if it's defined.
if len(cert.CSR) > 0 {
csr, err := pemDecodeTox509CSR(cert.CSR)
if err != nil {
return nil, err
csr, errP := pemDecodeTox509CSR(cert.CSR)
if errP != nil {
return nil, errP
}
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
return newCert, failures
@ -537,7 +540,6 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
}
func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) {
var identifiers []identifier
for _, domain := range domains {
identifiers = append(identifiers, identifier{Type: "dns", Value: domain})
@ -577,16 +579,16 @@ func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
// loop through the resources, basically through the domains. First pass just selects a solver for each authz.
for _, authz := range authorizations {
if authz.Status == "valid" {
if authz.Status == statusValid {
// Boulder might recycle recent validated authz (see issue #267)
log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
continue
}
if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil {
if i, solvr := c.chooseSolver(authz, authz.Identifier.Value); solvr != nil {
authSolvers = append(authSolvers, &selectedAuthSolver{
authz: authz,
challengeIndex: i,
solver: solver,
solver: solvr,
})
} else {
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
@ -597,7 +599,7 @@ func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
for _, item := range authSolvers {
authz := item.authz
i := item.challengeIndex
if presolver, ok := item.solver.(presolver); ok {
if presolver, ok := item.solver.(preSolver); ok {
if err := presolver.PreSolve(authz.Challenges[i], authz.Identifier.Value); err != nil {
failures[authz.Identifier.Value] = err
}
@ -607,12 +609,12 @@ func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
defer func() {
// clean all created TXT records
for _, item := range authSolvers {
if cleanup, ok := item.solver.(cleanup); ok {
if clean, ok := item.solver.(cleanup); ok {
if failures[item.authz.Identifier.Value] != nil {
// already failed in previous loop
continue
}
err := cleanup.CleanUp(item.authz.Challenges[item.challengeIndex], item.authz.Identifier.Value)
err := clean.CleanUp(item.authz.Challenges[item.challengeIndex], item.authz.Identifier.Value)
if err != nil {
log.Warnf("Error cleaning up %s: %v ", item.authz.Identifier.Value, err)
}
@ -755,7 +757,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
return nil, err
}
if retOrder.Status == "invalid" {
if retOrder.Status == statusInvalid {
return nil, err
}
@ -765,7 +767,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
PrivateKey: privateKeyPem,
}
if retOrder.Status == "valid" {
if retOrder.Status == statusValid {
// if the certificate is available right away, short cut!
ok, err := c.checkCertResponse(retOrder, &certRes, bundle)
if err != nil {
@ -809,9 +811,8 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
// should already have the Domain (common name) field populated. If bundle is
// true, the certificate will be bundled with the issuer's cert.
func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) {
switch order.Status {
case "valid":
case statusValid:
resp, err := httpGet(order.Certificate)
if err != nil {
return false, err
@ -860,7 +861,7 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou
case "processing":
return false, nil
case "invalid":
case statusInvalid:
return false, errors.New("order has invalid state: invalid")
default:
return false, nil
@ -922,12 +923,12 @@ func validate(j *jws, domain, uri string, c challenge) error {
// Repeatedly check the server for an updated status on our request.
for {
switch chlng.Status {
case "valid":
case statusValid:
log.Infof("[%s] The server validated our request", domain)
return nil
case "pending":
case "processing":
case "invalid":
case statusInvalid:
return handleChallengeError(chlng)
default:
return errors.New("the server returned an unexpected state")

View file

@ -81,20 +81,20 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
return nil, nil, errors.New("no issuing certificate URL")
}
resp, err := httpGet(issuedCert.IssuingCertificateURL[0])
if err != nil {
return nil, nil, err
resp, errC := httpGet(issuedCert.IssuingCertificateURL[0])
if errC != nil {
return nil, nil, errC
}
defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
if err != nil {
return nil, nil, err
issuerBytes, errC := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
if errC != nil {
return nil, nil, errC
}
issuerCert, err := x509.ParseCertificate(issuerBytes)
if err != nil {
return nil, nil, err
issuerCert, errC := x509.ParseCertificate(issuerBytes)
if errC != nil {
return nil, nil, errC
}
// Insert it into the slice on position 0
@ -258,15 +258,6 @@ func pemDecode(data []byte) (*pem.Block, error) {
return pemBlock, nil
}
func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {
return nil, err
}
return x509.ParseCertificate(pemBlock.Bytes)
}
func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {

View file

@ -196,7 +196,7 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro
}
if !found {
return false, fmt.Errorf("NS %s did not return the expected TXT record", ns)
return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s]", ns, fqdn)
}
}

View file

@ -148,29 +148,25 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) {
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
jsonBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.New("Failed to marshal network message")
return nil, errors.New("failed to marshal network message")
}
resp, err := j.post(uri, jsonBytes)
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
return nil, fmt.Errorf("failed to post JWS message. -> %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
err := handleHTTPError(resp)
err = handleHTTPError(resp)
switch err.(type) {
case NonceError:
// Retry once if the nonce was invalidated
retryResp, err := j.post(uri, jsonBytes)
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
retryResp, errP := j.post(uri, jsonBytes)
if errP != nil {
return nil, fmt.Errorf("failed to post JWS message. -> %v", errP)
}
defer retryResp.Body.Close()

View file

@ -35,7 +35,7 @@ func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
var err error
s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
if err != nil {
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
return fmt.Errorf("could not start HTTP server for challenge -> %v", err)
}
s.done = make(chan bool)
@ -62,20 +62,31 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
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")
w.Write([]byte(keyAuth))
_, 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)
w.Write([]byte("TEST"))
_, err := w.Write([]byte("TEST"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
})
httpServer := &http.Server{
Handler: mux,
}
httpServer := &http.Server{Handler: mux}
// Once httpServer is shut down we don't want any lingering
// connections, so disable KeepAlives.
httpServer.SetKeepAlivesEnabled(false)
httpServer.Serve(s.listener)
err := httpServer.Serve(s.listener)
if err != nil {
log.Println(err)
}
s.done <- true
}

View file

@ -3,6 +3,7 @@ package acme
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
)
@ -65,7 +66,10 @@ func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error {
// Shut the server down when we're finished.
go func() {
http.Serve(t.listener, nil)
err := http.Serve(t.listener, nil)
if err != nil {
log.Println(err)
}
}()
return nil

View file

@ -3,16 +3,20 @@ package acme
import (
"fmt"
"time"
"github.com/xenolf/lego/log"
)
// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
log.Infof("Wait [timeout: %s, interval: %s]", timeout, interval)
var lastErr string
timeup := time.After(timeout)
timeUp := time.After(timeout)
for {
select {
case <-timeup:
return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr)
case <-timeUp:
return fmt.Errorf("time limit exceeded: last error: %s", lastErr)
default:
}

View file

@ -1,11 +1,15 @@
package env
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
"github.com/xenolf/lego/log"
)
// Get environment variables
@ -14,7 +18,7 @@ func Get(names ...string) (map[string]string, error) {
var missingEnvVars []string
for _, envVar := range names {
value := os.Getenv(envVar)
value := GetOrFile(envVar)
if value == "" {
missingEnvVars = append(missingEnvVars, envVar)
}
@ -28,10 +32,72 @@ func Get(names ...string) (map[string]string, error) {
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(os.Getenv(envVar))
v, err := strconv.Atoi(GetOrFile(envVar))
if err != nil {
return defaultValue
}
@ -53,7 +119,7 @@ func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration
// 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 := os.Getenv(envVar)
v := GetOrFile(envVar)
if len(v) == 0 {
return defaultValue
}
@ -64,10 +130,34 @@ func GetOrDefaultString(envVar string, defaultValue string) string {
// 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(os.Getenv(envVar))
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)
}

View file

@ -1,6 +1,5 @@
// 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 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 (
@ -101,7 +100,7 @@ func (e ErrCNAMERequired) Error() string {
e.Domain, e.Domain, e.FQDN, e.Target)
}
// Present creates a TXT record to fulfil the DNS-01 challenge. If there is an
// 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

View file

@ -5,7 +5,6 @@ package alidns
import (
"errors"
"fmt"
"os"
"strings"
"time"
@ -57,7 +56,7 @@ func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.APIKey = values["ALICLOUD_ACCESS_KEY"]
config.SecretKey = values["ALICLOUD_SECRET_KEY"]
config.RegionID = os.Getenv("ALICLOUD_REGION_ID")
config.RegionID = env.GetOrFile("ALICLOUD_REGION_ID")
return NewDNSProviderConfig(config)
}
@ -105,7 +104,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -3,7 +3,6 @@ package auroradns
import (
"errors"
"fmt"
"os"
"sync"
"time"
@ -53,7 +52,7 @@ func NewDNSProvider() (*DNSProvider, error) {
}
config := NewDefaultConfig()
config.BaseURL = os.Getenv("AURORA_ENDPOINT")
config.BaseURL = env.GetOrFile("AURORA_ENDPOINT")
config.UserID = values["AURORA_USER_ID"]
config.Key = values["AURORA_KEY"]

View file

@ -99,7 +99,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
// 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, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -353,7 +353,7 @@ func (d *DNSProvider) lookupViewID(viewName string) (uint, error) {
queryArgs := map[string]string{
"parentId": strconv.FormatUint(uint64(confID), 10),
"name": d.config.DNSView,
"name": viewName,
"type": viewType,
}

View file

@ -1,212 +0,0 @@
package cloudflare
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/xenolf/lego/acme"
)
// defaultBaseURL represents the API endpoint to call.
const defaultBaseURL = "https://api.cloudflare.com/client/v4"
// APIError contains error details for failed requests
type APIError struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
ErrorChain []APIError `json:"error_chain,omitempty"`
}
// APIResponse represents a response from Cloudflare API
type APIResponse struct {
Success bool `json:"success"`
Errors []*APIError `json:"errors"`
Result json.RawMessage `json:"result"`
}
// TxtRecord represents a Cloudflare DNS record
type TxtRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
ID string `json:"id,omitempty"`
TTL int `json:"ttl,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
}
// HostedZone represents a Cloudflare DNS zone
type HostedZone struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Client Cloudflare API client
type Client struct {
authEmail string
authKey string
BaseURL string
HTTPClient *http.Client
}
// NewClient create a Cloudflare API client
func NewClient(authEmail string, authKey string) (*Client, error) {
if authEmail == "" {
return nil, errors.New("cloudflare: some credentials information are missing: email")
}
if authKey == "" {
return nil, errors.New("cloudflare: some credentials information are missing: key")
}
return &Client{
authEmail: authEmail,
authKey: authKey,
BaseURL: defaultBaseURL,
HTTPClient: http.DefaultClient,
}, nil
}
// GetHostedZoneID get hosted zone
func (c *Client) GetHostedZoneID(fqdn string) (string, error) {
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
result, err := c.doRequest(http.MethodGet, "/zones?name="+acme.UnFqdn(authZone), nil)
if err != nil {
return "", err
}
var hostedZone []HostedZone
err = json.Unmarshal(result, &hostedZone)
if err != nil {
return "", fmt.Errorf("cloudflare: HostedZone unmarshaling error: %v", err)
}
count := len(hostedZone)
if count == 0 {
return "", fmt.Errorf("cloudflare: zone %s not found for domain %s", authZone, fqdn)
} else if count > 1 {
return "", fmt.Errorf("cloudflare: zone %s cannot be find for domain %s: too many hostedZone: %v", authZone, fqdn, hostedZone)
}
return hostedZone[0].ID, nil
}
// FindTxtRecord Find a TXT record
func (c *Client) FindTxtRecord(zoneID, fqdn string) (*TxtRecord, error) {
result, err := c.doRequest(
http.MethodGet,
fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)),
nil,
)
if err != nil {
return nil, err
}
var records []TxtRecord
err = json.Unmarshal(result, &records)
if err != nil {
return nil, fmt.Errorf("cloudflare: record unmarshaling error: %v", err)
}
for _, rec := range records {
fmt.Println(rec.Name, acme.UnFqdn(fqdn))
if rec.Name == acme.UnFqdn(fqdn) {
return &rec, nil
}
}
return nil, fmt.Errorf("cloudflare: no existing record found for %s", fqdn)
}
// AddTxtRecord add a TXT record
func (c *Client) AddTxtRecord(fqdn string, record TxtRecord) error {
zoneID, err := c.GetHostedZoneID(fqdn)
if err != nil {
return err
}
body, err := json.Marshal(record)
if err != nil {
return fmt.Errorf("cloudflare: record marshaling error: %v", err)
}
_, err = c.doRequest(http.MethodPost, fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
return err
}
// RemoveTxtRecord Remove a TXT record
func (c *Client) RemoveTxtRecord(fqdn string) error {
zoneID, err := c.GetHostedZoneID(fqdn)
if err != nil {
return err
}
record, err := c.FindTxtRecord(zoneID, fqdn)
if err != nil {
return err
}
_, err = c.doRequest(http.MethodDelete, fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
return err
}
func (c *Client) doRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.BaseURL, uri), body)
if err != nil {
return nil, err
}
req.Header.Set("X-Auth-Email", c.authEmail)
req.Header.Set("X-Auth-Key", c.authKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cloudflare: error querying API: %v", err)
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("cloudflare: %s", toUnreadableBodyMessage(req, content))
}
var r APIResponse
err = json.Unmarshal(content, &r)
if err != nil {
return nil, fmt.Errorf("cloudflare: APIResponse unmarshaling error: %v: %s", err, toUnreadableBodyMessage(req, content))
}
if !r.Success {
if len(r.Errors) > 0 {
return nil, fmt.Errorf("cloudflare: error \n%s", toError(r))
}
return nil, fmt.Errorf("cloudflare: %s", toUnreadableBodyMessage(req, content))
}
return r.Result, 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))
}
func toError(r APIResponse) error {
errStr := ""
for _, apiErr := range r.Errors {
errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
for _, chainErr := range apiErr.ErrorChain {
errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
}
}
return fmt.Errorf("cloudflare: error \n%s", errStr)
}

View file

@ -8,12 +8,18 @@ import (
"net/http"
"time"
"github.com/cloudflare/cloudflare-go"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/log"
"github.com/xenolf/lego/platform/config/env"
)
// CloudFlareAPIURL represents the API endpoint to call.
const CloudFlareAPIURL = defaultBaseURL // Deprecated
const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4" // Deprecated
const (
minTTL = 120
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
@ -28,7 +34,7 @@ type Config struct {
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("CLOUDFLARE_TTL", 120),
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{
@ -39,7 +45,7 @@ func NewDefaultConfig() *Config {
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
client *Client
client *cloudflare.API
config *Config
}
@ -47,7 +53,9 @@ type DNSProvider struct {
// Credentials must be passed in the environment variables:
// CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("CLOUDFLARE_EMAIL", "CLOUDFLARE_API_KEY")
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)
}
@ -76,45 +84,92 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("cloudflare: the configuration of the DNS provider is nil")
}
client, err := NewClient(config.AuthEmail, config.AuthKey)
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
}
client.HTTPClient = config.HTTPClient
// TODO: must be remove. keep only for compatibility reason.
client.BaseURL = CloudFlareAPIURL
return &DNSProvider{
client: client,
config: config,
}, nil
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.
// 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 fulfil the dns-01 challenge
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
rec := TxtRecord{
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("cloudflare: %v", err)
}
zoneID, err := d.client.ZoneIDByName(authZone)
if err != nil {
return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err)
}
dnsRecord := cloudflare.DNSRecord{
Type: "TXT",
Name: acme.UnFqdn(fqdn),
Content: value,
TTL: d.config.TTL,
}
return d.client.AddTxtRecord(fqdn, rec)
response, _ := 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, _, _ := acme.DNS01Record(domain, keyAuth)
return d.client.RemoveTxtRecord(fqdn)
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("cloudflare: %v", err)
}
zoneID, err := d.client.ZoneIDByName(authZone)
if err != nil {
return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err)
}
dnsRecord := cloudflare.DNSRecord{
Type: "TXT",
Name: acme.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

@ -28,8 +28,8 @@ func NewDefaultConfig() *Config {
client.Timeout = time.Second * time.Duration(env.GetOrDefaultInt("CLOUDXNS_HTTP_TIMEOUT", 30))
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("AKAMAI_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("AKAMAI_POLLING_INTERVAL", acme.DefaultPollingInterval),
PropagationTimeout: env.GetOrDefaultSecond("CLOUDXNS_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("CLOUDXNS_POLLING_INTERVAL", acme.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("CLOUDXNS_TTL", 120),
HTTPClient: &client,
}
@ -79,7 +79,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: client}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -29,6 +29,7 @@ import (
"github.com/xenolf/lego/providers/dns/iij"
"github.com/xenolf/lego/providers/dns/lightsail"
"github.com/xenolf/lego/providers/dns/linode"
"github.com/xenolf/lego/providers/dns/linodev4"
"github.com/xenolf/lego/providers/dns/namecheap"
"github.com/xenolf/lego/providers/dns/namedotcom"
"github.com/xenolf/lego/providers/dns/netcup"
@ -76,6 +77,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
return dyn.NewDNSProvider()
case "fastdns":
return fastdns.NewDNSProvider()
case "exec":
return exec.NewDNSProvider()
case "exoscale":
return exoscale.NewDNSProvider()
case "gandi":
@ -96,6 +99,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
return lightsail.NewDNSProvider()
case "linode":
return linode.NewDNSProvider()
case "linodev4":
return linodev4.NewDNSProvider()
case "manual":
return acme.NewDNSProviderManual()
case "namecheap":
@ -106,6 +111,14 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
return netcup.NewDNSProvider()
case "nifcloud":
return nifcloud.NewDNSProvider()
case "ns1":
return ns1.NewDNSProvider()
case "otc":
return otc.NewDNSProvider()
case "ovh":
return ovh.NewDNSProvider()
case "pdns":
return pdns.NewDNSProvider()
case "rackspace":
return rackspace.NewDNSProvider()
case "route53":
@ -114,20 +127,10 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
return rfc2136.NewDNSProvider()
case "sakuracloud":
return sakuracloud.NewDNSProvider()
case "vultr":
return vultr.NewDNSProvider()
case "ovh":
return ovh.NewDNSProvider()
case "pdns":
return pdns.NewDNSProvider()
case "ns1":
return ns1.NewDNSProvider()
case "otc":
return otc.NewDNSProvider()
case "exec":
return exec.NewDNSProvider()
case "vegadns":
return vegadns.NewDNSProvider()
case "vultr":
return vultr.NewDNSProvider()
default:
return nil, fmt.Errorf("unrecognised DNS provider: %s", name)
}

View file

@ -5,7 +5,6 @@ package dnsimple
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
@ -45,8 +44,8 @@ type DNSProvider struct {
// See: https://developer.dnsimple.com/v2/#authentication
func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.AccessToken = os.Getenv("DNSIMPLE_OAUTH_TOKEN")
config.BaseURL = os.Getenv("DNSIMPLE_BASE_URL")
config.AccessToken = env.GetOrFile("DNSIMPLE_OAUTH_TOKEN")
config.BaseURL = env.GetOrFile("DNSIMPLE_BASE_URL")
return NewDNSProviderConfig(config)
}
@ -79,27 +78,27 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
client.BaseURL = config.BaseURL
}
return &DNSProvider{client: client}, nil
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneName, err := d.getHostedZone(domain)
if err != nil {
return err
return fmt.Errorf("dnsimple: %v", err)
}
accountID, err := d.getAccountID()
if err != nil {
return err
return fmt.Errorf("dnsimple: %v", err)
}
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, d.config.TTL)
recordAttributes := newTxtRecord(zoneName, fqdn, value, d.config.TTL)
_, err = d.client.Zones.CreateRecord(accountID, zoneName, recordAttributes)
if err != nil {
return fmt.Errorf("API call failed: %v", err)
return fmt.Errorf("dnsimple: API call failed: %v", err)
}
return nil
@ -111,22 +110,23 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
records, err := d.findTxtRecords(domain, fqdn)
if err != nil {
return err
return fmt.Errorf("dnsimple: %v", err)
}
accountID, err := d.getAccountID()
if err != nil {
return err
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 {
return err
lastErr = fmt.Errorf("dnsimple: %v", err)
}
}
return nil
return lastErr
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
@ -178,18 +178,18 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnsimple.ZoneRecord
return nil, err
}
recordName := d.extractRecordName(fqdn, zoneName)
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 []dnsimple.ZoneRecord{}, fmt.Errorf("API call has failed: %v", err)
return nil, fmt.Errorf("API call has failed: %v", err)
}
return result.Data, nil
}
func (d *DNSProvider) newTxtRecord(zoneName, fqdn, value string, ttl int) dnsimple.ZoneRecord {
name := d.extractRecordName(fqdn, zoneName)
func newTxtRecord(zoneName, fqdn, value string, ttl int) dnsimple.ZoneRecord {
name := extractRecordName(fqdn, zoneName)
return dnsimple.ZoneRecord{
Type: "TXT",
@ -199,7 +199,7 @@ func (d *DNSProvider) newTxtRecord(zoneName, fqdn, value string, ttl int) dnsimp
}
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
func extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]

View file

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
@ -57,7 +56,7 @@ func NewDNSProvider() (*DNSProvider, error) {
}
var baseURL string
if sandbox, _ := strconv.ParseBool(os.Getenv("DNSMADEEASY_SANDBOX")); sandbox {
if sandbox, _ := strconv.ParseBool(env.GetOrFile("DNSMADEEASY_SANDBOX")); sandbox {
baseURL = "https://api.sandbox.dnsmadeeasy.com/V2.0"
} else {
baseURL = "https://api.dnsmadeeasy.com/V2.0"

View file

@ -28,8 +28,8 @@ type Config struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("DNSPOD_TTL", 600),
PropagationTimeout: env.GetOrDefaultSecond("ALICLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("ALICLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval),
PropagationTimeout: env.GetOrDefaultSecond("DNSPOD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("DNSPOD_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("DNSPOD_HTTP_TIMEOUT", 0),
},
@ -84,7 +84,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: client}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneID, zoneName, err := d.getHostedZone(domain)

View file

@ -75,7 +75,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
return updateTxtRecord(domain, d.config.Token, txtRecord, false)

View file

@ -59,7 +59,7 @@ func NewDNSProviderProgram(program string) (*DNSProvider, error) {
return NewDNSProviderConfig(&Config{Program: program})
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// 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" {

View file

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/exoscale/egoscale"
@ -56,7 +55,7 @@ func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.APIKey = values["EXOSCALE_API_KEY"]
config.APISecret = values["EXOSCALE_API_SECRET"]
config.Endpoint = os.Getenv("EXOSCALE_ENDPOINT")
config.Endpoint = env.GetOrFile("EXOSCALE_ENDPOINT")
return NewDNSProviderConfig(config)
}
@ -93,7 +92,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, recordName, err := d.FindZoneAndRecordName(fqdn, domain)

View file

@ -99,10 +99,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
}
record := configdns.NewTxtRecord()
record.SetField("name", recordName)
record.SetField("ttl", d.config.TTL)
record.SetField("target", value)
record.SetField("active", true)
_ = record.SetField("name", recordName)
_ = record.SetField("ttl", d.config.TTL)
_ = record.SetField("target", value)
_ = record.SetField("active", true)
existingRecord := d.findExistingRecord(zone, recordName)
@ -110,8 +110,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
if reflect.DeepEqual(existingRecord.ToMap(), record.ToMap()) {
return nil
}
zone.RemoveRecord(existingRecord)
return d.createRecord(zone, record)
err = zone.RemoveRecord(existingRecord)
if err != nil {
return fmt.Errorf("fastdns: %v", err)
}
}
return d.createRecord(zone, record)

View file

@ -82,8 +82,6 @@ type responseBool struct {
Value bool `xml:"params>param>value>boolean"`
}
// POSTing/Marshalling/Unmarshalling
type rpcError struct {
faultCode int
faultString string

View file

@ -230,9 +230,9 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
}
// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by
// marshalling the data given in the call argument to XML and sending
// that via HTTP Post to Gandi. The response is then unmarshalled into
// the resp argument.
// 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, "", " ")

View file

@ -1,18 +1,123 @@
package gandiv5
// types for JSON method calls and parameters
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type addFieldRequest struct {
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"`
}
type deleteFieldRequest struct {
Delete bool `json:"delete"`
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
}
// types for JSON responses
func (d *DNSProvider) do(req *http.Request, v interface{}) error {
if len(d.config.APIKey) > 0 {
req.Header.Set(apiKeyHeader, d.config.APIKey)
}
type responseStruct struct {
Message string `json:"message"`
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

@ -3,8 +3,6 @@
package gandiv5
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
@ -184,61 +182,75 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
// functions to perform API actions
func (d *DNSProvider) addTXTRecord(domain string, name string, value string, ttl int) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
response, err := d.sendRequest(http.MethodPut, target, addFieldRequest{
RRSetTTL: ttl,
RRSetValues: []string{value},
})
if response != nil {
log.Infof("gandiv5: %s", response.Message)
// 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
}
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)
response, err := d.sendRequest(http.MethodDelete, target, deleteFieldRequest{
Delete: true,
})
if response != nil && response.Message == "" {
log.Infof("gandiv5: Zone record deleted")
req, err := d.newRequest(http.MethodDelete, target, nil)
if err != nil {
return err
}
return err
}
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
url := fmt.Sprintf("%s/%s", d.config.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.config.APIKey) > 0 {
req.Header.Set("X-Api-Key", 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)
if err != nil && method != http.MethodDelete {
return nil, err
}
return &response, nil
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
}

View file

@ -122,7 +122,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{config: config, client: svc}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -160,8 +160,6 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// POSTing/Marshalling/Unmarshalling
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
url := fmt.Sprintf("%s/%s", defaultBaseURL, resource)

View file

@ -86,6 +86,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
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
}
@ -103,7 +107,7 @@ func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
return name
}
// Present creates a TXT record to fulfil the dns-01 challenge
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
domainZone, err := d.getZone(fqdn)
@ -111,10 +115,6 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return err
}
if d.config.TTL < minTTL {
d.config.TTL = minTTL
}
recordName := d.extractRecordName(fqdn, domainZone)
rec := []DNSRecord{
{

View file

@ -89,7 +89,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"math/rand"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
@ -54,7 +53,7 @@ type Config struct {
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
DNSZone: os.Getenv("DNS_ZONE"),
DNSZone: env.GetOrFile("DNS_ZONE"),
PropagationTimeout: env.GetOrDefaultSecond("LIGHTSAIL_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("LIGHTSAIL_POLLING_INTERVAL", acme.DefaultPollingInterval),
Region: env.GetOrDefaultString("LIGHTSAIL_REGION", "us-east-1"),
@ -108,7 +107,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
err := d.newTxtRecord(domain, fqdn, `"`+value+`"`)
err := d.newTxtRecord(fqdn, `"`+value+`"`)
if err != nil {
return fmt.Errorf("lightsail: %v", err)
}
@ -141,7 +140,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) newTxtRecord(domain string, fqdn string, value string) error {
func (d *DNSProvider) newTxtRecord(fqdn string, value string) error {
params := &lightsail.CreateDomainEntryInput{
DomainName: aws.String(d.config.DNSZone),
DomainEntry: &lightsail.DomainEntry{

View file

@ -14,7 +14,7 @@ import (
)
const (
dnsMinTTLSecs = 300
minTTL = 300
dnsUpdateFreqMins = 15
dnsUpdateFudgeSecs = 120
)
@ -30,7 +30,7 @@ type Config struct {
func NewDefaultConfig() *Config {
return &Config{
PollingInterval: env.GetOrDefaultSecond("LINODE_POLLING_INTERVAL", 15*time.Second),
TTL: env.GetOrDefaultInt("LINODE_TTL", 60),
TTL: env.GetOrDefaultInt("LINODE_TTL", minTTL),
}
}
@ -79,6 +79,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
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),
@ -96,7 +100,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
minsRemaining := dnsUpdateFreqMins - (time.Now().Minute() % dnsUpdateFreqMins)
timeout = (time.Duration(minsRemaining) * time.Minute) +
(dnsMinTTLSecs * time.Second) +
(minTTL * time.Second) +
(dnsUpdateFudgeSecs * time.Second)
interval = d.config.PollingInterval
return
@ -110,7 +114,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return err
}
if _, err = d.client.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, 60); err != nil {
if _, err = d.client.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, d.config.TTL); err != nil {
return err
}
@ -138,6 +142,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
if err != nil {
return err
}
if resp.ResourceID != resource.ResourceID {
return errors.New("error deleting resource: resource IDs do not match")
}

View file

@ -0,0 +1,191 @@
// 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/linode/linodego"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
"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, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZoneInfo(fqdn)
if err != nil {
return err
}
createOpts := linodego.DomainRecordCreateOptions{
Name: acme.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, _ := acme.DNS01Record(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 := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return nil, err
}
// Query the authority zone.
data, err := json.Marshal(map[string]string{"domain": acme.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

@ -1,11 +1,18 @@
package namecheap
import "encoding/xml"
import (
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
// host describes a DNS record returned by the Namecheap DNS gethosts API.
// 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 host struct {
type Record struct {
Type string `xml:",attr"`
Name string `xml:",attr"`
Address string `xml:",attr"`
@ -32,7 +39,7 @@ type getHostsResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apierror `xml:"Errors>Error"`
Hosts []host `xml:"CommandResponse>DomainDNSGetHostsResult>host"`
Hosts []Record `xml:"CommandResponse>DomainDNSGetHostsResult>host"`
}
type getTldsResponse struct {
@ -42,3 +49,177 @@ type getTldsResponse 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

@ -3,12 +3,10 @@
package namecheap
import (
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -147,22 +145,28 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return fmt.Errorf("namecheap: %v", err)
}
hosts, err := d.getHosts(ch)
records, err := d.getHosts(ch.sld, ch.tld)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
d.addChallengeRecord(ch, &hosts)
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 hosts {
log.Printf(
"%-5.5s %-30.30s %-6s %-70.70s\n",
h.Type, h.Name, h.TTL, h.Address)
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, hosts)
err = d.setHosts(ch.sld, ch.tld, records)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
@ -181,16 +185,25 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("namecheap: %v", err)
}
hosts, err := d.getHosts(ch)
records, err := d.getHosts(ch.sld, ch.tld)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
if removed := d.removeChallengeRecord(ch, &hosts); !removed {
// Find the challenge TXT record and remove it if found.
var found bool
for i, h := range records {
if h.Name == ch.key && h.Type == "TXT" {
records = append(records[:i], records[i+1:]...)
found = true
}
}
if !found {
return nil
}
err = d.setHosts(ch, hosts)
err = d.setHosts(ch.sld, ch.tld, records)
if err != nil {
return fmt.Errorf("namecheap: %v", err)
}
@ -243,189 +256,15 @@ func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, e
host = strings.Join(parts[:longest-1], ".")
}
key, keyValue, _ := acme.DNS01Record(domain, keyAuth)
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
return &challenge{
domain: domain,
key: "_acme-challenge." + host,
keyFqdn: key,
keyValue: keyValue,
keyFqdn: fqdn,
keyValue: value,
tld: tld,
sld: sld,
host: host,
}, nil
}
// setGlobalParams adds the namecheap global parameters to the provided url
// Values record.
func (d *DNSProvider) setGlobalParams(v *url.Values, cmd string) {
v.Set("ApiUser", d.config.APIUser)
v.Set("ApiKey", d.config.APIKey)
v.Set("UserName", d.config.APIUser)
v.Set("Command", cmd)
v.Set("ClientIp", d.config.ClientIP)
}
// getTLDs requests the list of available TLDs from namecheap.
func (d *DNSProvider) getTLDs() (tlds map[string]string, err error) {
values := make(url.Values)
d.setGlobalParams(&values, "namecheap.domains.getTldList")
reqURL, err := url.Parse(d.config.BaseURL)
if err != nil {
return nil, err
}
reqURL.RawQuery = values.Encode()
resp, err := d.config.HTTPClient.Get(reqURL.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("getHosts HTTP error %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var gtr getTldsResponse
if err := xml.Unmarshal(body, &gtr); 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 using the Namecheap API.
func (d *DNSProvider) getHosts(ch *challenge) (hosts []host, err error) {
values := make(url.Values)
d.setGlobalParams(&values, "namecheap.domains.dns.getHosts")
values.Set("SLD", ch.sld)
values.Set("TLD", ch.tld)
reqURL, err := url.Parse(d.config.BaseURL)
if err != nil {
return nil, err
}
reqURL.RawQuery = values.Encode()
resp, err := d.config.HTTPClient.Get(reqURL.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("getHosts HTTP error %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var ghr getHostsResponse
if err = xml.Unmarshal(body, &ghr); 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 using the Namecheap API.
func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error {
values := make(url.Values)
d.setGlobalParams(&values, "namecheap.domains.dns.setHosts")
values.Set("SLD", ch.sld)
values.Set("TLD", ch.tld)
for i, h := range hosts {
ind := fmt.Sprintf("%d", i+1)
values.Add("HostName"+ind, h.Name)
values.Add("RecordType"+ind, h.Type)
values.Add("Address"+ind, h.Address)
values.Add("MXPref"+ind, h.MXPref)
values.Add("TTL"+ind, h.TTL)
}
resp, err := d.config.HTTPClient.PostForm(d.config.BaseURL, values)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("setHosts HTTP error %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
var shr setHostsResponse
if err := xml.Unmarshal(body, &shr); 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
}
// addChallengeRecord adds a DNS challenge TXT record to a list of namecheap
// host records.
func (d *DNSProvider) addChallengeRecord(ch *challenge, hosts *[]host) {
host := host{
Name: ch.key,
Type: "TXT",
Address: ch.keyValue,
MXPref: "10",
TTL: strconv.Itoa(d.config.TTL),
}
// If there's already a TXT record with the same name, replace it.
for i, h := range *hosts {
if h.Name == ch.key && h.Type == "TXT" {
(*hosts)[i] = host
return
}
}
// No record was replaced, so add a new one.
*hosts = append(*hosts, host)
}
// removeChallengeRecord removes a DNS challenge TXT record from a list of
// namecheap host records. Return true if a record was removed.
func (d *DNSProvider) removeChallengeRecord(ch *challenge, hosts *[]host) bool {
// Find the challenge TXT record and remove it if found.
for i, h := range *hosts {
if h.Name == ch.key && h.Type == "TXT" {
*hosts = append((*hosts)[:i], (*hosts)[i+1:]...)
return true
}
}
return false
}

View file

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"
@ -15,6 +14,9 @@ import (
"github.com/xenolf/lego/platform/config/env"
)
// 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
@ -29,7 +31,7 @@ type Config struct {
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt("NAMECOM_TTL", 120),
TTL: env.GetOrDefaultInt("NAMECOM_TTL", minTTL),
PropagationTimeout: env.GetOrDefaultSecond("NAMECOM_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("NAMECOM_POLLING_INTERVAL", acme.DefaultPollingInterval),
HTTPClient: &http.Client{
@ -56,7 +58,7 @@ func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.Username = values["NAMECOM_USERNAME"]
config.APIToken = values["NAMECOM_API_TOKEN"]
config.Server = os.Getenv("NAMECOM_SERVER")
config.Server = env.GetOrFile("NAMECOM_SERVER")
return NewDNSProviderConfig(config)
}
@ -87,6 +89,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
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
@ -97,7 +103,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/xenolf/lego/acme"
@ -52,7 +51,7 @@ func NewDNSProvider() (*DNSProvider, error) {
}
config := NewDefaultConfig()
config.BaseURL = os.Getenv("NIFCLOUD_DNS_ENDPOINT")
config.BaseURL = env.GetOrFile("NIFCLOUD_DNS_ENDPOINT")
config.AccessKey = values["NIFCLOUD_ACCESS_KEY_ID"]
config.SecretKey = values["NIFCLOUD_SECRET_ACCESS_KEY"]

View file

@ -10,6 +10,7 @@ import (
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/log"
"github.com/xenolf/lego/platform/config/env"
"gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
@ -81,7 +82,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
@ -90,10 +91,36 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return fmt.Errorf("ns1: %v", err)
}
record := d.newTxtRecord(zone, fqdn, value, d.config.TTL)
_, err = d.client.Records.Create(record)
if err != nil && err != rest.ErrRecordExists {
return fmt.Errorf("ns1: failed to create record [zone: %q, fqdn: %q]: %v", zone.Zone, fqdn, err)
record, _, err := d.client.Records.Get(zone.Zone, acme.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, acme.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
@ -110,7 +137,10 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
name := acme.UnFqdn(fqdn)
_, err = d.client.Records.Delete(zone.Zone, name, "TXT")
return fmt.Errorf("ns1: failed to delete record [zone: %q, domain: %q]: %v", zone.Zone, name, err)
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.
@ -141,17 +171,3 @@ func getAuthZone(fqdn string) (string, error) {
return strings.TrimSuffix(authZone, "."), nil
}
func (d *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record {
name := acme.UnFqdn(fqdn)
return &dns.Record{
Type: "TXT",
Zone: zone.Zone,
Domain: name,
TTL: ttl,
Answers: []*dns.Answer{
{Rdata: []string{value}},
},
}
}

View file

@ -113,6 +113,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
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
}
@ -124,10 +128,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
if d.config.TTL < minTTL {
d.config.TTL = minTTL
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("otc: %v", err)

View file

@ -114,7 +114,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -110,7 +110,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zone, err := d.getHostedZone(fqdn)

View file

@ -146,7 +146,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
}
// Present creates a TXT record to fulfil the dns-01 challenge
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
zoneID, err := d.getHostedZoneID(fqdn)

View file

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"net"
"os"
"strings"
"time"
@ -62,8 +61,8 @@ func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.Nameserver = values["RFC2136_NAMESERVER"]
config.TSIGKey = os.Getenv("RFC2136_TSIG_KEY")
config.TSIGSecret = os.Getenv("RFC2136_TSIG_SECRET")
config.TSIGKey = env.GetOrFile("RFC2136_TSIG_KEY")
config.TSIGSecret = env.GetOrFile("RFC2136_TSIG_SECRET")
return NewDNSProviderConfig(config)
}

View file

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"math/rand"
"os"
"strings"
"time"
@ -35,7 +34,7 @@ func NewDefaultConfig() *Config {
TTL: env.GetOrDefaultInt("AWS_TTL", 10),
PropagationTimeout: env.GetOrDefaultSecond("AWS_PROPAGATION_TIMEOUT", 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond("AWS_POLLING_INTERVAL", 4*time.Second),
HostedZoneID: os.Getenv("AWS_HOSTED_ZONE_ID"),
HostedZoneID: env.GetOrFile("AWS_HOSTED_ZONE_ID"),
}
}
@ -45,17 +44,16 @@ type DNSProvider struct {
config *Config
}
// 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).
// 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.
// It uses a basic exponential backoff algorithm:
// that returns an initial delay of ~400ms with an upper limit of ~30 seconds,
// which should prevent causing a high number of consecutive throttling errors.
// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.
func (d customRetryer) RetryRules(r *request.Request) time.Duration {
retryCount := r.RetryCount
@ -67,57 +65,81 @@ func (d customRetryer) RetryRules(r *request.Request) time.Duration {
return time.Duration(delay) * time.Millisecond
}
// NewDNSProvider returns a DNSProvider instance configured for the AWS
// Route 53 service.
// NewDNSProvider returns a DNSProvider instance configured for the AWS Route 53 service.
//
// AWS Credentials are automatically detected in the following locations
// and prioritized in the following order:
// AWS Credentials are automatically detected in the following locations and prioritized in the following order:
// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
// AWS_REGION, [AWS_SESSION_TOKEN]
// 2. Shared credentials file (defaults to ~/.aws/credentials)
// 3. Amazon EC2 IAM role
//
// If AWS_HOSTED_ZONE_ID is not set, Lego tries to determine the correct
// public hosted zone via the FQDN.
// If AWS_HOSTED_ZONE_ID is not set, Lego tries to determine the correct 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 takes a given config ans returns a custom configured
// DNSProvider instance
// NewDNSProviderConfig takes a given config ans returns a custom configured DNSProvider instance
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("route53: the configuration of the Route53 DNS provider is nil")
}
r := customRetryer{}
r.NumMaxRetries = config.MaxRetries
sessionCfg := request.WithRetryer(aws.NewConfig(), r)
retry := customRetryer{}
retry.NumMaxRetries = config.MaxRetries
sessionCfg := request.WithRetryer(aws.NewConfig(), retry)
sess, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg})
if err != nil {
return nil, err
}
cl := route53.New(sess)
return &DNSProvider{
client: cl,
config: config,
}, nil
cl := route53.New(sess)
return &DNSProvider{client: cl, config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation.
func (r *DNSProvider) Timeout() (timeout, interval time.Duration) {
return r.config.PropagationTimeout, r.config.PollingInterval
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 (r *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
err := r.changeRecord("UPSERT", fqdn, `"`+value+`"`, r.config.TTL)
hostedZoneID, err := d.getHostedZoneID(fqdn)
if err != nil {
return fmt.Errorf("route53: failed to determine hosted zone ID: %v", err)
}
records, err := d.getExistingRecordSets(hostedZoneID, fqdn)
if err != nil {
return fmt.Errorf("route53: %v", err)
}
realValue := `"` + value + `"`
var found bool
for _, record := range records {
if aws.StringValue(record.Value) == realValue {
found = true
}
}
if !found {
records = append(records, &route53.ResourceRecord{Value: aws.String(realValue)})
}
recordSet := &route53.ResourceRecordSet{
Name: aws.String(fqdn),
Type: aws.String("TXT"),
TTL: aws.Int64(int64(d.config.TTL)),
ResourceRecords: records,
}
err = d.changeRecord(route53.ChangeActionUpsert, hostedZoneID, recordSet)
if err != nil {
return fmt.Errorf("route53: %v", err)
}
@ -125,61 +147,101 @@ func (r *DNSProvider) Present(domain, token, keyAuth string) error {
}
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
err := r.changeRecord("DELETE", fqdn, `"`+value+`"`, r.config.TTL)
hostedZoneID, err := d.getHostedZoneID(fqdn)
if err != nil {
return fmt.Errorf("failed to determine Route 53 hosted zone ID: %v", err)
}
records, err := d.getExistingRecordSets(hostedZoneID, fqdn)
if err != nil {
return fmt.Errorf("route53: %v", err)
}
if len(records) == 0 {
return nil
}
recordSet := &route53.ResourceRecordSet{
Name: aws.String(fqdn),
Type: aws.String("TXT"),
TTL: aws.Int64(int64(d.config.TTL)),
ResourceRecords: records,
}
err = d.changeRecord(route53.ChangeActionDelete, hostedZoneID, recordSet)
if err != nil {
return fmt.Errorf("route53: %v", err)
}
return nil
}
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
hostedZoneID, err := r.getHostedZoneID(fqdn)
if err != nil {
return fmt.Errorf("failed to determine Route 53 hosted zone ID: %v", err)
}
recordSet := newTXTRecordSet(fqdn, value, ttl)
reqParams := &route53.ChangeResourceRecordSetsInput{
func (d *DNSProvider) changeRecord(action, hostedZoneID string, recordSet *route53.ResourceRecordSet) error {
recordSetInput := &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(hostedZoneID),
ChangeBatch: &route53.ChangeBatch{
Comment: aws.String("Managed by Lego"),
Changes: []*route53.Change{
{
Action: aws.String(action),
ResourceRecordSet: recordSet,
},
},
Changes: []*route53.Change{{
Action: aws.String(action),
ResourceRecordSet: recordSet,
}},
},
}
resp, err := r.client.ChangeResourceRecordSets(reqParams)
resp, err := d.client.ChangeResourceRecordSets(recordSetInput)
if err != nil {
return fmt.Errorf("failed to change record set: %v", err)
}
statusID := resp.ChangeInfo.Id
changeID := resp.ChangeInfo.Id
return acme.WaitFor(r.config.PropagationTimeout, r.config.PollingInterval, func() (bool, error) {
reqParams := &route53.GetChangeInput{
Id: statusID,
}
resp, err := r.client.GetChange(reqParams)
return acme.WaitFor(d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
reqParams := &route53.GetChangeInput{Id: changeID}
resp, err := d.client.GetChange(reqParams)
if err != nil {
return false, fmt.Errorf("failed to query change status: %v", err)
}
if aws.StringValue(resp.ChangeInfo.Status) == route53.ChangeStatusInsync {
return true, nil
}
return false, nil
return false, fmt.Errorf("unable to retrieve change: ID=%s", aws.StringValue(changeID))
})
}
func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
if r.config.HostedZoneID != "" {
return r.config.HostedZoneID, nil
func (d *DNSProvider) getExistingRecordSets(hostedZoneID string, fqdn string) ([]*route53.ResourceRecord, error) {
listInput := &route53.ListResourceRecordSetsInput{
HostedZoneId: aws.String(hostedZoneID),
StartRecordName: aws.String(fqdn),
StartRecordType: aws.String("TXT"),
}
recordSetsOutput, err := d.client.ListResourceRecordSets(listInput)
if err != nil {
return nil, err
}
if recordSetsOutput == nil {
return nil, nil
}
var records []*route53.ResourceRecord
for _, recordSet := range recordSetsOutput.ResourceRecordSets {
if aws.StringValue(recordSet.Name) == fqdn {
records = append(records, recordSet.ResourceRecords...)
}
}
return records, nil
}
func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
if d.config.HostedZoneID != "" {
return d.config.HostedZoneID, nil
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
@ -191,7 +253,7 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
reqParams := &route53.ListHostedZonesByNameInput{
DNSName: aws.String(acme.UnFqdn(authZone)),
}
resp, err := r.client.ListHostedZonesByName(reqParams)
resp, err := d.client.ListHostedZonesByName(reqParams)
if err != nil {
return "", err
}
@ -215,14 +277,3 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
return hostedZoneID, nil
}
func newTXTRecordSet(fqdn, value string, ttl int) *route53.ResourceRecordSet {
return &route53.ResourceRecordSet{
Name: aws.String(fqdn),
Type: aws.String("TXT"),
TTL: aws.Int64(int64(ttl)),
ResourceRecords: []*route53.ResourceRecord{
{Value: aws.String(value)},
},
}
}

View file

@ -85,7 +85,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: client}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -5,7 +5,6 @@ package vegadns
import (
"errors"
"fmt"
"os"
"strings"
"time"
@ -50,8 +49,8 @@ func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
config.BaseURL = values["VEGADNS_URL"]
config.APIKey = os.Getenv("SECRET_VEGADNS_KEY")
config.APISecret = os.Getenv("SECRET_VEGADNS_SECRET")
config.APIKey = env.GetOrFile("SECRET_VEGADNS_KEY")
config.APISecret = env.GetOrFile("SECRET_VEGADNS_SECRET")
return NewDNSProviderConfig(config)
}
@ -87,7 +86,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfil the dns-01 challenge
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)

View file

@ -90,7 +90,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{client: client, config: config}, nil
}
// Present creates a TXT record to fulfil the DNS-01 challenge.
// Present creates a TXT record to fulfill the DNS-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)