1
0
Fork 0

Update lego.

This commit is contained in:
Fernandez Ludovic 2019-06-11 23:11:17 +02:00 committed by Traefiker Bot
parent 63c3ed3931
commit 0034bef6b9
27 changed files with 1663 additions and 14 deletions

View file

@ -5,7 +5,7 @@ package sender
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/2.5.0"
ourUserAgent = "xenolf-acme/2.6.0"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release

View file

@ -464,6 +464,33 @@ func (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) {
return ocspResBytes, ocspRes, nil
}
// Get attempts to fetch the certificate at the supplied URL.
// The URL is the same as what would normally be supplied at the Resource's CertURL.
//
// The returned Resource will not have the PrivateKey and CSR fields populated as these will not be available.
//
// If bundle is true, the Certificate field in the returned Resource includes the issuer certificate.
func (c *Certifier) Get(url string, bundle bool) (*Resource, error) {
cert, issuer, err := c.core.Certificates.Get(url, bundle)
if err != nil {
return nil, err
}
// Parse the returned cert bundle so that we can grab the domain from the common name.
x509Certs, err := certcrypto.ParsePEMBundle(cert)
if err != nil {
return nil, err
}
return &Resource{
Domain: x509Certs[0].Subject.CommonName,
Certificate: cert,
IssuerCertificate: issuer,
CertURL: url,
CertStableURL: url,
}, nil
}
func checkOrderStatus(order acme.Order) (bool, error) {
switch order.Status {
case acme.StatusValid:

View file

@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"os"
"time"
)
const (
@ -50,3 +51,9 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
return nil
}
// Sequential All DNS challenges for this provider will be resolved sequentially.
// Returns the interval between each iteration.
func (d *DNSProviderManual) Sequential() time.Duration {
return DefaultPropagationTimeout
}

View file

@ -159,5 +159,5 @@ func GetOrFile(envVar string) string {
return ""
}
return string(fileContents)
return strings.TrimSuffix(string(fileContents), "\n")
}

View file

@ -0,0 +1,99 @@
// Package bindman implements a DNS provider for solving the DNS-01 challenge.
package bindman
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/platform/config/env"
"github.com/labbsr0x/bindman-dns-webhook/src/client"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
PropagationTimeout time.Duration
PollingInterval time.Duration
BaseURL string
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("BINDMAN_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("BINDMAN_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("BINDMAN_HTTP_TIMEOUT", time.Minute),
},
}
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
// Bindman's Address Manager REST API to manage TXT records for a domain.
type DNSProvider struct {
config *Config
client *client.DNSWebhookClient
}
// NewDNSProvider returns a DNSProvider instance configured for Bindman.
// BINDMAN_MANAGER_ADDRESS should have the scheme, hostname, and port (if required) of the authoritative Bindman Manager server.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("BINDMAN_MANAGER_ADDRESS")
if err != nil {
return nil, fmt.Errorf("bindman: %v", err)
}
config := NewDefaultConfig()
config.BaseURL = values["BINDMAN_MANAGER_ADDRESS"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Bindman.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("bindman: the configuration of the DNS provider is nil")
}
if config.BaseURL == "" {
return nil, fmt.Errorf("bindman: bindman manager address missing")
}
bClient, err := client.New(config.BaseURL, config.HTTPClient)
if err != nil {
return nil, fmt.Errorf("bindman: %v", err)
}
return &DNSProvider{config: config, client: bClient}, 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)
if err := d.client.AddRecord(fqdn, "TXT", value); err != nil {
return fmt.Errorf("bindman: %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)
if err := d.client.RemoveRecord(fqdn, "TXT"); err != nil {
return fmt.Errorf("bindman: %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

@ -57,10 +57,10 @@ func (d *DNSProvider) removeTxtRecord(domain string, recordID int) error {
return nil
}
func (d *DNSProvider) addTxtRecord(domain, fqdn, value string) (*txtRecordResponse, error) {
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
func (d *DNSProvider) addTxtRecord(fqdn, value string) (*txtRecordResponse, error) {
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(fqdn))
if err != nil {
return nil, fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
return nil, fmt.Errorf("could not determine zone for domain: '%s'. %s", fqdn, err)
}
reqData := record{Type: "TXT", Name: fqdn, Data: value, TTL: d.config.TTL}

View file

@ -88,7 +88,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
respData, err := d.addTxtRecord(domain, fqdn, value)
respData, err := d.addTxtRecord(fqdn, value)
if err != nil {
return fmt.Errorf("digitalocean: %v", err)
}
@ -104,6 +104,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
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("digitalocean: %v", err)
}
// get the record's unique ID from when we created it
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[fqdn]
@ -112,7 +117,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("digitalocean: unknown record ID for '%s'", fqdn)
}
err := d.removeTxtRecord(domain, recordID)
err = d.removeTxtRecord(authZone, recordID)
if err != nil {
return fmt.Errorf("digitalocean: %v", err)
}

View file

@ -9,6 +9,7 @@ import (
"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/bindman"
"github.com/go-acme/lego/providers/dns/bluecat"
"github.com/go-acme/lego/providers/dns/cloudflare"
"github.com/go-acme/lego/providers/dns/cloudns"
@ -23,6 +24,7 @@ import (
"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/easydns"
"github.com/go-acme/lego/providers/dns/exec"
"github.com/go-acme/lego/providers/dns/exoscale"
"github.com/go-acme/lego/providers/dns/fastdns"
@ -35,6 +37,7 @@ import (
"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/joker"
"github.com/go-acme/lego/providers/dns/lightsail"
"github.com/go-acme/lego/providers/dns/linode"
"github.com/go-acme/lego/providers/dns/linodev4"
@ -72,6 +75,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return azure.NewDNSProvider()
case "auroradns":
return auroradns.NewDNSProvider()
case "bindman":
return bindman.NewDNSProvider()
case "bluecat":
return bluecat.NewDNSProvider()
case "cloudflare":
@ -102,6 +107,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return dyn.NewDNSProvider()
case "fastdns":
return fastdns.NewDNSProvider()
case "easydns":
return easydns.NewDNSProvider()
case "exec":
return exec.NewDNSProvider()
case "exoscale":
@ -124,6 +131,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return iij.NewDNSProvider()
case "inwx":
return inwx.NewDNSProvider()
case "joker":
return joker.NewDNSProvider()
case "lightsail":
return lightsail.NewDNSProvider()
case "linode":

View file

@ -0,0 +1,97 @@
package easydns
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"path"
)
const defaultEndpoint = "https://rest.easydns.net"
type zoneRecord struct {
ID string `json:"id,omitempty"`
Domain string `json:"domain"`
Host string `json:"host"`
TTL string `json:"ttl"`
Prio string `json:"prio"`
Type string `json:"type"`
Rdata string `json:"rdata"`
LastMod string `json:"last_mod,omitempty"`
Revoked int `json:"revoked,omitempty"`
NewHost string `json:"new_host,omitempty"`
}
type addRecordResponse struct {
Msg string `json:"msg"`
Tm int `json:"tm"`
Data zoneRecord `json:"data"`
Status int `json:"status"`
}
func (d *DNSProvider) addRecord(domain string, record interface{}) (string, error) {
pathAdd := path.Join("/zones/records/add", domain, "TXT")
response := &addRecordResponse{}
err := d.doRequest(http.MethodPut, pathAdd, record, response)
if err != nil {
return "", err
}
recordID := response.Data.ID
return recordID, nil
}
func (d *DNSProvider) deleteRecord(domain, recordID string) error {
pathDelete := path.Join("/zones/records", domain, recordID)
return d.doRequest(http.MethodDelete, pathDelete, nil, nil)
}
func (d *DNSProvider) doRequest(method, path string, requestMsg, responseMsg interface{}) error {
reqBody := &bytes.Buffer{}
if requestMsg != nil {
err := json.NewEncoder(reqBody).Encode(requestMsg)
if err != nil {
return err
}
}
endpoint, err := d.config.Endpoint.Parse(path + "?format=json")
if err != nil {
return err
}
request, err := http.NewRequest(method, endpoint.String(), reqBody)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
request.SetBasicAuth(d.config.Token, d.config.Key)
response, err := d.config.HTTPClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode >= http.StatusBadRequest {
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("%d: failed to read response body: %v", response.StatusCode, err)
}
return fmt.Errorf("%d: request failed: %v", response.StatusCode, string(body))
}
if responseMsg != nil {
return json.NewDecoder(response.Body).Decode(responseMsg)
}
return nil
}

View file

@ -0,0 +1,165 @@
// Package easydns implements a DNS provider for solving the DNS-01 challenge using EasyDNS API.
package easydns
import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/miekg/dns"
"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 {
Endpoint *url.URL
Token string
Key string
TTL int
HTTPClient *http.Client
PropagationTimeout time.Duration
PollingInterval time.Duration
SequenceInterval time.Duration
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
PropagationTimeout: env.GetOrDefaultSecond("EASYDNS_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
SequenceInterval: env.GetOrDefaultSecond("EASYDNS_SEQUENCE_INTERVAL", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("EASYDNS_POLLING_INTERVAL", dns01.DefaultPollingInterval),
TTL: env.GetOrDefaultInt("EASYDNS_TTL", dns01.DefaultTTL),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("EASYDNS_HTTP_TIMEOUT", 30*time.Second),
},
}
}
// DNSProvider describes a provider for acme-proxy
type DNSProvider struct {
config *Config
recordIDs map[string]string
recordIDsMu sync.Mutex
}
// NewDNSProvider returns a DNSProvider instance.
func NewDNSProvider() (*DNSProvider, error) {
config := NewDefaultConfig()
endpoint, err := url.Parse(env.GetOrDefaultString("EASYDNS_ENDPOINT", defaultEndpoint))
if err != nil {
return nil, fmt.Errorf("easydns: %v", err)
}
config.Endpoint = endpoint
values, err := env.Get("EASYDNS_TOKEN", "EASYDNS_KEY")
if err != nil {
return nil, fmt.Errorf("easydns: %v", err)
}
config.Token = values["EASYDNS_TOKEN"]
config.Key = values["EASYDNS_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider .
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("easydns: the configuration of the DNS provider is nil")
}
if config.Token == "" {
return nil, errors.New("easydns: the API token is missing")
}
if config.Key == "" {
return nil, errors.New("easydns: the API key is missing")
}
return &DNSProvider{config: config, recordIDs: map[string]string{}}, 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)
apiHost, apiDomain := splitFqdn(fqdn)
record := &zoneRecord{
Domain: apiDomain,
Host: apiHost,
Type: "TXT",
Rdata: value,
TTL: strconv.Itoa(d.config.TTL),
Prio: "0",
}
recordID, err := d.addRecord(apiDomain, record)
if err != nil {
return fmt.Errorf("easydns: error adding zone record: %v", err)
}
key := getMapKey(fqdn, value)
d.recordIDsMu.Lock()
d.recordIDs[key] = recordID
d.recordIDsMu.Unlock()
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, challenge := dns01.GetRecord(domain, keyAuth)
key := getMapKey(fqdn, challenge)
recordID, exists := d.recordIDs[key]
if !exists {
return nil
}
_, apiDomain := splitFqdn(fqdn)
err := d.deleteRecord(apiDomain, recordID)
d.recordIDsMu.Lock()
defer delete(d.recordIDs, key)
d.recordIDsMu.Unlock()
if err != nil {
return fmt.Errorf("easydns: %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
}
// 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 splitFqdn(fqdn string) (host, domain string) {
parts := dns.SplitDomainName(fqdn)
length := len(parts)
host = strings.Join(parts[0:length-2], ".")
domain = strings.Join(parts[length-2:length], ".")
return
}
func getMapKey(fqdn, value string) string {
return fqdn + "|" + value
}

View file

@ -105,3 +105,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
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.PropagationTimeout
}

View file

@ -156,7 +156,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZone(domain)
zone, err := d.getHostedZone(fqdn)
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
@ -264,7 +264,7 @@ func (d *DNSProvider) applyChanges(zone string, change *dns.Change) error {
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := d.getHostedZone(domain)
zone, err := d.getHostedZone(fqdn)
if err != nil {
return fmt.Errorf("googlecloud: %v", err)
}
@ -311,7 +311,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) {
}
for _, z := range zones.ManagedZones {
if z.Visibility == "public" {
if z.Visibility == "public" || z.Visibility == "" {
return z.Name, nil
}
}

View file

@ -0,0 +1,197 @@
package joker
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/go-acme/lego/challenge/dns01"
"github.com/go-acme/lego/log"
)
const defaultBaseURL = "https://dmapi.joker.com/request/"
// Joker DMAPI Response
type response struct {
Headers url.Values
Body string
StatusCode int
StatusText string
AuthSid string
}
// parseResponse parses HTTP response body
func parseResponse(message string) *response {
r := &response{Headers: url.Values{}, StatusCode: -1}
parts := strings.SplitN(message, "\n\n", 2)
for _, line := range strings.Split(parts[0], "\n") {
if strings.TrimSpace(line) == "" {
continue
}
kv := strings.SplitN(line, ":", 2)
val := ""
if len(kv) == 2 {
val = strings.TrimSpace(kv[1])
}
r.Headers.Add(kv[0], val)
switch kv[0] {
case "Status-Code":
i, err := strconv.Atoi(val)
if err == nil {
r.StatusCode = i
}
case "Status-Text":
r.StatusText = val
case "Auth-Sid":
r.AuthSid = val
}
}
if len(parts) > 1 {
r.Body = parts[1]
}
return r
}
// login performs a login to Joker's DMAPI
func (d *DNSProvider) login() (*response, error) {
if d.config.AuthSid != "" {
// already logged in
return nil, nil
}
response, err := d.postRequest("login", url.Values{"api-key": {d.config.APIKey}})
if err != nil {
return response, err
}
if response == nil {
return nil, fmt.Errorf("login returned nil response")
}
if response.AuthSid == "" {
return response, fmt.Errorf("login did not return valid Auth-Sid")
}
d.config.AuthSid = response.AuthSid
return response, nil
}
// logout closes authenticated session with Joker's DMAPI
func (d *DNSProvider) logout() (*response, error) {
if d.config.AuthSid == "" {
return nil, fmt.Errorf("already logged out")
}
response, err := d.postRequest("logout", url.Values{})
if err == nil {
d.config.AuthSid = ""
}
return response, err
}
// getZone returns content of DNS zone for domain
func (d *DNSProvider) getZone(domain string) (*response, error) {
if d.config.AuthSid == "" {
return nil, fmt.Errorf("must be logged in to get zone")
}
return d.postRequest("dns-zone-get", url.Values{"domain": {dns01.UnFqdn(domain)}})
}
// putZone uploads DNS zone to Joker DMAPI
func (d *DNSProvider) putZone(domain, zone string) (*response, error) {
if d.config.AuthSid == "" {
return nil, fmt.Errorf("must be logged in to put zone")
}
return d.postRequest("dns-zone-put", url.Values{"domain": {dns01.UnFqdn(domain)}, "zone": {strings.TrimSpace(zone)}})
}
// postRequest performs actual HTTP request
func (d *DNSProvider) postRequest(cmd string, data url.Values) (*response, error) {
uri := d.config.BaseURL + cmd
if d.config.AuthSid != "" {
data.Set("auth-sid", d.config.AuthSid)
}
if d.config.Debug {
log.Infof("postRequest:\n\tURL: %q\n\tData: %v", uri, data)
}
resp, err := d.config.HTTPClient.PostForm(uri, data)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP error %d [%s]: %v", resp.StatusCode, http.StatusText(resp.StatusCode), string(body))
}
return parseResponse(string(body)), nil
}
// Temporary workaround, until it get fixed on API side
func fixTxtLines(line string) string {
fields := strings.Fields(line)
if len(fields) < 6 || fields[1] != "TXT" {
return line
}
if fields[3][0] == '"' && fields[4] == `"` {
fields[3] = strings.TrimSpace(fields[3]) + `"`
fields = append(fields[:4], fields[5:]...)
}
return strings.Join(fields, " ")
}
// removeTxtEntryFromZone clean-ups all TXT records with given name
func removeTxtEntryFromZone(zone, relative string) (string, bool) {
prefix := fmt.Sprintf("%s TXT 0 ", relative)
modified := false
var zoneEntries []string
for _, line := range strings.Split(zone, "\n") {
if strings.HasPrefix(line, prefix) {
modified = true
continue
}
zoneEntries = append(zoneEntries, line)
}
return strings.TrimSpace(strings.Join(zoneEntries, "\n")), modified
}
// addTxtEntryToZone returns DNS zone with added TXT record
func addTxtEntryToZone(zone, relative, value string, ttl int) string {
var zoneEntries []string
for _, line := range strings.Split(zone, "\n") {
zoneEntries = append(zoneEntries, fixTxtLines(line))
}
newZoneEntry := fmt.Sprintf("%s TXT 0 %q %d", relative, value, ttl)
zoneEntries = append(zoneEntries, newZoneEntry)
return strings.TrimSpace(strings.Join(zoneEntries, "\n"))
}

View file

@ -0,0 +1,174 @@
// Package joker implements a DNS provider for solving the DNS-01 challenge using joker.com DMAPI.
package joker
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"
)
// Config is used to configure the creation of the DNSProvider.
type Config struct {
Debug bool
BaseURL string
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
AuthSid string
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
BaseURL: defaultBaseURL,
Debug: env.GetOrDefaultBool("JOKER_DEBUG", false),
TTL: env.GetOrDefaultInt("JOKER_TTL", dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond("JOKER_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond("JOKER_POLLING_INTERVAL", dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond("JOKER_HTTP_TIMEOUT", 60*time.Second),
},
}
}
// DNSProvider is an implementation of the ChallengeProviderTimeout interface
// that uses Joker's DMAPI to manage TXT records for a domain.
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for Joker DMAPI.
// Credentials must be passed in the environment variable JOKER_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("JOKER_API_KEY")
if err != nil {
return nil, fmt.Errorf("joker: %v", err)
}
config := NewDefaultConfig()
config.APIKey = values["JOKER_API_KEY"]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Joker DMAPI.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("joker: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, fmt.Errorf("joker: credentials missing")
}
if !strings.HasSuffix(config.BaseURL, "/") {
config.BaseURL += "/"
}
return &DNSProvider{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 installs a TXT record for the DNS challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %v", err)
}
relative := getRelative(fqdn, zone)
if d.config.Debug {
log.Infof("[%s] joker: adding TXT record %q to zone %q with value %q", domain, relative, zone, value)
}
response, err := d.login()
if err != nil {
return formatResponseError(response, err)
}
response, err = d.getZone(zone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
dnsZone := addTxtEntryToZone(response.Body, relative, value, d.config.TTL)
response, err = d.putZone(zone, dnsZone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
return nil
}
// CleanUp removes a TXT record used for a previous DNS challenge.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %v", err)
}
relative := getRelative(fqdn, zone)
if d.config.Debug {
log.Infof("[%s] joker: removing entry %q from zone %q", domain, relative, zone)
}
response, err := d.login()
if err != nil {
return formatResponseError(response, err)
}
defer func() {
// Try to logout in case of errors
_, _ = d.logout()
}()
response, err = d.getZone(zone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
dnsZone, modified := removeTxtEntryFromZone(response.Body, relative)
if modified {
response, err = d.putZone(zone, dnsZone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
}
response, err = d.logout()
if err != nil {
return formatResponseError(response, err)
}
return nil
}
func getRelative(fqdn, zone string) string {
return dns01.UnFqdn(strings.TrimSuffix(fqdn, dns01.ToFqdn(zone)))
}
// formatResponseError formats error with optional details from DMAPI response
func formatResponseError(response *response, err error) error {
if response != nil {
return fmt.Errorf("joker: DMAPI error: %v Response: %v", err, response.Headers)
}
return fmt.Errorf("joker: DMAPI error: %v", err)
}