2018-10-23 09:18:02 +00:00
// Package duckdns implements a DNS provider for solving the DNS-01 challenge using DuckDNS.
2018-04-06 15:04:03 +00:00
// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.
package duckdns
import (
"errors"
"fmt"
"io/ioutil"
2018-09-17 13:16:03 +00:00
"net/http"
2018-10-23 09:18:02 +00:00
"net/url"
"strconv"
"strings"
2018-09-17 13:16:03 +00:00
"time"
2018-04-06 15:04:03 +00:00
2018-10-23 09:18:02 +00:00
"github.com/miekg/dns"
2018-05-31 07:30:04 +00:00
"github.com/xenolf/lego/acme"
2018-07-03 10:44:04 +00:00
"github.com/xenolf/lego/platform/config/env"
2018-04-06 15:04:03 +00:00
)
2018-09-17 13:16:03 +00:00
// Config is used to configure the creation of the DNSProvider
type Config struct {
Token string
PropagationTimeout time . Duration
PollingInterval time . Duration
HTTPClient * http . Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig ( ) * Config {
client := acme . HTTPClient
client . Timeout = env . GetOrDefaultSecond ( "DUCKDNS_HTTP_TIMEOUT" , 30 * time . Second )
return & Config {
PropagationTimeout : env . GetOrDefaultSecond ( "DUCKDNS_PROPAGATION_TIMEOUT" , acme . DefaultPropagationTimeout ) ,
PollingInterval : env . GetOrDefaultSecond ( "DUCKDNS_POLLING_INTERVAL" , acme . DefaultPollingInterval ) ,
HTTPClient : & client ,
}
}
2018-04-06 15:04:03 +00:00
// DNSProvider adds and removes the record for the DNS challenge
type DNSProvider struct {
2018-09-17 13:16:03 +00:00
config * Config
2018-04-06 15:04:03 +00:00
}
// NewDNSProvider returns a new DNS provider using
// environment variable DUCKDNS_TOKEN for adding and removing the DNS record.
func NewDNSProvider ( ) ( * DNSProvider , error ) {
2018-07-03 10:44:04 +00:00
values , err := env . Get ( "DUCKDNS_TOKEN" )
if err != nil {
2018-09-17 13:16:03 +00:00
return nil , fmt . Errorf ( "duckdns: %v" , err )
2018-07-03 10:44:04 +00:00
}
2018-04-06 15:04:03 +00:00
2018-09-17 13:16:03 +00:00
config := NewDefaultConfig ( )
config . Token = values [ "DUCKDNS_TOKEN" ]
return NewDNSProviderConfig ( config )
2018-04-06 15:04:03 +00:00
}
2018-09-17 13:16:03 +00:00
// NewDNSProviderCredentials uses the supplied credentials
// to return a DNSProvider instance configured for http://duckdns.org
// Deprecated
2018-07-23 15:30:03 +00:00
func NewDNSProviderCredentials ( token string ) ( * DNSProvider , error ) {
2018-09-17 13:16:03 +00:00
config := NewDefaultConfig ( )
config . Token = token
return NewDNSProviderConfig ( config )
}
// NewDNSProviderConfig return a DNSProvider instance configured for DuckDNS.
func NewDNSProviderConfig ( config * Config ) ( * DNSProvider , error ) {
if config == nil {
return nil , errors . New ( "duckdns: the configuration of the DNS provider is nil" )
2018-04-06 15:04:03 +00:00
}
2018-09-17 13:16:03 +00:00
if config . Token == "" {
return nil , errors . New ( "duckdns: credentials missing" )
}
return & DNSProvider { config : config } , nil
2018-04-06 15:04:03 +00:00
}
2018-10-10 14:28:04 +00:00
// Present creates a TXT record to fulfill the dns-01 challenge.
2018-07-23 15:30:03 +00:00
func ( d * DNSProvider ) Present ( domain , token , keyAuth string ) error {
_ , txtRecord , _ := acme . DNS01Record ( domain , keyAuth )
2018-09-17 13:16:03 +00:00
return updateTxtRecord ( domain , d . config . Token , txtRecord , false )
2018-07-23 15:30:03 +00:00
}
// CleanUp clears DuckDNS TXT record
func ( d * DNSProvider ) CleanUp ( domain , token , keyAuth string ) error {
2018-09-17 13:16:03 +00:00
return updateTxtRecord ( domain , d . config . Token , "" , true )
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func ( d * DNSProvider ) Timeout ( ) ( timeout , interval time . Duration ) {
return d . config . PropagationTimeout , d . config . PollingInterval
2018-04-06 15:04:03 +00:00
}
2018-07-23 15:30:03 +00:00
// updateTxtRecord Update the domains TXT record
// To update the TXT record we just need to make one simple get request.
// In DuckDNS you only have one TXT record shared with the domain and all sub domains.
func updateTxtRecord ( domain , token , txt string , clear bool ) error {
2018-10-23 09:18:02 +00:00
u , _ := url . Parse ( "https://www.duckdns.org/update" )
2018-07-23 15:30:03 +00:00
2018-10-23 09:18:02 +00:00
query := u . Query ( )
query . Set ( "domains" , getMainDomain ( domain ) )
query . Set ( "token" , token )
query . Set ( "clear" , strconv . FormatBool ( clear ) )
query . Set ( "txt" , txt )
u . RawQuery = query . Encode ( )
response , err := acme . HTTPClient . Get ( u . String ( ) )
2018-04-06 15:04:03 +00:00
if err != nil {
return err
}
defer response . Body . Close ( )
bodyBytes , err := ioutil . ReadAll ( response . Body )
if err != nil {
return err
}
2018-07-23 15:30:03 +00:00
2018-04-06 15:04:03 +00:00
body := string ( bodyBytes )
if body != "OK" {
2018-07-23 15:30:03 +00:00
return fmt . Errorf ( "request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]" , body , u )
2018-04-06 15:04:03 +00:00
}
return nil
}
2018-10-23 09:18:02 +00:00
// DuckDNS only lets you write to your subdomain
// so it must be in format subdomain.duckdns.org
// not in format subsubdomain.subdomain.duckdns.org
// so strip off everything that is not top 3 levels
func getMainDomain ( domain string ) string {
domain = acme . UnFqdn ( domain )
split := dns . Split ( domain )
if strings . HasSuffix ( strings . ToLower ( domain ) , "duckdns.org" ) {
if len ( split ) < 3 {
return ""
}
firstSubDomainIndex := split [ len ( split ) - 3 ]
return domain [ firstSubDomainIndex : ]
}
return domain [ split [ len ( split ) - 1 ] : ]
}